《分布式JAVA应用 基础与实践》 第三章 3.2 JVM内存管理(二)
3.2.3? 内存回收(1)
收集器
JVM通过GC来回收堆和方法区中的内存,GC的基本原理首先会找到程序中不再被使用的对象,然后回收这些对象所占用的内存,通常采用收集器的方式实现GC,主要的收集器有引用计数收集器和跟踪收集器。
1. 引用计数收集器
引用计数收集器采用的为分散式的管理方式,通过计数器记录对象是否被引用。当计数器为零时,说明此对象已经不再被使用,于是可进行回收,如图3.9所示。
????????图3.16? CMS GC时浮动垃圾产生的示例Concurrent Marking扫描到a引用了对象b,b引用了c和e。如果在此之后应用将b引用的对象由c改为了d,同时g不再引用d,此时会将b、g对象的状态在card中标识为dirty,但c的状态并不会因此而改变。
3. 重新标记(Final Marking(remark))
该步需要暂停整个应用,在Concurrent Marking时应用可能会修改对象的引用关系或创建新的对象,因此要对这些改变或新创建的对象也进行扫描,包括Mod Union Table及Card Table中dirty的对象,并重新进行着色。
4. 并发收集(Concurrent Sweeping)
在完成了Final Marking后,恢复所有应用的线程,就进入到这步了,这步要负责的是将没有标记的对象进行回收。
由于内存碎片的原因,可能会造成每次回收的内存比之前分配出去的小。为避免这种现象,在进行sweeping的时候,CMS会尽量将相邻的块重新组装为一个块,采用的方法为首先从free list中删除块,组装完毕后再重新放入free list中。
从以上整个步骤来看,CMS中只有Initial Marking和Final Marking需要暂停整个应用,其他动作均与应用并发进行,这也是它能够做到GC过程中影响应用时间很短的原因。但同时由于并发进行,也意味着CMS会和应用线程争抢CPU资源,为降低和应用争抢CPU资源的现象发生,CMS还提供了一种增量的模式,称为i-CMS。在这种模式下,CMS仅启动一个处理器线程来并发扫描标记和清除,并且该线程在执行一小段时间后会先将CPU使用权让出来,分多次多段的方式来完成整个扫描标记和清除的过程,这样降低了对CPU资源的消耗,同时也降低了CMS的性能和回收的效率,因此仅适用于CPU少和内存分配不频繁的应用。
对比并行GC,CMS GC需要执行三次mark,因此其完整的一次GC执行的时间会比并行GC长。对于关注GC总耗时的应用而言,CMS GC并不是合适的选择。
另外,CMS回收内存的方式使得其很容易产生内存碎片,降低了内存空间的利用率。为了减少产生的内存碎片,提高空间的利用率,CMS提供了一个整理碎片的功能,可通过在JVM中指定-XX:+UseCMSCompactAtFullCollection来启动此功能。在启动此功能后默认为每次执行Full GC时都会进行整理,也可通过-XX:CMSFullGCsBeforeCompaction=来指定多少次Full GC后才执行整理。值得注意的是,整理这个步骤是需要暂停整个应用的。
除内存碎片外,CMS在回收时容易产生一些应该回收但要等到下次CMS才能被回收掉的对象,例如图3.16中的c对象,通常把这些对象称为"浮动垃圾",再加上CMS回收过程中大部分时间是和应用并发进行的,因此该过程中应用可能会分配内存,这就要求采用CMS的情况下需要提供更多可用的旧生代空间。
默认情况下CMS GC并不开启,可通过在启动参数上增加-XX:+UseConcMarkSweepGC来启用CMS进行旧生代对象的GC,其默认开启的回收线程数为(并行GC线程数+3)/4,可通过-XX:ParallelCMSThreads=10来强行指定。
CMS GC触发的条件为旧生代已使用的空间达到设定的CMSInitiatingOccupancyFraction百分比,例如默认CMSInitiatingOccupancyFraction为68%,如旧生代空间为1 000MB,那么当旧生代已使用的空间达到680MB时,CMS GC即开始执行;另外一种触发方式是JVM自动触发,JVM基于之前GC的频率及旧生代的增长趋势来评估决定什么时候执行CMS GC,如果不希望JVM自行触发,可设置-XX:UseCMSInitiatingOccupancyOnly=true。
持久代的GC也可采用CMS方式,方式为设置以下参数:-XX:+CMSPermGenSweepingEnabled -XX:+CMSClassUnloadingEnabled。
由于CMS GC在执行时需要分为多个步骤,其输出的GC日志信息也会比较多,下面为一段CMS GC的日志及对其的解释 。
[GC [1 CMS-initial-mark: 13433K(20480K)]? 14465K(29696K), 0.0001830 secs] [Times:? user = 0 .00? sys = 0 .00,? real = 0 .00 secs]?
开始执行CMS GC,进行Initial Marking步骤,旧生代的空间为20 480KB,CMS GC在旧生代被占用了13 433KB后触发。
[CMS-concurrent-mark: 0.004/0.004 secs]? [Times:? user = 0 .01? sys = 0 .00,? real = 0 .01 secs]??
完成Concurrent mark步骤,耗时4ms。
[CMS-concurrent-preclean: 0.000/0.000 secs]? [Times:? user = 0 .00? sys = 0 .00,? real = 0 .00 secs]??
该步用于重新扫描在concurrent mark阶段CMS Heap中被新创建的对象或从新生代晋升到旧生代对象的引用关系,以减少remark所需耗费的时间,这是Sun JDK 1.5后增加的一个优化步骤。
CMS: abort preclean due to time [CMS-concurrent- abortable-preclean: 0.007/5.042 secs] [Times:? user = 0 .00? sys = 0 .00,? real = 5 .04 secs]?
当eden space占用超过2MB时,执行此步,并且将一直并发的执行到eden space的使用率超过50%,之后触发remark动作,这两个值可通过-XX: CMSScheduleRemarkEdenSizeThreshold和-XX: CMSScheduleRemarkEdenPenetration来设置。以上日志信息表示为preclean动作执行了5秒后,eden space使用仍然未超过50%,此时也停止执行preclean,触发remark动作。5秒这个值可通过-XX: CMSMaxAbortablePrecleanTime=5000(单位为毫秒)来进行设置。
[GC[YG occupancy: 3300 K (9216 K)][Rescan (parallel) , 0.0002740 secs][weak refs processing, 0.0000090 secs]? [1 CMS-remark: 13433K(20480K)] 16734K(29696K),? 0.0003710 secs] [Times:? user = 0 .00? sys = 0 .00,? real = 0 .00 secs]??
执行并完成remark动作。
[CMS-concurrent-sweep: 0.000/0.000 secs] [Times:? user = 0 .00? sys = 0 .00,? real = 0 .00 secs]???
执行并完成concurrent sweeping动作。
< P > [CMS-concurrent-reset: 0.000/0.000 secs] [Times:? user = 0 .00? sys = 0 .00,? real = 0 .00 secs] </ P >?
重新初始化CMS的相关数据,为下次CMS GC执行做准备。
除以上GC实现外,在JDK 5以前的版本中还有一个GC实现是增量收集器,增量收集器可通过-Xincgc来启用,但在JDK 5及以上版本中废弃了此增量收集器。当在这些版本中设置-Xincgc时,会自动转为采用并行收集器去进行垃圾回收,原因是其性能低于并行收集器,因此本书中就不介绍此收集器了。
?
?
?
?
?
?