【笔记】JVM内存管理(一) - 运行时内存区域

JVM的内存区域分为程序计数器、虚拟机栈、本地方法栈等。

JVM内存区域

程序计数器

程序计数器主要用于存储正在执行的虚拟机字节码指令的地址(不是直接存储指令)。Java虚拟机会按照PC的地址从内存中读取第一条指令,每一条指令执行时,CPU会自动修改PC的量至下一条指令的地址,指令之间的跳转离不开PC。

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

虚拟机栈

栈帧

每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。

Java 虚拟机栈在方法调用和返回中也扮演了很重要的角色。因为除了栈帧的入栈和出栈之外,Java虚拟机栈不会再受其它因素的影响,因此栈帧可在系统的堆上分配内存(注意,是系统的Heap而不是Java Heap)。Java虚拟机栈所使用的内存不需要保证是连续的。

可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小:

1
java -Xss=512M HackTheJava

该区域可能抛出以下异常:

  • 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
  • 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。

本地方法栈

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

JNI

Java堆

在JVM中,Java堆是可供各线程共享的运行时内存区域,是Java虚拟机所管理的内存区域中最大的一块,所有对象实例都在这里分配内存。同时,Java堆也是发生GC收集的主要区域。现代的垃圾收集器基本都是采用分代收集算法,主要思想是针对不同的对象采取不同的垃圾回收算法。虚拟机把Java堆分成以下三块:

  • 新生代(Young Generation)
  • 老年代(Old Generation)
  • 永久代(Permanent Generation)

当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。
新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间:

  • Eden(伊甸园)
  • From Survivor(幸存者)
  • To Survivor

heap区内存

Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。

可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。

1
java -Xms=1M -Xmx=2M HackTheJava

方法区

方法区是线程共享的,它储存了每一个类的结构信息,比如运行时常量池(runtime constant pool)、字段和方法数据、构造函数和普通方法的字节码内容,还包括一些初始化的时候用到的特殊方法。方法区是堆的逻辑部分。

和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。

对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。

在JDK1.7及以前的HotSpot JVM中,方法区位于永久代(Permanent Generation,PermGen)中。由于永久代内可能会发生内存泄露或溢出等问题而导致的java.lang.OutOfMemoryError: PermGen ,JEP小组从JDK1.7开始就筹划移除永久代,并且在JDK 1.7中把字符串常量,符号引用等移出了永久代。到了Java 8,永久代被彻底地移出了JVM,取而代之的是元空间(Metaspace)。

运行时常量池

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

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

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

直接内存

在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

参考

周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E8%99%9A%E6%8B%9F%E6%9C%BA.md
https://www.sczyh30.com/posts/Java/jvm-memory/#%E6%96%B9%E6%B3%95%E5%8C%BA