JVM 运行时堆内存如何分代?

463 阅读4分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

JVM 运行时内存分配

根据《Java虚拟机规范》,JVM 在执行 Java 程序的时候,会将它所管理的内存划分成若干个数据区域,它们各司其职,有各自的生命周期:

  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器。
  • Java虚拟机栈(Java Virtual MachineStack):每个方法被执行时,JVM 都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。方法被调用直至执行完毕的过程,对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 本地方法栈(Native Method Stacks):与虚拟机栈相似,区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
  • Java堆(Java Heap):今天的主角。它的作用就是存储对象实例,Java 程序运行过程中,几乎所有的对象都在这里。
  • 方法区(Method Area):用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
    • 运行时常量池(Runtime Constant Pool):方法区的一部分,用于存放编译期生成的各种字面量与符号引用。

注:在一些入门学习资料中,经常讲 JVM 所管理的内存笼统地分为堆和栈,它们分别指的是「Java堆」和「Java虚拟机栈」。

在以上的所有数据区域中,Java堆时最大的一块区域,也是被所有的线程共享的一块区域,在虚拟机启动时创建。堆内存里存储着几乎所有的对象实例,因此,它也是程序运行过程中,垃圾回收器进行垃圾回收的主要对象。

堆内存的分代

堆内存之所以要分代,主要是为了方便进行垃圾回收,因为目前大多数主流的垃圾回收器都是基于分代收集理论设计的。

分代收集,是一套符合大多数程序运行实际情况的经验法则,它基于以下两个假说:

  1. 绝大多数对象都是朝生夕灭的。
  2. 熬过越多次垃圾收集过程的对象就越难以消亡。

以上两个假说奠定了堆内存分代划分的基础。在 JDK 1.8 以后,根据被回收对象的年龄,堆内存主要被分为「新生代」和「老年代」两部分。

  • 新生代中保存的对象,大部分都是朝生夕灭的。
  • 老年代中保存的对象,都是难以消亡的对象。

分代带来的好处

通过这样划分,垃圾回收器可以以较高的频率,对新生代中的对象进行回收。在这部分回收过程中,只需要标记出少量继续留存的对象,而不是去标记大量将被回收的对象。对于老年代的对象,则以较低的频率进行回收。这样便兼顾了效率和内存使用率。

新生代又被划分为「Eden」、「From Survivor」和「To Survivor」三部分,这主要与新生代对象回收的算法有关。实际上,垃圾回收器针对堆内存的不同分区,不仅有不同的回收频率,也采用了不同的回收算法。

分代存在的缺陷和解决办法

在带来好处的同时,分代收集也存在很多缺陷。其中最显而易见的一点就是:对象都不是孤立的,即对象之间存在引用关系,当我们把对象划分到新生代和老年代两个区域以后,就会出现对象之间的跨代引用。

解决办法就是在新生代创建一个全局的记忆集(Remembered Set),它将老年代划分成若干小块,并标记处哪些老年代的小块内存存在跨代引用。当对新生代进行垃圾回收时,只要找到被标记的老年代的这一小部分内存进行扫描即可。

虽然在对象引用关系发生变化时维护记忆集带来了额外的开销,但相比起扫描整个老年代仍然是划算的。因为根据前面提到的两条假说,跨代引用相较于同代引用,所占的比例时很小的。

最后

  • 堆内存的分代,与垃圾收集器的垃圾回收算法存在着密切的关系。限于篇幅,垃圾回收算法的总结放在下一篇文章中。
  • 本文内容参考自《深入理解Java虚拟机(第3版)》,作者:周志明。