Java虚拟机(JVM)垃圾回收机制是Java语言的重要组成部分,它负责自动管理内存,确保已不再使用的对象能够被及时回收。以下是一个详细的学习文档,帮助你理解JVM垃圾回收机制的原理和实现。
一、JVM内存模型
JVM内存模型包括以下几个主要区域:
- 堆(Heap):存放所有的对象实例,垃圾回收主要发生在堆上。
- 方法区(Method Area):存储类信息、常量、静态变量等。
- 栈(Stack):每个线程一个栈,存储局部变量和部分结果。
- 本地方法栈(Native Method Stack):为native方法服务。
- 程序计数器(Program Counter Register):当前线程执行的字节码的行号指示器。
二、标记需要回收的对象
1、引用计数法
这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1;当引用失效时,计数器值-1。任何时刻计数值为0的对象就是不可能再被使用的。这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况。
2、可达性分析法
这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链(即GC Roots到对象不可达)时,则证明此对象是不可用的。
那么问题又来了,如何选取GCRoots对象呢?在Java语言中,可以作为GCRoots的对象包括下面几种:
(1). 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象。
(2). 方法区中的类静态属性引用的对象。
(3). 方法区中常量引用的对象。
(4). 本地方法栈中JNI(Native方法)引用的对象。
三、垃圾回收算法
1. 标记-清除算法(Mark-Sweep)
这是最基础的算法,标记-清除算法就如同它的名字样,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。这种算法的不足主要体现在效率和空间,从效率的角度讲,标记和清除两个过程的效率都不高;从空间的角度讲,标记清除后会产生大量不连续的内存碎片, 内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。标记-清除算法执行过程如图:
- 优点:简单,容易实现。
- 缺点:标记和清除过程效率较低,可能产生大量内存碎片。
2. 复制算法(Copying)
复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配时也不需要考虑内存碎片等复杂情况,只需要移动指针,按照顺序分配即可。复制算法的执行过程如图:
不过这种算法有个缺点,内存缩小为了原来的一半,这样代价太高了。现在的商用虚拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。
- 优点:消除了碎片问题,适用于新生代对象。
- 缺点:需要额外的内存空间。
3. 标记-压缩算法(Mark-Compact)
复制算法在对象存活率较高的场景下要进行大量的复制操作,效率很低。万一对象100%存活,那么需要有额外的空间进行分配担保。老年代都是不易被回收的对象,对象存活率高,因此一般不能直接选用复制算法。根据老年代的特点,有人提出了另外一种标记-整理算法,过程与标记-清除算法一样,不过不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界以外的内存。标记-整理算法的工作过程如图:、
- 优点:解决了内存碎片问题。
- 缺点:对象移动的成本较高。
4. 分代收集算法(Generational Collecting)
现代商用虚拟机基本都采用分代收集算法来进行垃圾回收。这种算法没什么特别的,无非是上面内容的结合罢了,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。
大批对象死去、少量对象存活的(新生代),使用复制算法,复制成本低;
对象存活率高、没有额外空间进行分配担保的(老年代),采用标记-清理算法或者标记-整理算法。
- 优点:充分利用了对象的生命周期特征,性能较高。
- 缺点:实现复杂。
四、JVM垃圾收集器
JVM提供了多种垃圾收集器,每种收集器适用于不同的场景:
- Serial收集器:适用于单线程环境,简单高效。
- Parallel收集器:适用于多线程环境,吞吐量高。
- CMS(Concurrent Mark-Sweep)收集器:低延迟收集器,适用于对响应时间要求高的应用。
- G1(Garbage-First)收集器:适用于多核CPU和大内存,能够提供可预测的停顿时间。
五、垃圾收集器的调优
垃圾收集器调优包括以下几个方面:
- 选择合适的垃圾收集器:根据应用场景选择合适的垃圾收集器。
- 调整堆的大小:合理设置初始堆大小(-Xms)和最大堆大小(-Xmx)。
- 调整新生代和老年代的比例:设置新生代和老年代的比例(-XX)。
- 设置年轻代和老年代的收集器:可以分别为年轻代和老年代设置不同的收集器(-XX:+UseParNewGC、-XX:+UseConcMarkSweepGC等)。
六、调试和监控工具
JVM提供了多种工具来调试和监控垃圾收集器的性能:
- jstat:监控JVM的各种运行时统计信息。
- jmap:生成堆转储,分析堆内存。
- jconsole:图形化监控工具。
- VisualVM:强大的图形化监控和调优工具。
结语
JVM垃圾回收机制是Java程序性能优化的重要方面,通过理解垃圾回收算法、选择合适的垃圾收集器、合理配置参数和使用监控工具,可以有效提升Java应用的性能和稳定性。