java垃圾回收器:G1和cms

CMS收集算法 参考:图解 CMS 垃圾回收机制原理,-阿里面试题

G1收集算法 参考:G1 垃圾收集器入门

首先要知道 Stop the world的含义(网易面试):不管选择哪种GC算法,stop-the-world都是不可避免的。Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。GC调优通常就是为了改善stop-the-world的时间 

CMS收集器是一种以获取最短回收停顿时间为目标的收集器,CMS收集器是基于“”标记--清除”(Mark-Sweep)算法实现的,整个过程分为四个步骤:  

          1. 初始标记 (Stop the World事件 CPU停顿, 很短) 初始标记仅标记一下GC Roots能直接关联到的对象,速度很快;

           2. 并发标记 (收集垃圾跟用户线程一起执行) 初始标记和重新标记任然需要“stop the world”,并发标记过程就是进行GC Roots Tracing的过程;

           3. 重新标记 (Stop the World事件 CPU停顿,比初始标记稍微长,远比并发标记短)修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记时间短

           4. 并发清理 -清除算法;

  整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。 

初始标记:仅仅是标记一下GC roots 能直接关联的对象,速度很快  (何为GC roots :

在Java语言中,可作为GC Roots的对象包括4种情况:

  a) 虚拟机栈中引用的对象(栈帧中的本地变量表);

  b) 方法区中类静态属性引用的对象;

  c) 方法区中常量引用的对象;

  d) 本地方法栈中JNI(Native方法)引用的对象。

具体参考:JVM的垃圾回收机制 总结(垃圾收集、回收算法、垃圾回收器)) 

CMS是一款优秀的收集器,它的主要优点是:并发收集、低停顿,但他有以下3个明显的缺点:

优点:并发收集,低停顿 

理由: 由于在整个过程和中最耗时的并发标记和 并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,Cms收集器的内存回收过程是与用户线程一起并发执行的

缺点:

   1.CMS收集器对CPU资源非常敏感 

      在并发阶段,虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢,总吞吐量会降低,为了解决这种情况,虚拟机提供了一种“增量式并发收集器” 

的CMS收集器变种, 就是在并发标记和并发清除的时候让GC线程和用户线程交替运行,尽量减少GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐) 

  2. CMS处理器无法处理浮动垃圾 

      CMS在并发清理阶段线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾” , 

 3. CMS是基于“标记--清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。 

    为了解决这个问题,CMS提供了一个开关参数,用于在CMS顶不住要进行full gc的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了  

------------------------------------------------------------------------------------------------------------------  

G1(Garbage First)是一款面向服务端应用的垃圾收集器。G1具备如下特点:

5、G1运作步骤:

1、初始标记(stop the world事件 CPU停顿只处理垃圾);

2、并发标记(与用户线程并发执行);

3、最终标记(stop the world事件 ,CPU停顿处理垃圾);

