本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
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堆时最大的一块区域,也是被所有的线程共享的一块区域,在虚拟机启动时创建。堆内存里存储着几乎所有的对象实例,因此,它也是程序运行过程中,垃圾回收器进行垃圾回收的主要对象。
堆内存的分代
堆内存之所以要分代,主要是为了方便进行垃圾回收,因为目前大多数主流的垃圾回收器都是基于分代收集理论设计的。
分代收集,是一套符合大多数程序运行实际情况的经验法则,它基于以下两个假说:
- 绝大多数对象都是朝生夕灭的。
- 熬过越多次垃圾收集过程的对象就越难以消亡。
以上两个假说奠定了堆内存分代划分的基础。在 JDK 1.8 以后,根据被回收对象的年龄,堆内存主要被分为「新生代」和「老年代」两部分。
- 新生代中保存的对象,大部分都是朝生夕灭的。
- 老年代中保存的对象,都是难以消亡的对象。
分代带来的好处
通过这样划分,垃圾回收器可以以较高的频率,对新生代中的对象进行回收。在这部分回收过程中,只需要标记出少量继续留存的对象,而不是去标记大量将被回收的对象。对于老年代的对象,则以较低的频率进行回收。这样便兼顾了效率和内存使用率。
新生代又被划分为「Eden」、「From Survivor」和「To Survivor」三部分,这主要与新生代对象回收的算法有关。实际上,垃圾回收器针对堆内存的不同分区,不仅有不同的回收频率,也采用了不同的回收算法。
分代存在的缺陷和解决办法
在带来好处的同时,分代收集也存在很多缺陷。其中最显而易见的一点就是:对象都不是孤立的,即对象之间存在引用关系,当我们把对象划分到新生代和老年代两个区域以后,就会出现对象之间的跨代引用。
解决办法就是在新生代创建一个全局的记忆集(Remembered Set),它将老年代划分成若干小块,并标记处哪些老年代的小块内存存在跨代引用。当对新生代进行垃圾回收时,只要找到被标记的老年代的这一小部分内存进行扫描即可。
虽然在对象引用关系发生变化时维护记忆集带来了额外的开销,但相比起扫描整个老年代仍然是划算的。因为根据前面提到的两条假说,跨代引用相较于同代引用,所占的比例时很小的。
最后
- 堆内存的分代,与垃圾收集器的垃圾回收算法存在着密切的关系。限于篇幅,垃圾回收算法的总结放在下一篇文章中。
- 本文内容参考自《深入理解Java虚拟机(第3版)》,作者:周志明。