垃圾收集算法实现与垃圾收集器(笔记)

一、HotSpot中垃圾收集的算法实现

1、枚举根节点

1.1、从可达性分析中从GC Roots节点找引用链这个操作为例,可作为GC Roots的节点主要在全局性的引用(例如常量和类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在很多应用仅仅方法区就有数百兆,逐个检查所有引用的话必然会消耗很多时间。

1.2、可达性分析对执行时间的敏感还体现在停顿上,因为这项工作分析工作必须在一个能确保一致性的快照中进行——这里一致性的意思是指在整个分析期间对象引用关系不再发生变化。这点是导致GC 进行时必须停顿所有Java执行线程(“Stop The World”)的重要原因。

1.3、准确式GC:Java虚拟机不需要检查所有执行上下文和全局的引用变量,它是有办法知道哪些地方存着对象的引用(在HotSpot中,使用一组称为OopMap数据结构来达到这个目的,在类加载完成的时候HotSpot就把对象内什么偏移量上存的是什么类型的数据计算出来,在JIT编译的过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用)。

2、安全点

2.1、定义:HotSpot没有为每条指令都生成OopMap(需要大量的额外空间),它只是在“特定的位置”记录了这些信息,这些位置就称为“安全点”。程序只有执行到安全点才能停下来进行GC工作。(SafePoint既不能太少,以致于让GC等待的时间太长,也不能太多以致于过分增大运行时的负荷。)

2.2、选定标准——“是否具有让程序长时间执行的特征”

每条指令的执行时间都非常短暂,程序不太可能因为指令流长度太长这个原因而长时间运行。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。(?)

这些特定的位置主要在:
A、循环的末尾
B、方法临返回前 / 调用方法的call指令后
C、可能抛异常的位置

2.3、线程如何跑到安全点停顿

A、抢先式中断:在GC发生时强行中断所有线程,如果发现线程中断的地方不在安全点上,再恢复线程,让它跑到安全点。

B、主动式中断(主流):GC不需要对线程进行操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。(具体的方法就是,虚拟机把某个内存页设置为不可读,线程执行到相应的test指令时就会产生一个自陷异常信号,在预先注册的异常处理器中暂停线程实现等待)

3、安全区域

安全点的扩展。解决程序“不执行”的问题(即线程处于sleep或者Blocked等状态,无法响应JVM的中断请求,“走”到安全的地方去挂起,JVM也不太可能等待线程重新被分配CPU空间)

定义:安全区域是指在一段代码片段之中,引用关系不会发生改变。在这个区域中的任何地方开始GC都是安全的。

过程:当线程执行到SafeRegion中的代码时,首先标志自己已经进入Safe Region了,那样,当在这段时间里JVM要发起GC时就不用管标识自己为Safe Region状态的线程了。当线程要离开安全区域时,它要检查系统是否已经完成了跟节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。

二、垃圾收集器

不同厂商、不同版本使用的垃圾收集器都不太一样。HotSpot的垃圾收集器如下图(图中相互连线表示可以搭配使用,虚拟机所处的区域则表示它是老年代还是新生代的收集器)——目前还有一种适用于任何场景的垃圾收集器,因此我们要根据不同的场景来选择合适的垃圾收集器。

1、Serial收集器

1.1、特性:单线程收集器。A、只会使用一个CPU或一条收集线程去完成垃圾收集工作;B、在它进行垃圾收集期间必须暂停其他所有的工作线程,直到它收集结束。

1.2、不足:“Stop The World”会带来很差的用户体验。

1.3、优点:简单高效。(因此它目前还是虚拟机运行在Client模式下的默认新生代收集器。)

对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集m,自然可以获得最高的效率。在用户的桌面应用场景中,分配给虚拟机的内存一般不会很大,收集几十兆甚至几百兆的新生代,停顿时间完全可以控制在几十毫秒最多一百毫秒之内。

1.4、使用场景:Client模式+新生代

2、ParNew收集器

2.1、特性:多线程收集器(Serial收集器的多线程版本)。使用多条线程进行垃圾收集。

2.2、不足:ParNew在单CPU的环境中绝对不会有比Serial收集器更好的效果。

2.3、优点:A、目前只有它和Serial能与CMS收集器配合工作(CMS第一次实现了让垃圾收集线程与用户线程(基本上)同时工作);B、随着可以使用的CPU的数量的增加,它可以更有效地利用系统资源。——它默认开启的收集线程数与CPU的数量相同,也可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

2.4、使用场景:Server模式+新生代

3、Parallel Scavenge收集器

3.1、特性:关注点不在于尽可能缩短垃圾收集时用户线程的停顿时间,而是达到一个可控的吞吐量。

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

停顿时间越短越适合需要与用户交互的程序,良好的响应速度可以提升用户体验,而高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

3.2、参数设置

A、-XX:MaxGCPauseMillis(如果更关注最大停顿时间,就设置这个参数)

大于0的毫秒数,收集器尽可能地保证内存回收时间不超过设定值。(但是并不是把这个参数设置得越小越好,因为GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的。比如,系统把新生代调小一些,收集300M的空间肯定要比收集500M的空间来得快。但是这也直接导致垃圾收集次数变得更加频繁。)

B、-XX:GCTimeRatio(如果更关注最大吞吐量,就设置这个参数)

大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19))。默认值为99,即允许最大1%的垃圾收集时间。

C、-XX:SurvivorRatio

