深入探究 JVM | Java 的内存区域解析

Java 虚拟机在执行Java程序的时候会把它管理的内存区域划为几部分,这一节我们就来解析一下Java的内存区域。


有的人把JVM管理的内存简单地分为堆内存和栈内存,这样分未免有些太肤浅了。
Java的内存区域主要分为五部分:

  • 程序计数器(PC)
  • 虚拟机栈(JVM Stack)
  • 本地方法栈(Native Method Stack)
  • Java 堆内存(Java Heap)
  • 方法区(Method Area)

Java内存区域
(图转自网络)

下面我们来解析这几个区域。

程序计数器

相信学过计算机组成原理的人都知道,CPU内部的寄存器中就包含一个程序计数器(x86下为eip寄存器,ARM下为R15寄存器),存放程序执行的下一条指令地址。在程序开始执行前,将程序指令序列的起始地址,即程序的第一条指令所在的内存单元地址送入PC,CPU按照PC的地址从内存中读取第一条指令。每一条指令执行时,CPU会自动修改PC的量至下一条指令的地址,指令之间的跳转离不开PC。JVM内存中的程序计数器也是这样的作用,它储存JVM当前执行bytecode的地址。

Java虚拟机允许多个线程同时执行指令。如果有多个线程正在执行指令,那么每个线程都会有一个程序计数器,它是线程私有的。在任意时刻,一个线程只允许执行一个方法的代码。每当执行到一条Java方法的指令时,程序计数器保存当前执行字节码的地址;若执行的为native方法,则PC的值为undefined。

Java 虚拟机栈

Java虚拟机栈也是线程私有的,每一条线程都拥有自己私有的Java 虚拟机栈,它与线程同时创建。它描述了Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至完成的过程,都对应一个栈帧从入栈到出栈的过程。关于栈帧详细的内容在后边复习虚拟机字节码执行引擎的时候再说吧。
Java 虚拟机栈在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的入栈和出栈之外,Java虚拟机栈不会再受其它因素的影响,因此栈帧可在系统的堆上分配内存(注意,是系统的Heap而不是Java Heap)。Java虚拟机栈所使用的内存不需要保证是连续的。

本地方法栈

本地方法栈和Java虚拟机栈的作用相似,Java虚拟机栈执行的是字节码,而本地方法栈执行的是native方法。本地方法栈使用传统的栈(C Stack)来支持native方法。在HotSpot JVM中Java虚拟机栈和本地方法栈合二为一。

Java 堆

在JVM中,Java 堆是可供各线程共享的运行时内存区域,是Java 虚拟机所管理的内存区域中最大的一块。此区域非常重要,几乎所有的对象实例和数组实例都要在Java堆上分配,但随着JIT编译器及逃逸分析技术的发展,也可能会被优化为栈上分配,高大上。。。)。同时,Java 堆也是发生GC收集的主要区域。
从内存回收的角度来看,它可以分为新生代老年代,再细分可以分为Eden Space,From Survivor Space,To Survivor Space区域。Java堆的容量可以是固定的,也可以随着需要来扩展,并且在用不到的时候自动收缩。

方法区

方法区是线程共享的,它储存了每一个类的结构信息,比如运行时常量池(runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些初始化的时候用到的特殊方法。方法区是堆的逻辑部分。
在JDK1.7及以前的HotSpot JVM中,方法区位于永久代(Permanent Generation,PermGen)中。由于永久代内可能会发生内存泄露或溢出等问题而导致的java.lang.OutOfMemoryError: PermGen ,JEP小组从JDK1.7开始就筹划移除永久代(JEP 122: Remove the Permanent Generation),并且在JDK 1.7中把字符串常量,符号引用等移出了永久代。到了Java 8,永久代被彻底地移出了JVM,取而代之的是元空间(Metaspace):

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.

所以从Java 8开始,方法区被移至 Metaspace 内。有关Metaspace的相关总结,见下一篇文章。

运行时常量池

运行时常量池是class文件中每一个类或接口的常量池表的运行时表示形式,是方法区的一部分。它包括了若干种不同的常量。常量池表存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。运行时常量池具有动态性,运行期间也可以将新的量放到运行时常量池中,典型的应用是String类的intern方法:

1
public native String intern()

JDK 1.7开始,字符串常量和符号引用等就被移出永久代:

  • 符号引用迁移至系统堆内存(Native Heap)
  • 字符串字面量迁移至Java堆(Java Heap)

下一篇文章我将会总结Java 8中的Metaspace相关知识


参考资料

文章目录
  1. 1. 程序计数器
  2. 2. Java 虚拟机栈
  3. 3. 本地方法栈
  4. 4. Java 堆
  5. 5. 方法区
  6. 6. 运行时常量池
  • 参考资料