JVM(二)JVM之垃圾收集
一、引言
在Java语言中,由于内存分配和回收由JVM自动进行管理,而不需要手动进行内存管理,这就避免了许多常见的内存管理问题,如内存泄漏、悬挂指针等问题。但是,随着程序运行过程中对象的不断创建和销毁,JVM所管理的内存也会不断产生垃圾。如果不及时清理这些垃圾,就会导致内存不足,甚至导致程序崩溃。因此,垃圾收集成为JVM必不可少的一部分,用于自动地回收无用对象所占用的内存空间,使得可用内存得到最大的利用,从而保证程序的正常运行。
二、垃圾标记
既然想要清理垃圾,那么首先要能够找到垃圾,那么有哪些方法可确定一个对象是否是垃圾呢?
1.引用计数法
引用计数法是一种垃圾收集算法,其核心思想是通过维护每个对象的引用计数器来判断对象是否还有被引用的可能,从而决定是否回收该对象
具体来说,每当一个对象被引用时,该对象的引用计数器就会加1;当一个对象的引用失效时,该对象的引用计数器就会减1。当某个对象的引用计数器为0时,表示该对象没有任何引用指向它,即无法再被访问,因此可以被回收
弊端:如果两个对象相互引用,它们的引用计数器都不为0,但是它们已经不再被程序所使用,因此这两个对象将永远不会被回收
2.可达性分析法
可达性分析法是Java虚拟机中最常用的垃圾收集算法之一,也是垃圾收集器判断对象是否存活的标准。该算法的基本思想是通过一系列称为”GC Roots”的对象作为起始点集,从这些节点开始向下搜索,搜索过程中所遇到的对象都被视为存活的对象,反之则被视为垃圾对象。简单来说,如果一个对象不可达(即没有被引用),那么这个对象就被判定为垃圾对象
GC Roots一般包括以下几种:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
在可达性分析法中,垃圾收集器会从GC Roots开始,遍历所有的引用关系,如果一个对象没有被引用,就将其判定为垃圾对象,进而对其进行回收。这样可以保证被引用的对象不会被误判为垃圾对象,从而被回收。同时,也可以确保不被引用的对象被及时回收,从而释放内存空间
需要注意的是,标记算法找到的都是可以继续存活的对象,没有被标记到的对象才会被回收
三、垃圾收集算法
垃圾标记算法可以找到哪些垃圾需要被清除,那么如何清除这些垃圾呢?
1.标记-清除(Mark-Sweep)
分为标记和清除两个阶段
在标记阶段,从根节点开始遍历堆内存中所有的对象,标记所有被引用的对象为“存活”对象,未被标记的对象为“垃圾”对象
在清除阶段,遍历整个堆内存,回收未被标记的对象所占用的内存。回收完成后,堆内存中将只有存活对象存在
标记:蓝色部分为能够被标记的对象,灰色是不可达对象
清除:将不可达对象都清除掉
优点:实现简单,可以解决循环引用问题
缺点:产生内存碎片,需要扫描整个堆来处理,所以效率略差
2.标记-复制(Mark-Copy)
在内存中划分出两块儿相同大小的区域,每次只使用其中一块儿,清理垃圾时就把其中一块儿上的存活对象复制到另一块儿上
优点:
- 无内存碎片
- 实现简单,运行高效
缺点:
- 需要额外空间
- 存活对象很多时,复制成本高,影响性能
3.标记-整理(Mark-Compact)
同样是先标记,但是与标记-清除不同的是,标记之后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
4.分代收集
前面提到过,JVM中的堆内存结构就是为了配合垃圾收集而划分的,那么在堆里面到底是怎么进行垃圾收集的呢?
在JVM中,采用了垃圾分代收集的概念,将堆内存划分为了Young区和Old区,每个区域采用的垃圾收集算法都不相同
在Young区中,由于特意划分出了From区和To区,所以很明显,使用的是标记-复制算法
在Old区中,则根据实际情况,可以选择标记-清除或者标记-整理算法
四、垃圾收集器
上面提到的垃圾收集算法都是理论上的算法,那么在实际当中,肯定要有对应的落地实现来完成这些算法,这就是垃圾收集器,下面整体看一下不同垃圾收集器工作的区域和他们之间的配合关系
在介绍不同的垃圾收集器之前,需要先引入一个概念:STW
STW(Stop-The-World)指的是停顿式垃圾收集器在进行垃圾回收时,需要暂停应用程序的执行,直到垃圾回收完成。在这个暂停期间,所有应用程序的线程都被挂起,无法继续执行。因此,STW会影响应用程序的响应时间和吞吐量。
所以在垃圾收集器中,如果想提高吞吐量和响应时间就要尽量减少STW的时间
1.Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择
它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线程
优点:简单高效,拥有很高的单线程手机效率
缺点:收集过程中会暂停其他所有线程
算法:标记-复制算法
适用范围:新生代
应用:Client模式下的默认新生代收集器
2.ParNew收集器
可以看作Serial的多线程版本
优点:多CPU时比Serial效率高
缺点:收集过程中会暂停其他所有线程,单CPU时比Serial效率差
算法:标记-复制算法
适用范围:新生代
应用:运行在Server模式下的虚拟机中首选的新生代收集器
3.Parallel Scavenge收集器
Parallel Scavenge是一个新生代收集器,使用复制算法,支持并行,更加关注吞吐量
吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)
一些参数和它相关
1 | -XX:MaxGCPauseMillis #控制最大的垃圾收集停顿时间 |
4.Serial Old收集器
相当于Serial的老年代版本,也是单线程收集器,但是采用的是标记-整理算法,运行过程和Serial一样
它能够配合上述三种新生代垃圾收集器一起使用
5.Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法
也是吞吐量优先的垃圾收集器
6.CMS收集器
CMS(Concurrent Mark Sweep)垃圾收集器是一种以获取最短回收停顿时间为目标的低延迟垃圾收集器。它是一种并发收集器,可以和用户线程一起工作,尽量减少停顿时间,提高系统的响应速度,使用标记-清除算法
CMS垃圾收集器的工作流程如下:
- 初始标记:标记GC Roots直接关联的对象,速度较快
- 并发标记:和用户线程一起标记存活对象,速度较慢
- 重新标记:暂停用户线程,标记在并发标记期间产生的新的存活对象,速度较快
- 并发清除:和用户线程一起清除垃圾对象,速度较快
得益于它采用的并发标记策略,它具有以下优点
- 并发收集:CMS垃圾收集器采用标记-清除算法,其中标记阶段是在应用程序线程运行的同时进行的,因此不需要停顿整个应用程序,从而减少了垃圾收集的停顿时间,使得垃圾收集对应用程序的影响更小。
- 低延迟:CMS垃圾收集器具有低延迟的特点,因为它只在垃圾收集过程的标记和清除阶段停顿应用程序线程,而在其他阶段都是与应用程序线程并发执行的。
- 内存回收效率高:CMS垃圾收集器在堆内存空间不足时,会触发一次CMS收集,只会回收那些无法再分配的空间,从而避免了Full GC的发生,提高了内存回收效率。
- 适用于响应时间要求高的应用程序:由于CMS垃圾收集器具有低延迟的特点,因此适用于响应时间要求高的应用程序,如Web应用等。
但是采用标记-清除算法也导致了它会产生内存碎片的问题
7.G1收集器
G1收集器是在JDK 7 Update 4发布之后引入的。
与传统的分代收集器不同,G1收集器把Java堆分成多个大小相等的独立区域(Region),并按需收集这些独立区域,同时避免全局垃圾收集带来的长时间停顿问题,它的实现原理如下:
- 将堆内存分割成若干个大小相等的内存区域(Region),每个Region可以是Eden区、Survivor区或Old区
- 在Young GC时,G1收集器只对部分的Young区域进行回收,而非整个Young区域,以此减少收集的时间
- 在Old GC时,G1收集器会选择多个Region(包括Young区域和Old区域)进行回收,而非全局扫描整个堆
- 在收集时,G1收集器会优先选择回收垃圾最多的Region
- G1收集器采用并发标记算法,可以在应用程序运行的同时进行垃圾收集
G1收集器的实现原理比较复杂,但它的主要优点是可以大幅度减少STW的时间,并且能够自适应地调整分代大小、分配回收的内存等参数,以提高性能和稳定性
G1收集器下的堆结构和其他垃圾收集器下的堆结构对比