4、筛选回收(stop the world事件 根据用户期望的GC停顿时间回收)(注意:CMS 在这一步不需要stop the world)(阿里问为何停顿时间可以设置,参考:G1 垃圾收集器架构和如何做到可预测的停顿(阿里)

与其他GC收集器相比,G1具备如下特点:

1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。

3、空间整合:与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的

4、可预测的停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,

 

上面几个步骤的运作过程和CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这一阶段需要停顿线程,但是耗时很短,并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段时耗时较长,但可与用户程序并发执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remenbered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这一阶段需要停顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

什么是CMS

CMS全称 ConcurrentMarkSweep,是一款并发的、使用标记-清除算法的垃圾回收器, 如果老年代使用CMS垃圾回收器,需要添加虚拟机参数-"XX:+UseConcMarkSweepGC"。

使用场景:

GC过程短暂停,适合对时延要求较高的服务,用户线程不允许长时间的停顿。

缺点:

服务长时间运行,造成严重的内存碎片化。 另外,算法实现比较复杂(如果也算缺点的话)

实现机制

根据GC的触发机制分为:周期性Old GC(被动)和主动Old GC,纯属个人理解,实在不知道怎么分才好。

周期性Old GC

周期性Old GC,执行的逻辑也叫 BackgroundCollect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否需要触发。

java垃圾回收器:G1和cms_第1张图片

触发条件

  1. 如果没有设置 UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(线上环境建议带上这个参数,不然会加大问题排查的难度)

  2. 老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%

  3. 永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled

  4. 新生代的晋升担保失败

晋升担保失败

老年代是否有足够的空间来容纳全部的新生代对象或历史平均晋升到老年代的对象,如果不够的话,就提早进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败。

周期性Old GC过程

当条件满足时,采用“标记-清理”算法对老年代进行回收,过程可以说很简单,标记出存活对象,清理掉垃圾对象,但是为了实现整个过程的低延迟,实际算法远远没这么简单,整个过程分为如下几个部分:

java垃圾回收器:G1和cms_第2张图片

对象在标记过程中,根据标记情况,分成三类:

  1. 白色对象,表示自身未被标记;

  2. 灰色对象,表示自身被标记,但内部引用未被处理;

  3. 黑色对象,表示自身被标记,内部引用都被处理;

java垃圾回收器:G1和cms_第3张图片

假设发生Background Collect时,Java堆的对象分布如下:

java垃圾回收器:G1和cms_第4张图片

1、InitialMarking(初始化标记,整个过程STW)

该阶段单线程执行,主要分分为两步:

  1. 标记GC Roots可达的老年代对象;

  2. 遍历新生代对象,标记可达的老年代对象;

该过程结束后,对象分布如下:

java垃圾回收器:G1和cms_第5张图片

2、Marking(并发标记)

该阶段GC线程和应用线程并发执行,遍历InitialMarking阶段标记出来的存活对象,然后继续递归标记这些对象可达的对象。

因为该阶段并发执行的,在运行期间可能发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。

为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代。

java垃圾回收器:G1和cms_第6张图片

3、Precleaning(预清理)

通过参数 CMSPrecleaningEnabled选择关闭该阶段,默认启用,主要做两件事情:

  1. 处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。

  2. 在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(其实这里并非使用CardTable,而是一个类似的数据结构,叫ModUnionTalble),通过扫描这些Table,重新标记那些在并发标记阶段引用被更新的对象(晋升到老年代的对象、原本就在老年代的对象)

4、AbortablePreclean(可中断的预清理)

该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。

为什么需要这个阶段,存在的价值是什么?

因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。

在该阶段,主要循环的做两件事:

  1. 处理 From 和 To 区的对象,标记可达的老年代对象

  2. 和上一个阶段一样,扫描处理Dirty Card中的对象

当然了,这个逻辑不会一直循环下去,打断这个循环的条件有三个:

  1. 可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,表示没有循环次数的限制。

  2. 如果执行这个逻辑的时间达到了阈值 CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。

  3. 如果新生代Eden区的内存使用率达到了阈值 CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。(这个条件能够成立的前提是,在进行Precleaning时,Eden区的使用率小于十分之一)

如果在循环退出之前,发生了一次YGC,对于后面的Remark阶段来说,大大减轻了扫描年轻代的负担,但是发生YGC并非人为控制,所以只能祈祷这5s内可以来一次YGC。

  1. ...

  2. 1678.150:[CMS-concurrent-preclean-start]

  3. 1678.186:[CMS-concurrent-preclean:0.044/0.055secs]

  4. 1678.186:[CMS-concurrent-abortable-preclean-start]

  5. 1678.365:[GC 1678.465:[ParNew:2080530K->1464K(2044544K),0.0127340secs]

  6. 1389293K->306572K(2093120K),

  7. 0.0167509secs]

  8. 1680.093:[CMS-concurrent-abortable-preclean:1.052/1.907secs]

  9. ....

在上面GC日志中,1678.186启动了AbortablePreclean阶段,在随后不到2s就发生了一次YGC。

5、FinalMarking(并发重新标记,STW过程)

该阶段并发执行,在之前的并行阶段(GC线程和应用线程同时执行,好比你妈在打扫房间,你还在扔纸屑),可能产生新的引用关系如下:

  1. 老年代的新对象被GC Roots引用

  2. 老年代的未标记对象被新生代对象引用

  3. 老年代已标记的对象增加新引用指向老年代其它对象

  4. 新生代对象指向老年代引用被删除

  5. 也许还有其它情况..

上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以还有进行如下的处理:

  1. 遍历新生代对象,重新标记

  2. 根据GC Roots,重新标记

  3. 遍历老年代的Dirty Card,重新标记,这里的Dirty Card大部分已经在clean阶段处理过

在第一步骤中,需要遍历新生代的全部对象,如果新生代的使用率很高,需要遍历处理的对象也很多,这对于这个阶段的总耗时来说,是个灾难(因为可能大量的对象是暂时存活的,而且这些对象也可能引用大量的老年代对象,造成很多应该回收的老年代对象而没有被回收,遍历递归的次数也增加不少),如果在AbortablePreclean阶段中能够恰好的发生一次YGC,这样就可以避免扫描无效的对象。

如果在AbortablePreclean阶段没来得及执行一次YGC,怎么办?

CMS算法中提供了一个参数: CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。

不过,这种参数有利有弊,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,然后在该阶段又傻傻的触发一次。

所以利弊需要把握。

主动Old GC

这个主动Old GC的过程,触发条件比较苛刻:

  1. YGC过程发生Promotion Failed,进而对老年代进行回收

  2. System.gc(),前提是添加了-XX:+ExplicitGCInvokesConcurrent参数

如果触发了主动Old GC,这时周期性Old GC正在执行,那么会夺过周期性Old GC的执行权(同一个时刻只能有一种在Old GC在运行),并记录 concurrent mode failure 或者 concurrent mode interrupted。

主动GC开始时,需要判断本次GC是否要对老年代的空间进行Compact(因为长时间的周期性GC会造成大量的碎片空间),判断逻辑实现如下:

  1. *should_compact =

  2. UseCMSCompactAtFullCollection&&

  3. ((_full_gcs_since_conc_gc >=CMSFullGCsBeforeCompaction)||

  4. GCCause::is_user_requested_gc(gch->gc_cause())||

  5. gch->incremental_collection_will_fail(true/* consult_young */));

在三种情况下会进行压缩:

  1. 其中参数 UseCMSCompactAtFullCollection(默认true)和CMSFullGCsBeforeCompaction(默认0),所以默认每次的主动GC都会对老年代的内存空间进行压缩,就是把对象移动到内存的最左边。

  2. 当然了,比如执行了 System.gc(),也会进行压缩。

  3. 如果新生代的晋升担保会失败。

带压缩动作的算法,称为MSC,标记-清理-压缩,采用单线程,全暂停的方式进行垃圾收集,暂停时间很长很长...

那不带压缩动作的算法是什么样的呢?

不带压缩动作的执行逻辑叫 ForegroundCollect,整个过程相对周期性Old GC来说,少了Precleaning和AbortablePreclean两个阶段,其它过程都差不多。

 

G1垃圾回收器其实是JDK7的特性,在目前JDK10都已经发布的情况下,已经不是什么新特性了,而我到它现在才关注它,可见我是有多么的懒;而我终于关注它了,可见我的懒还算是有救的 :)

