如果说收集算法(标记-清理、复制、标记-整理、分代收集)是内存回收的方法论,那毫无疑问,垃圾收集器就是内存回收的具体实现。
主要有7个gc器,如下图:
介绍
Serial收集器是单线程的收集器。
单线程:
- 它只会使用一个CPU或一条收集线程去完成垃圾收集工作;
- 在垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
Stop the world:
是Java虚拟机在后台自动发起和自动完成的,在用户不可见情况下把用户正常工作的线程全部停掉。
缺点
由于Stop The World,给用户带来不良体验,比如,计算机每运行一段时间就会暂停响应几分钟来处理垃圾收集,要是这应用在你的系统里,每用一会儿,就给你卡一下时间,你能忍?要是我,早翻脸了。
优点
- 简单而高效(与其他收集器的单线程比);
- 对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集工作,自然而然地可以获得最高的单线程收集效率。
应用场景
- Java虚拟机运行在Client模式下的默认新生代收集器;
- 在用户的桌面应用场景中,停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,不频繁发生,是可接受的(我们使用电脑也总归遇到一些卡顿的时候,别慌,拿起手机刷会儿微博抖音,再抬头,咦,这么快就好了,毫秒级别无感知,还是能接受的嘛)。
Serial/Serial Old收集器运行示意图
介绍
ParNew收集器是Serial收集器多线程版本(是GC线程的多线程,并行)。
并行:
Parallel指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态(多个处理器同时处理多条指令);
并发:
Concurrent指用户线程与垃圾收集线程同时执行(但并不一定是并行的,可能交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上(同一时刻只能有一条指令执行,多个进程指令是交替执行)。
缺点
在单CPU的环境中,跟上面的Serial老大哥比,绝对逊色不少,毕竟Serial单线程一条路走到黑!ParNew甚至会出现线程交互的开销,资源消耗大。
优点
- 除了Serial收集器外,只有ParNew收集器能与CMS收集器配合工作。CMS的备胎之一,没辙,其他都不合适。
- CMS(Concurrent Mark Sweep)第一次实现让垃圾收集线程与用户线程(基本上)同时工作。
应用场景
运行在Server模式下的Java虚拟机首选新生代收集器。
ParNew/Serial Old收集器运行示意图
参数控制
- 使用
-XX:+UseConcMarkSweepGC
选项后默认新生代收集器为ParNew收集器; - 使用
-XX:+UseParNewGC
选项强制指定使用ParNew收集器; - 使用
-XX:ParallelGCThreads
参数限制垃圾收集的线程数;
介绍
Parallel Scavenge收集器是一个新生代收集器,使用复制算法的收集器,并行的多线程收集器。更关注吞吐量。
吞吐量:
CPU用于运行用户代码的时间与cpu总消耗时间的比值,即
如虚拟机总共运行100分钟,垃圾收集花费1分钟,则吞吐量是99%,吞吐量高效率利用CPU时间,尽快完成程序的运算任务,主要适合后台运算而不需要太多交互的任务。
停顿时间:
如CMS等收集器重心是关注如何尽可能缩短垃圾收集时用户线程的停顿时间,停顿时间越短就越适合需要与用户交互的程序,良好的响应速度可以提升用户体验(适合交互),你想,我打开一个程序和系统,还没反应过来就把你所想要看到的呈现眼前,贼带劲!
参数控制
1)用户精确控制吞吐量
- 使用
-XX:MaxGCPauseMillis
参数:控制最大垃圾收集停顿时间。 - 使用
-XX:GCTimeRatio
参数:直接设置吞吐量大小。 - 使用
-XX:+UseAdaptiveSizePolicy
开关参数:GC自适应的调节策略。
2)MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器尽可能保证内存回收时间不超过设定值。
GC停顿时间:
缩短牺牲吞吐量和新生代空间——若将MaxGCPauseMillis该值调小带来的问题:系统把新生代调小一些,收集发生更频繁一些,吞吐量下降。
GCTimeRatio:
参数值是一个大于0且小于100的整数,即垃圾收集时间占总时间的比率,相当于吞吐量的倒数。如设置为19,则最大GC时间占1/(1+19)=5%,默认值为99.则最大允许1/(1+99)=1%的垃圾收集时间。
UseAdaptiveSizePolicy:
开关参数,JVM会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量。自适应调节策略是Parallel Scavenge收集器与ParNew收集器的重要区别。
应用场景
主要适合后台运算而不需要太多交互的任务
Serial Old收集器介绍
Serial Old收集器是Serial收集器的老年代版本,是一个单线程收集器,使用“标记-整理”算法。
应用场景
- 主要给Client模式下的VM使用。
- 若在Server模式下用,两大用途:a)在JDK1.5及之前的版本中与Parallel Scavenge收集器搭配使用;b)作为CMS收集器备选,并在Concurrent Mode Failure时使用。
Serial/Serial Old收集器运行示意图
介绍
Parallel Old是Parallel Scavenge收集器的老年代版本,是一个多线程收集器,使用“标记-整理”算法,在JDK1.6开始提供。
应用场景
注重吞吐量以及CPU资源敏感的场合,优先考虑Parallel Scavenge + Parallel Old收集器,适合吞吐量优先。
Parallel Scavenge /Parallel Old收集器运行示意图
介绍
CMS收集器(Concurrent Mark Sweep)是一种以获取最短回收停顿时间为目标的收集器,是基于“标记-清除”算法。
CMS的整个过程有4个步骤
- 初始标记:CMS initial mark仅仅是标记一下GC Roots能直接关联的对象,速度快;需要stop the world
- 并发标记:CMS concurrent mark是进行GC Roots Tracing的过程;
- 重新标记:CMS remark是修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,停顿时间比初始标记长,比并发标记短;需要stop the world
- 并发清除:CMS concurrent sweep,清除算法会在收集结束时产生大量空间碎片,有可能导致没有足够大的连续空间来分配当前对象而触发一次Full GC。
缺点
- CMS收集器对CPU资源非常敏感;
- CMS收集器无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败(备选用Serial Old)而导致另一次Full GC的产生;
- CMS是一款基于“标记-清除”算法的收集器,在收集结束后会产生大量空间碎片。
缺点具体分析
- 对CPU资源敏感:在并发阶段会占用一部分线程而导致应用程序变慢,总吞吐量降低;(解决方法是“增量式并发收集器”,但不提倡使用,i-CMS收集器是与单CPU年代PC机操作系统使用抢占式模拟多任务机制的思想,在并发标记、清理的时候让GC线程、用户线程交替执行,尽量减少GC线程的独占资源的时间)
- 无法处理浮动垃圾:CMS并发清理阶段用户线程还在运行,会产生新的垃圾,这部分垃圾出现在标记过程之后,CMS无法在当次收集中处理它们,只好留到下一次GC时再处理。CMS需要预留一部分提供并发收集时的程序运行使用,CMS收集时老年代不能填满再收集。
- 收集后产生大量空间碎片:“标记-清除”算法的缺点,解决方案是使用
-XX:+UseCMSCompactAtFullCollection
和-XX:CMSFullGCsBeforeCompaction
参数
优点
- 并发收集;
- 低停顿。
应用场景
在互联网站或者B/S系统的服务端上,重视服务的响应速度,希望系统停顿时间最短,给用户带来较好的体验。
参数控制
- 使用
-XX:CMSInitiatingOccupancyFraction
参数:提高触发老年代CMS垃圾回收的百分比; - 使用
-XX:+UseCMSCompactAtFullCollection
开关参数:默认开启,用于CMS收集器要进行Full GC时开启内存碎片合并整理过程,非并发的过程; - 使用
-XX:CMSFullGCsBeforeCompaction
参数:用于设置执行多少次不压缩的Full GC后,紧接着一次带压缩的(默认为0,表示每次进入Full GC时就进行碎片整理)
CMS收集器运行时示意图
介绍
G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器,为了代替JDK1.5中发布的CMS收集器。将整个Java堆划分为多个大小相等的独立区域(Region),保留新生代和老年代概念,但不再是物理隔离,是一部分Region的集合(不需要连续)。
优点
并发与并行;分代收集;空间整合;可预测的停顿
- 并发与并行:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU缩短storp-the-world停顿时间;G1可通过并发的方式使得java程序运行;
- 分代收集:可以独立管理整个GC堆,采用不同的方式处理新创建的对象和已经存活一段时间、熬过多次GC的旧对象以获取更好的收集效果;
- 空间整合:整体上基于“标记-整理”算法,局部(两个Region之间)基于“复制”算法,G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存,有利于程序长时间运行,分配大对象时不会因为无法获得连续内存空间而提前触发下一次GC;
- 可预测的停顿:相比于CMS的另一优势,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。因为可以有计划地避免在整个java堆上进行全区域的垃圾收集。G1跟踪各个Region内垃圾堆积的价值大小(回收所获得的空间大小+回收所需时间的经验值),在后台维护一个优先列表,根据允许的收集时间,回收价值最大的Region(Garbage-First的由来)。
G1将内存“化整为零”的思路
Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,JVM都是通过Remember Set来避免全堆扫描。G1中每个Region中都有一个与之对应的Remember Set:
- JVM发现程序对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作;
- 检查Reference引用的对象是否处于不同的Region之中;如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remember Set之中;
- 当进行内存回收时,在GC根节点的枚举范围中加入Remember Set即可保证不对全堆扫描;
G1收集器运作的步骤:
- 初始标记:initial marking,标记一下GC Roots能直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,需要停顿线程,耗时短;
- 并发标记:concurrent marking,从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时长,但可与用户程序并发执行;
- 最终标记:final marking,修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,对象变化记录存在线程Remember Set Logs中,然后把这些数据合并到Remember Set中,该阶段停顿线程,但是可并行执行;
- 筛选回收:live data counting and evacuation,对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。
G1收集器运行示意图:
介绍
- Safepoint:在HotSpot的实现中,使用一组称为OopMap的数据结构,在类加载完成的时候,HotSpot把对象内什么偏移量上是什么类型的数据都计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用,其实就是快速且准确的找到GC Roots枚举,这些特定的位置就是安全点。
- GC点:程序执行时并非在所有的地方都能停顿下来开始GC,只有到达安全点时才能暂停。
- 安全点的选定:标准是“是否具有让程序长时间执行的特征”,不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。
安全点的停顿
如何在GC发生时让所有线程都“跑”到最近的安全点停顿?两种方案:抢先式中断和主动式中断。
- 抢先式中断:不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。(现在几乎不用)
- 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
安全点的作用
- safepoint保证程序执行时,在不太长的时间内就会遇到可进入的GC的safepoint。
- 若程序不执行,则没有分配CPU时间,如线程处于Sleep或Blocked状态,无法响应JVM的中断请求,此时需要安全区域解决,在一段代码片段中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。
GC器的参数
GC器的使用
参考书籍
《深入理解Java虚拟机》