G1的垃圾收集过程如下:
- 初始标记(Initial Mark):STW(Stop the World)的过程,会标记出根对象和部分老年代对象的存活状态,这些存活的对象被标记为”Marked”
- 并发标记(Concurrent Mark):在堆中进行并发标记,此时应用程序线程和GC线程并发执行,G1会找到所有存活的对象并标记为”Marked”
- 最终标记(Final Mark):STW的过程,收集器会对整个堆进行扫描,以确保标记出所有存活的对象
- 筛选回收(Live Data Counting And Evacuation):对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
在以上过程中,G1收集器会根据各个Region的回收价值,先回收回收价值高的Region,这样可以最大程度的降低GC的停顿时间,并尽可能减少垃圾收集的次数,从而提高应用程序的整体性能
8.垃圾收集器分类
串行收集器
收集器:Serial、Serial Old
特征:只能有一个垃圾回收线程执行,用户线程暂停
适用:适用于内存比较小的嵌入式设备
并行收集器(吞吐量优先)
收集器:Parallel Scanvenge、Parallel Old
特征:多条垃圾收集线程并行工作,但此时用户仍然处于等待状态
适用:适用于科学计算,后台处理等弱交互的场景
并发收集器(停顿时间优先)
收集器:CMS、G1
特征:用户线程和垃圾收集线程同时执行(但并不一定是并行的,可能是交替执行的),垃圾收集线程在执行的时候不会停顿用户线程的运行
适用:适用于对于时间有要求的场景,比如web
9.如何选择垃圾收集器
选择垃圾收集器的标准
- 优先调整堆的大小让服务器自己来选择
- 如果内存小于100M,使用串行收集器
- 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
- 如果允许停顿时间超过1秒,选择并行或JVM自己选
- 如果响应时间最重要,并且不能超过1秒,使用并发收集器
那么对于G1收集器,何时使用呢?
- 50%以上的堆被存活对象占用:如果堆中存活对象占用堆的50%以上,那么使用传统的标记-清除、标记-整理或复制算法可能会导致长时间的暂停。而G1收集器是一款基于分代思想的收集器,它可以把堆空间分为多个不同大小的区域,每个区域都可以独立地进行垃圾回收。这样就可以避免在一次垃圾回收中需要同时处理大量存活对象的情况,降低了STW的时间
- 对象分配和晋升的速度变化非常大:G1垃圾收集器可以通过智能地选择要处理的回收区域,动态调整回收集和堆的大小,优化垃圾回收的效率,从而尽可能减少长时间的STW
- 垃圾回收时间比较长:如果垃圾回收时间比较长,会导致应用程序停顿时间过长,影响用户体验。G1采用了可预测停顿时间的机制,可以根据用户设定的目标停顿时间,合理地分配各个阶段的垃圾回收时间,尽可能减少停顿时间,提高应用程序的性能和可用性
五、总结
根据上面的介绍可知,没有万能的垃圾收集器,在不同的业务环境下可以考虑采用不同的垃圾收集器
相比较而言,CMS和G1这两个并发的垃圾收集器具有比较好的适用性,因为他们采用了停顿时间优先的策略,可以并发标记存活对象,从而减少了STW时间
在JDK1.8中,Young区默认采用Serial垃圾收集器,Old区默认采用CMS收集器
在JDK1.9中,默认采用G1收集器