G1其实是Garbage First的意思,垃圾优先? 不是,是优先处理那些垃圾多的内存块的意思。在大的理念上,它还是遵循JVM的内存分代假设(其实叫假设不准确,这是从实际Java应用的内存使用观察得到的结论):

90%的对象熬不过第一次垃圾回收,而老的对象(经历了好几次垃圾回收的对象)则有98%的概率会一直活下来。

基于这个分代假设,一般的垃圾回收器把内存分成三类: Eden(E), Suvivor(S)和Old(O), 其中Eden和Survivor都属于年轻代,Old属于老年代,新对象始终分配在Eden里面,熬过一次垃圾回收的对象就被移动到Survisor区了,经过数次垃圾回收之后还活着的对象会被移到Old区。

一般GC的内存分布

这样分代的好处是,把一个复杂的大问题,分成两类不同的小问题,针对不同的小问题,采用更有针对性的措施(分而治之):

  • 对于年轻代的对象,由于对象来的快去得快,垃圾收集会比较频繁,因此执行时间一定要短,效率要高,因此要采用执行时间短,执行时间的长短只取决于对象个数的垃圾回收算法。但是这类回收器往往会比较浪费内存,比如Copying GC,会浪费一半的内存,以空间换取了时间。
  • 对于老年代的对象,由于本身对象的个数不多,垃圾收集的次数不多,因此可以采用对内存使用比较高效的算法。

跟其它垃圾回收器不一样的是:G1虽然也把内存分成了这三大类,但是在G1里面这三大类不是泾渭分明的三大块内存,G1把内存划分成很多小块, 每个小块会被标记为E/S/O中的一个,可以前面一个是Eden后面一个就变成Survivor了。

 

G1的内存分布

这么做给G1带来了很大的好处,由于把三块内存变成了几百块内存,内存块的粒度变小了,从而可以垃圾回收工作更彻底的并行化。

G1的并行收集做得特别好,我们第一次听到并行收集应该是CMS(Concurrent Mark & Sweep)垃圾回收算法, 但是CMS的并行收集也只是在收集老年代能够起效,而在回收年轻代的时候CMS是要暂停整个应用的(Stop-the-world)。而G1整个收集全程几乎都是并行的,它回收的大致过程是这样的:

  • 在垃圾回收的最开始有一个短暂的时间段(Inital Mark)会停止应用(stop-the-world)
  • 然后应用继续运行,同时G1开始Concurrent Mark
  • 再次停止应用,来一个Final Mark (stop-the-world)
  • 最后根据Garbage First的原则,选择一些内存块进行回收。(stop-the-world)

由于它高度的并行化,因此它在应用停止时间(Stop-the-world)这个指标上比其它的GC算法都要好。

G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。 (阿里面试)

由于内存被分成了很多小块,又带来了另外好处,由于内存块比较小,进行内存压缩整理的代价都比较小,相比其它GC算法,可以有效的规避内存碎片的问题。

说了G1的这么多好处,也该说说G1的坏处了,如果应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个Heap的回收,那么G1要做的工作量就一点也不会比其它垃圾回收器少,而且因为本身算法复杂了一点,可能比其它回收器还要差。因此G1比较适合内存稍大一点的应用(一般来说至少4G以上),小内存的应用还是用传统的垃圾回收器比如CMS比较合适。

总结

G1通过在垃圾回收领域应用并行化的策略,把几块大内存块的回收问题,变成了几百块小内存的回收问题,使得回收算法可以高度并行化,同时也因为分成很多小块,使得垃圾回收的单位变成了小块内存,而不是整代内存,使得用户可能对回收时间进行配置,垃圾回收变得可以预期了。

分而治之、化整为零这些朴素的架构思想往往是很多牛叉技术产品背后的思想根源啊。

 

参考:G1垃圾回收器

 

总结这篇文章和其他的资料,G1可以基本稳定在0.5s到1s左右的延迟,但是并不能保证更低的比如毫秒级(金融场景,所以说涉及到钱的,对技术要求真高),号称zing可以(但是一般做到低延时,在其他方面肯定有所损耗,比如吞吐),但是没有实际去研究过这种。另外,G1也可能和CMS一样出现Full GC,如果区域不够提升的话,所以它一般用于需求更大的堆中。但G1最显著于CMS的,在于它对空间做了整理,这样减少了空间的碎片化。CMS的空间碎片话相较于G1要严重很多,试想下它的Mark-Sweep之后的空间,有很多小碎片,但是都比要分配的小,然后触发一次Full GC,简直了。再说点杂的,G1的思想,感觉有点像Java CocurrentHashMap,也是将一个大的分成若干个Region,然后再处理

G1 垃圾收集器入门

概览


目的

这个教程覆盖了如何使用G1垃圾收集器和它是怎样被Hotspot JVM使用的,你会学到G1收集器内部是如何工作的,使用G1时的一些关键命令行开关和记录它的操作的一些选项。

完成耗时

大约1小时

介绍

这个OBE(Oracle By Example)覆盖了Java里的Java虚拟机G1垃圾回收的基本概念,在OBE的第一部分, 
在介绍垃圾收集器和性能时会附带提供JVM的概览。下一部分回顾一下Hotspot JVMCMS收集器如何工作。然后,一步一步来指导使用Hotspot JVMG1垃圾回收,跟着,会用一段来讲G1垃圾回收器的可用的命令行选项。最后,你会学到G1回收器的日志选项。

硬件、软件需求