打开这个参数,就不需要手动去设定新生代的大小、Eden与Survivor的比例、晋升老年代对象的大小等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。——GC自适应的调节策略

3.3、优点:“吞吐量优先”收集器

3.4、适用场景:Server模式+新生代

4、Serial Old 收集器

4.1、特性:Serial收集器的老年代版本

4.2、适用场景:

A、Client模式+老年代(主要是给Client模式下的虚拟机用)

B、Server模式+老年代:(Server模式下的两大用途:一是在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用;二是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。)

5、Parallel Old 收集器

5.1、特性:Parallel Scavenge收集器的老年代版本。使用多线程以及“标记-整理算法”。

5.2、优点:与Parallel Scavenge配合使用。之前没有Parallel Old的时候,如果新生代选择了Parallel Scavenge,那么老年代就只能使用Serial Old,但是由于Serial Old在服务器应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果。

5.3、优点:“吞吐量优先”收集器

5.4、使用场景:Server模式+老年代

6、CMS收集器——Concurrent Mark Sweep(并发低停顿收集器)

6.1、开发目标:获得最短回收停顿时间。

6.2、实际应用场景:互联网站或者B/Z系统的服务端等尤其重视服务响应速度的应用上。

6.3、特性:使用“标记-清除”算法

6.4、具体的收集过程

初始标记(CMS initial mark):

需要“Stop The World”。仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。

并发标记(CMS concurrent mark):

不需要“Stop The World”。进行GC Roots tracing,这个需要的时间比较长,但是由于这个阶段允许并发执行用户进程,因此问题不大。

重新标记(CMS remark):

需要“Stop The World”。修正并发标记期间因用户程序继续运作而导致产生变动的那一部分对象的标记记录。这个停顿时间会比初始标记的时间要长一些,但是远比并发标记的时间短。

并发清除(CMS concurrent sweep):

不需要“Stop The World”。回收标记的对象。

6.5、优点:并发收集、低停顿。

6.6、缺点:

A、CMS收集器对CPU资源非常敏感。面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4。(“增量式并发收集器”可以在并发标记和清除的时候让GC线程和用户线程交替进行,减少被GC线程占用的CPU资源,但是回收效果很一般,已被弃用)

B、CMS无法处理浮动垃圾。在CMS并发清理阶段用户现成产生的新垃圾就被称为“浮动垃圾”,CMS无法在当次收集处理它们,只好等待下一次GC时在清理掉。CMS不能等到老年代几乎填满了在进行收集(必须留一部分给并发过程中产生的新对象用)。相应的参数为-XX:CMSInitiatingOccupancyFraction。JDK1.5默认设置为68%,JDK1.6默认设置为92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old来重新进行老年代的垃圾收集。(参数不能设置太高,否则容易导致大量的“Concurrent Mode Failure”,性能反而降低)

C、CMS采用“标记-清除”算法,在垃圾收集结束后会有大量的空间碎片产生。解决方案:打开-XX:UseCMSCompactAtFullCollection参数。当在CMS快要顶不住进行Full GC时开启内存碎片的合并整理过程。——这个过程是无法并发的,空间碎片的问题解决了,但是停顿时间不得不变长。另外一个参数-XX:CMSFullGCsBeforeCompaction。这个参数用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC都要进行碎片压缩)。

7、G1收集器

7.1、设计目标:面向服务端应用的垃圾收集器,未来可以替换掉CMS。

7.2、特点:

A、并发与并行:充分利用多CPU的硬件优势,使用多个CPU来缩短Stop-The-World的时间。部分其他收集器原本需要停顿线程执行的GC动作,G1收集器仍然可以通过并发让它继续执行。

B、分代收集:它能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象以获取更好的收集效果。

C、空间整合:从整体上看,采用了“标记-整理”算法;从局部上看,采用了“复制”算法。不会产生空间碎片

D、可预测的停顿:G1可以建立可预测的停顿时间模型,能让使用者指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。——因为它可以有计划地避免在整个堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的回收时间,优先回收价值大的Region。

7.3、内存布局:G1收集器将整个Java堆分为多个大小相等的独立区域,虽然还保留有新生代和老年代的概念,但它们已经不再是物理隔离的了,都是一部分Region的集合。

7.4、扫描方式:使用Remembered Set来避免进行全堆扫描。每个Region都有一个Remembered Set。一旦有对Reference类型的数据进行写操作,就会检查Reference引用的对象是否位于不同的Region中。如果是,就把引用信息记录到被引用对象所属的
Region的Remembered Set中。在进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可。

7.5、运作步骤

A、初始标记(Initial Marking)

需要Stop-The-World。仅仅只是标记一下GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一个阶段用户程序并发运行时能在正确的Region中创建新对象。

B、并发标记(Concurrent Marking)

可与用户程序并发执行。从GC Roots中开始对堆进行可达性分析,找出存活的对象。

C、最终标记(Final Marking)

修正并发标记期间由于用户程序继续运行而导致标记产生变动的那一部分标记记录。,虚拟机将这段时间对象变化的信息记录在线程Remembered Set中,这阶段需要停顿线程,但是可并行执行(应该只是垃圾收集线程并行执行)。

D、筛选回收(Live Data Counting and Evacuation)

首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到和用户线程一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

博客内容来自《深入理解Java虚拟机》

猜你喜欢

转载自blog.csdn.net/Alexwym/article/details/81980799
今日推荐