下面是一个硬件软件需求列表:

  • 一台运行Windows XP或者更高版本的PC,Mac OS X或者Linux。注意已在Windows 7系统亲测,还没有在所有平台测试。然而,应该会在OS X或Linux上正常运行。多处理器核心的机器更好。
  • Java 7 Update 9或更高版本
  • 最新Java 7 演示和示例压缩文件

先决条件

开启教程之前,你应该:

  • 如果你没有这样做,下载安装最新版本的JDK,Java 7 JDK Downloads。
  • 从同样的地址下载安装演示示例压缩文件,解压文件到一个目录中,比如:C:/javademos

Java技术和虚拟机


Java概览

Java是Sun微系统公司在1995年首次发布的一个编程语言和计算平台。它是支撑Java程序包括工具、游戏、商业应用的底层技术。在全球Java运行在超过8亿5千万台个人电脑上,数以十亿计的设备上,包括移动、电视设备。Java由一些关键组件组成,作为一个整体,创建Java平台。

Java运行时

当你下载Java,你获得Java运行环境(JRE,Java Runtime Environment)。 JRE由Java虚拟机(JVM)、Java平台核心类和支撑Java平台的库组成。要在你电脑上运行Java应用,所有的这些都是必须的。有了Java 7, Java应用从操作系统方面来说是作为桌面应用运行的,作为一个桌面应用但是从网络上安装需要使用Java Web Start,或者在浏览器里作为一个Web嵌入式的应用(使用JavaFX

Java编程语言

Java是一个面向对象的编程语言,包含以下特性:

  • 平台独立 - Java应用被编译成存储在类文件中的字节码,在JVM里被加载。一旦应用在JVM里运行,它们可以运行在许多不同的操作系统和设备上。
  • 面向对象 - Java是一个面向对象的语言,借鉴了C和C++的诸多特性,在它们之上改进。
  • 自动垃圾收集 - Java自动分配和释放内存,所以程序不必背负这个任务。
  • 丰富的标准库 - Java包括大量预先做好的对象,它用被用在执行比如输入/输出、网络、数据操作这样的任务上。

Java开发工具箱

Java开发工具箱(JDK,Java Development Kit)是一个开发Java应用的工具集。有了JDK,你可以编译用Java语言编写的应用程序,在JVM里面运行它们。另外,JDK提供工具打包和发布你的应用。

JDK和JRE分享同样的Java应用编程接口(Java API,Java Application Programming Interfaces)。Java API是已经打包的库的集合,开发者用来创建Java应用。Java API通过提供工具完成很多通常的编程任务包括字符串操作、时间日期处理、网络和实现数据结构(比如:列表、映射、栈和队列)使开发更加容易。

Java虚拟机

Java虚拟机(JVM,Java Virtual Machine)是一个抽象的计算机器,Java虚拟机是一个程序,对在它里面运行的编写的程序来说, 看起来像一个机器。这样,Java程序就会用相同的接口和库来编写。每一个针对特定操作系统的JVM实现,把Java程序指令翻译成运行在本地操作系统的指令和命令。这样,Java程序实同了平台独立。

Sun微系统公司完成了第一个Java虚拟机的原型实现,仿真Java虚拟机指令集设置进一个被类似当时的个人数码助手(PDA,Personal Digital Assistant)手持设备的托管的软件里。Oracle现在实现仿真Java虚拟机在移动、桌面和服务器设备上,但是Java虚拟机没有承担任何具体的技术实现,管理硬件或者管理操作系统。It is not inherently interpreted, but can just as well be implemented by compiling its instruction set to that of a silicon CPU. It may also be implemented in microcode or directly in silicon斜体这里翻译不清楚,请大神指点。好像涉及解释执行、指令集、微码等

Java虚拟机对Java编程语言一概不知,只知道一个特定的二进制格式,就是类文件格式,一个类文件包含Java虚拟机指令(或者叫字节码)和一个符号表,和一些辅助信息。

为了达到安全的目的,Java虚拟机在类文件代码上利用强大的语法和结构化的约束条件。然而任何函数性语言可以依照一个可用的被Java虚拟机托管类文件来表达。被通用的、机器平台独立性吸引,其它语言的实现者可以把Java虚拟机视为其它语言的递送载体,(1)The Java Virtual Machine

探索Java虚拟机架构

Hotspot架构

Hotspot虚拟机拥有一个架构,它支持强大特性和能力的基础平台,支持实现高性能和强大的可伸缩性的能力。举个例子,Hotspot虚拟机JIT编译器生成动态的优化,换句话说,它们在Java应用执行期做出优化,为底层系统架构生成高性能的本地机器指令。另外,经过它的运行时环境和多线程垃圾回收成熟的进化和连续的设计, Hotspot虚拟机在高可用计算系统上产出了高伸缩性。 
hotspot jvm architecture
Java虚拟机的主要组件,包括类加载器、运行时数据区和执行引擎

Hotspot关键组件

Java虚拟机有关性能的关键组件已经在下面的图片上高亮显示了。 
key hotspot jvm components
Java虚拟机有三个组件关注着什么时候进行性能优化,堆空间是你的对象所存储的地方,这个区域 
被启动时选择的垃圾回收器管理,大部分调优选项与调整堆大小和根据你的情况选择最适当的垃圾收集器相关。即时编译器对性能也有很大的影响,但是使用新版本的Java虚拟机时很少需要调整。

性能基础

典型的,当调优一个Java应用时,把焦点放在两个主要的目标上:响应能力或者吞吐量。随着教程的进行我们会再重新提及这些概念。

响应能力

响应能力指的是一个应用回应一个请求数据的速度有多快。示例包括:

  • 桌面UI响应事件的速度
  • 网站返回网页的速度
  • 数据查询返回的速度 
    对关注响应能力的应用来说,长暂停时间是不可接受的,重点是在短的时间周期内能做出响应。

吞吐量

吞吐量关注在特定的时间周期内一个应用的工作量的最大值。举例如何衡量吞吐量,包括:

  • 给定时间内完成事务的数量
  • 一小时内批处理程序完成的工作数量
  • 一小时内数据查询完成的数量 
    对关注吞吐量的应用来说长暂停时间是可以接受的。由于高吞吐量的应用关注的基准在更长周期时间上,所以快速响应时间不在考虑之内。

G1垃圾回收器


G1垃圾回收器

Garbage-First(G1,垃圾优先)收集器是服务类型的收集器,目标是多处理器机器、大内存机器。它高度符合垃圾收集暂停时间的目标,同时实现高吞吐量。Oracle JDK 7 update 4 以及更新发布版完全支持G1垃圾收集器。G1垃圾回集器为以下应用设计:

  • 类似CMS收集器,可以和应用线程同时并发的执行
  • 压缩空闲空间时没有GC引起的暂停时间
  • 需要更可预言的GC暂停时间
  • 不想牺牲大量的吞吐量性能
  • 不需要特别大的Java堆

G1垃圾收集器计划长期替换并发标记清除收集器(CMS,Concurrent Mark-Sweep Collector)。G1和CMS比较,有一些不同点让G1成为一个更好的解决方案。一个不同点是G1是一个压缩收集器。G1收集器充分地压缩空间以完全避免为分配空间使用细粒度的空闲列表,而不是依赖于区块。这相当简化了收集器的部件,和尽量消除可能的碎片问题。同时,G1收集器相比CMS收集器而方言,提供更可预言的垃圾收集暂停时间,允许用户指定想要暂停时间指标。

G1收集器操作概览

旧的垃圾收集器(串行的:serial,并行的:parallel,并发标记清除:CMS)都把堆结构化为三个部分:年轻代、年老代和固定大小的永久代。 
Hotspot Heap Structure
所以内存对象最终都在这三个区域里。 
G1收集器应用了一个不同的方法。 
G1 Heap Allocation
堆空间被分割成一些相同大小的堆区域,每一个都是连续范围的虚拟内存。特定的区域集合像旧的收集器一样被指派为相同的角色(伊甸:eden、幸存:survivor、年老:old),但是它们没有一个固定大小。这在内存使用上提供了更强大的灵活性。

当执行垃圾收集时,G1收集器以与CMS收集器类似的方式操作。G1收集器执行一个全局的并发标记阶段来决定堆中的对象的活跃度。之后标记阶段就完成了。G1收集器知道哪个区域基本上是空的。它首先会收集那些产出大量空闲空间的区域。这就是为什么这个垃圾收集的方法叫做垃圾优先的原因。就像名称显示的那样,G1收集器集中它的收集和压缩活动在堆里的那些可完全被回收的区域,那就是垃圾。G1收集器使用一个暂停预言的模式去达到一个用户定义的暂停时间指标,基于用户指定的暂停时间指标去选择收集区域的数量。

被G1收集器鉴定为可以回收的区域就是垃圾,使用抽空的方式收集。G1收集器从堆空间的一个或多个区域里复制对象到堆空间的一个单独的区域内,这个过程中同时压缩和释放内存。这个抽空过程在多处理上以并行的方式运行,以减小暂停时间和增加吞吐量。因此,每一次垃圾收集G1收集器连续不断地去减少碎片,在用户指定的暂停时间内工作。这超越了以往方法的能力。并发标记-清除(CMS,Concurrent Mark Sweep)垃圾收集器不做压缩操作。并行年老代(ParallelOld)垃圾收集只进行整个堆的压缩,会导致相当大的暂停时间。

注意: G1收集器不是实时的收集器非常重要。它在很大程度上符合用户设定的暂停时间指标但是并不绝对符合。基于前面垃圾收集的数据来看,G1收集器会估算在用户指定的时间指标能收集多少区域。因此,收集器有一个合理的精确的收集这些区域的代价模型,它使用这个模型决定在用户指定的暂停时间内收集哪些、多少个区域。

注意: G1收集器同时有并发(和应用线程一起运行,比如,提炼、标记、清理)和并行(多线程,比如,stop the world)两个阶段。全量垃圾回收仍然是单线程的,但是如果调优的适当你的应用应该会避免全量垃圾回收。

G1回收器足迹

如果你从ParallelOldGc或者CMS收集器迁移到G1收集器,你很有可能会看到一个大的Java虚拟机进程大小,这和审计”数据结构比如已记忆集合(Remembered Sets)和收集集合(Collection Sets)有很大关系“。

Remembered Sets或者RSets把对象引用推进一个给定的区域。在堆空间中每一个区有一个RSet。RSet允许一个区域并行的、独立的收集。RSet总体的足迹影响小于5%。

Collection Sets或者CSets,是在垃圾回收过程中会被回收的区域集合。在RSet中的所有活跃对象在垃圾回收过程中会被抽空(复制/移动)。集合包含的区域可以是eden、survivor或者年老代。CSets在Java虚拟机大小的影响小于1%。

建议使用G1收集器的场景

G1收集器首要关注的是为用户运行着需要大堆空间、限制的垃圾回收延迟的应用提供一个解决方案。这意味着堆大小为6GB左右或者更大,稳定的、可预言的暂停时间小于0.5秒。

如果应用有以下一个或多个特点,当下运行着CMS或ParallelOldGC垃圾收集器的应用把收集器切换到G1收集器的话,会从中受益的:

  • Full GC持续时间太长或者太频繁
  • 对象分配比率或者提升有显著的变化
  • 不期望的长时间垃圾收集或者压缩暂停(大于0.5到1秒)

注意:如果你在使用CMS或者ParallenOldGC收集器,你的应用不曾经历过长时间的垃圾收集暂停,保持使用你当前的收集器比较好。在使用最新的JDK的情况下,改变到G1收集器不是一个必要的事情。

回顾CMS垃圾回收


回顾分代垃圾回收和CMS

并发标记清除(CMS)收集器(也叫并发低延迟收集器)回收年老代垃圾。它通过和应用线程并发的执行大部分垃圾收集工作的方式来尝试最小化垃圾回收引起的暂停。正常情况下并发低延迟收集器不会复制或者压缩活跃对象。一次垃圾收集的完成不必移动活跃对象。如果内存碎片成为一个问题,分配更大的堆空间。

注意: CMS收集器在年轻代上使用和并行收集器相同的算法。

CMS收集阶段

CMS收集器在堆的年老代空间上执行以下阶段:

阶段 描述
(1)初始标记(Stop the World事件) 年老代里的对象被标记为可达的包括那些可能从年轻代可达的对象。此期间暂停时间相对minor gc的暂停时间是比较 短的
(2)并发标记 当Java应用线程运行时,并发的遍历年老代对象图可达的对象。从标记的对象和根上可达到标记对象开始扫描。设值方法在并发的2、3、5阶段期间执行,在这些阶段(包括晋升的对象)被分配进CMS代所有对象都会立刻被标记为活跃对象。
(3)重新标记(Stop the World事件) 寻找那些在并发标记阶段丢失的,在并发收集器完成之后跟踪对象之后由Java应用线程的更新的对象。
(4)(并发清除) 收集在标记阶段被鉴定为不可达的对象。收集死亡对象会增加空闲列表的空间,方便之后的对象分配。聚合死亡对象可以会在此点发生。注意活跃对象是不会被移动。
(5)(重新设置) 清理数据结构为下一次并发收集做准备

回顾垃圾收集步骤

接下来,让我们一步一步的回顾下CMS收集器的操作步骤

1. CMS收集器堆结构

堆空间被分割为三块空间。 
CMS Heap Structure
年轻代分割成一个Eden区和两个Survivor区。年老代一个连续的空间。就地完成对象收集。除非有FullGC否则不会压缩。

2.CMS年轻代垃圾收集如何工作

年轻代被标为浅绿色,年老代被标记为蓝色。如果你的应用已经运行了一段时间,CMS的堆看起来应该是这个样子。对象分散在年老代区域里。 
How Young GC Works
使用CMS,年老代对象就地释放。它们不会被来回移动。这个空间不会被压缩除非发生FullGC。

3.年轻代收集

从Eden和Survivor区复制活跃对象到另一个Survivor区。所有达到他们的年龄阈值的对象会晋升到年老代。 
Young Generation Collection

4.年轻代回收之后

一次年轻代垃圾收集之后,Eden区和其中一个Survivor区被清空。 
After Young GC
最近晋升的对象以深蓝色显示在上图中,绿色的对象是年轻代幸免的还没有晋升到老年代对象。

5.CMS的年老代收集

发生两次stop the world事件:初始标记和重新标记。当年老代达到特定的占用比例时,CMS开始执行。 
Old gen collection in CMS
(1)初始标记是一个短暂暂停的、可达对象被标记的阶段。(2)并发标记寻找活跃对象在应用连续执行时。最后,在(3)重新标记阶段,寻找在之前并发标记阶段中丢失的对象。

6.年老代收集-并发清除

在之前阶段没有被标记的对象会被就地释放。不进行压缩操作。 
Old Gen Collection - Concurrent Sweep
注意:未被标记的对象等于死亡对象

7.年老代收集-清除之后

(4)清除阶段之后,你可以看到大量内存被释放。你还可以注意到没有进行压缩操作。 
Olg Gen Collection - After Sweeping
最后,CMS收集器会走过(5)重新设置阶段,等待下一次垃圾收集时机的到来。

循序渐进G1垃圾收集器


循序渐进G1垃圾收集器

G1收集器在分配堆空间的方法上有些不同。下面的图片一步一步系统的回顾G1收集器。

1.G1堆结构

堆空间是一个被分成许多固定大小区域的内存块。 
G1 Heap Structure
Java虚拟机启动时选定区域大小。Java虚拟机通常会指定2000个左右的大小相等、每个大小范围在1到32M的区域。

2.G1堆空间分配

实际上,这些区域被映射成Eden、Survivor、年老代空间的逻辑表述形式。 
G1 Heap Allocation
图片中的颜色表明了哪个区域被关联上什么角色。活跃对象从一个区域疏散(复制、移动)到另一个区域。区域被设计为并行的方式收集,可以暂停或者不暂停所有的其它用户线程。

明显的区域可以被分配成Eden、Survivor、Old区域。另外,有第四种类型的区域叫做极大区域(Humongous regions)。这些区域被设计成保持标准区域大小的50%或者更大的对象。它们被保存在一个连续的区域集合里。最后,最后一个类型的区域就是堆空间里没有使用的区域。

注意:写作此文章时,收集极大对象时还没有被优化。因此,你应该避免创建这个大小的对象。

3.G1的年轻代

堆空间被分割成大约2000个区域。最小1M,最大32M,蓝色区域保持年老代对象,绿色区域保持年轻代对象。 
Young Generation in G1
注意:区域没有必要像旧的收集器一样是保持连续的。

4.G1的年轻代收集

活跃对象会被疏散(复制、移动)到一个或多个survivor区域。如果达到晋升总阈值,对象会晋升到年老代区域。 
A Young GC in G1
这是一个stop the world暂停。为下一次年轻代垃圾回收计算Eden和Survivor的大小。保留审计信息有助于计算大小。类似目标暂停时间的事情会被考虑在内。

这个方法使重调区域大小变得很容易,按需把它们调大或调小。

5.G1年轻代回收的尾声

活跃对象被疏散到Survivor或者年老代区域。 
End of Young GC with G1
最近晋升的对象显示为深蓝色。Survivor区域显示为绿色。

关于G1的年轻代回收做以下总结:

  • 堆空间是一块单独的内存空间被分割成多个区域。
  • 年轻代内存是由一组非连续的区域组成。这使得需要重调大小变得容易。
  • 年轻代垃圾回收是stop the world事件,所有应用线程都会因此操作暂停。
  • 年轻代垃圾收集使用多线程并行回收。
  • 活跃对象被复制到新的Survivor区或者年老代区域。

G1年老代垃圾回收

类似CMS收集器,G1收集器为年老代对象被设计成一个低暂停收集器。下面的表描述了在年老代上的G1收集阶段。 
G1垃圾收集器在堆上的年老代执行以下阶段。注意一些阶段是年轻代回收的一部分。

阶段 描述
(1)初始标记(stop the world事件) 这是一个stop the world事件,使用G1回收器,背负着一个常规的年轻代收集。标记那些有引用到年老代的对象的survivor区(根区)
(2)根区扫描 为到年老代的引用扫描survivor区,这个发生在应用继续运行时。这个阶段在年轻代收集前必须完成
(3)并发标记 遍历整个堆寻找活跃对象,这个发生在应用运行时,这个阶段可以被年轻代垃圾回收打断。
(4)重新标记(stop the world事件) 完全标记堆中的活跃对象,使用一个叫作snapshot-at-the-beginning(SATB)的比CMS收集器的更快的算法
(5)清理(stop the world事件和并发) 在活跃对象上执行审计操作和释放区域空间(stop the world);净化已记忆集合(stop the world);重置空间区域和返回它们到空闲列表(并发)
(*)复制(stop the world事件) 这些是stop the world暂停为了疏散或者复制活跃对象到新的未使用的区域。这个可以由被记录为[GC Pause (young)]的年轻代区域或者被记录为[GC Pause (mixed)]年轻代和年老代区域完成

循序渐进G1年老代垃圾回收

记住已被定义的阶段,让我们来看一下G1收集器是如何作用于年老代的。

6.初始标记阶段

年轻代垃圾收集肩负着活跃对象初始标记的任务。在日志文件中被标为GC pause (young)(inital-mark) 
Initial Marking Phase

7.并发标记阶段

如果发现空区域(“X”标示的),在重新标记阶段它们会被马上清除掉。当然,决定活性的审计信息也在此时被计算。 
Concurrent Marking Phase

8.重新标记阶段

空的区域被清除和回收掉。所有区域的活性在此时计算。 
Remark Phase

9.复制/清理阶段

G1选择活性最低的区域,这些区域能够以最快的速度回收。然后这些区域会在年轻代垃圾回收过程中被回收。在日志中被指示为[GC pause (mixed)]。所以年轻代和年老代在同一时间被回收。 
Coping/Cleanup Phase

10.复制/清理阶段之后

被选择的区域已经被回收和压缩到图中显示的深蓝色区和深绿色区中。 
After Coping/Cleanup Phase

年老代垃圾回收总结

总结下,我们可以列出一些关于G1收集器在年老代的上关键点。 
并发标记阶段

  • 当应用运行时,并发的计算活性信
  • 在疏散暂停期间,活性信息鉴定哪些区被最好的回收
  • 没有像CMS一样的清除操作

重新标记阶段

  • 使用比在CMS中使用的算法更快的Snapshot-at-the-Beginning(SATB)算法
  • 完全空的区域会被回收掉

复制/清理阶段

  • 年轻代和年老代被同时回收
  • 年老代区域基于它们的活性被选择

命令行选项最佳实践


命令行选项最佳实践

在这部分我们看一下G1收集器的多样的命令行选项。

基本命令行

为了启用G1收集器,使用:-XX:+UseG1GC 
这个是启动在已下载的JDK演示和示例里的Java2Demo程序的示例命令行: 
java -Xmx50m -Xms50m -XX:UserG1GC -XX:MaxGCPauseMillis=200 -jar c:\javademos\demo\jfc\Java2D\Java2demo.jar

关键命令行开关

-XX:+UseG1GC - 告诉Java虚拟机使用G1垃圾收集器 
-XX:MaxGCPauseMillis=200 - 为最大GC暂停时间设置一个指标。这是一个软目标,Java虚拟机将尽最大努力实现它。因此,暂停时间目标有时候可能不会达到。默认值是200毫秒。 
-XX:InitiatingHeapOccupancyPercent=45 - 触发并发垃圾收集周期的整个堆的百分比时机。

最佳实践

使用G1收集器时你应该遵守的一些最佳实践 
不要设置年轻代大小 
通过-Xmn明确地设置年轻代大小来插手G1收集器的默认行为。

  • 收集时G1收集器将不再遵照暂停时间指标。所以本质上,设置年轻代大小将不会启用暂停时间目标。
  • G1收集器将不能按需扩张、收缩年轻代空间。自从大小被固定之后,大小将不再会被改变。

响应时间指标

代替使用平均响应时间(ART)做为指标,来设置XX:MaxGCPauseMillis=,考虑设置值将会符合这个时间的90%或者更高比例。这意味着90%的用户发出一个请求将不会经历高于这个目标的时间。记住,暂停时间只是一个目标,不保证总是能够达到。

什么是疏散失败?

当Java虚拟机在Survivor和晋升的对象垃圾回收期间,堆空间用光了就会发生晋升失败。堆空间不能再扩展了因为已经在最大值了,使用-XX:+PrintGCDetails参数时,这种情况会在GC日志中通过to-space-overflow指示出来。这个代价非常大。

  • 垃圾收集仍然会继续运行,空间必须被释放。
  • 没有成功复制的对象必须就地被提升。
  • 在CSet里的任何到区域的RSets的更新都会重新生成
  • 所有这些步骤代价都非常大

如何避免疏散失败

为了避免疏散失败,考虑以下选项。 
增大堆大小

  • 增大-XX:G1ReservePercent=n参数值,默认是10
  • G1收集器创建一个假的上限通过尝试保留储备内存的自由假如’to-space’被渴望得到。 
    提前启动标记周期 
    使用-XX:ConcGCThreads=n选项增大标记线程的数量

G1垃圾收集器开关完整列表

这是一个G1垃圾收集器开关的完整列表,记着去使用上述的最佳实践。

选项和默认值 描述
-XX:+UseG1GC 使用垃圾优先(G1,Garbage First)收集器
-XX:MaxGCPauseMillis=n 设置垃圾收集暂停时间最大值指标。这是一个软目标,Java虚拟机将尽最大努力实现它
-XX:InitiatingHeapOccupancyPercent=n 触发并发垃圾收集周期的整个堆空间的占用比例。它被垃圾收集使用,用来触发并发垃圾收集周期,基于整个堆的占用情况,不只是一个代上(比如:G1)。0值 表示’do constant GC cycles’。默认是45
-XX:NewRatio=n 年轻代与年老代的大小比例,默认值是2
-XX:SurvivorRatio=n eden与survivor空间的大小比例,默认值8
-XX:MaxTenuringThreshold=n 最大晋升阈值,默认值15
-XX:ParallerGCThreads=n 设置垃圾收集器并行阶段的线程数量。默认值根据Java虚拟机运行的平台有所变化
-XX:ConcGCThreads=n 并发垃圾收集器使用的线程数量,默认值根据Java虚拟机运行的平台有所变化
-XX:G1ReservePercent=n 为了降低晋升失败机率设置一个假的堆的储备空间的上限大小,默认值是10
-XX:G1HeapRegionSize=n 使用G1收集器,Java堆被细分成一致大小的区域。这设置个体的细分的大小。这个参数的默认值由工学意义上的基于堆的大小决定

G1收集器的垃圾收集日志


G1收集器的垃圾收集日志

我们需要涵盖的最后的主题是使用G1垃圾回收器的日志记录信息来分析性能。这部分提供你可以用来收集打印在日志里的数据和信息的开关的快速的概览。

设置日志详情

你可以设置三种不同级别的详情。 
(1)-verbosegc (和-XX:+PrintGC等效)参数设置fine日志详情级别 
sample Output 
[GC pause (G1 Humongous Allocation) (young) (initial-mark) 24M- >21M(64M), 0.2349730 secs] 
[GC pause (G1 Evacuation Pause) (mixed) 66M->21M(236M), 0.1625268 secs]

(2)-XX:PrintGCDetails设置finer详情级别。使用这个选项会显示以下信息: 
* 显示每个阶段的平均、最小、最大时间 
* 根扫描、RSet更新(附带处理的缓冲信息)、RSet扫描、对象复制、终止(附带尝试次数)。 
* 也显示’other’时间,比如花费在选择CSet上的时间、引用处理、引用排队和释放CSet。 
* 显示Eden、Survivor和总堆空间占用。 
Sample Output 
[Ext Root Scanning (ms): Avg: 1.7 Min: 0.0 Max: 3.7 Diff: 3.7] 
[Eden: 818M(818M)->0B(714M) Survivors: 0B->104M Heap: 836M(4096M)->409M(4096M)]

(3)-XX:+UnlockExperimentalVMOptions -XX:G1LogLevel=finest设置finest详情级别。类似finer但是包括每个工作者线程的信息。 
Sample Output 
[Ext Root Scanning (ms): 2.1 2.4 2.0 0.0 
Avg: 1.6 Min: 0.0 Max: 2.4 Diff: 2.3] 
[Update RS (ms): 0.4 0.2 0.4 0.0 
Avg: 0.2 Min: 0.0 Max: 0.4 Diff: 0.4] 
[Processed Buffers : 5 1 10 0 
Sum: 16, Avg: 4, Min: 0, Max: 10, Diff: 10]

决定时间显示格式

有两个开关可以决写在垃圾收集日志中如何显示时间。 
(1)-XX:+PrintGCTimeStamps - 显示自从Java虚拟机启动之后流逝的时间。 
Sample Output 
1.729: [GC pause (young) 46M->35M(1332M), 0.0310029 secs]

(2)-XX:+PrintGCDateStamps - 为每一项添加日期时间的前缀。 
Sample Output 
2012-05-02T11:16:32.057+0200: [GC pause (young) 46M->35M(1332M), 0.0317225 secs]

理解G1日志

为了理解这个日志,这部分使用实际的垃圾收集输出日志来明确一些术语。下面的示例列出了输出日志中的术语和值,你会在日志中找到它们。 

你可能感兴趣的:(java)