JVM 垃圾收集器

JVM 垃圾收集器

基本概念

  • Young Generation:新生代,hotspot vm中又细分为Eden和两个Survivor(from survivor和to survivor)
  • Tenured Generation:老年代
  • Minor GC:只GCyoung generation。
  • Full GC、Major GC:一个意思,指GC整个heap,包括young generation和tenured generation。

先了解两个重要的垃圾收集性能的度量:

  • Throughput(吞吐量) is the percentage of total time not spent in garbage collection considered over long periods of time. Throughput includes time spent in allocation (but tuning for speed of allocation is generally not needed). throughput = 运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集的时间) * 100%。
  • Pauses(用户线程停顿时间) are the times when an application appears unresponsive because garbage collection is occurring.因为垃圾收集而导致用户线程暂停的时长。

Hotspot包含的所有收集器:

 

 如果两个收集器之间有连线,则这两个收集器可以搭配使用
 

一、Serial收集器

顾名思义,单线程收集器,“单线程”不仅仅指单个线程或者单个cpu去完成垃圾收集的工作,更重要的是在垃圾收集过程中,所有用户线程都必须暂停,等待整个垃圾收集完成后才恢复执行,官方称为“Stop The World”,新生代young neneration采用停止复制算法,老年代tenured generation采用标记整理算法(如下图)。该收集器是Hotspot在Client模式下的默认新生代young generation的收集器。

 

Serial收集器运行过程图示: 

 

 

Serial Collector有以下特点:

 

  • Serial Collector does Minor Collection and Full Collection serially in a stop-the-world fashion. That is, the Java application execution is halted while collection is taking place.
  • Serial Collector uses the 3-area (Eden, Survivor Space "From" and Survivor Space "To") algorithm for Minor Collections in the Young Generation.改进版的停止复制算法
  • Serial Collector uses the 3-step (Mark, Sweep and Compact) algorithm for Full Collections in the Tenured Generation.标记整理算法
  • Serial Collector is the default garbage collector in HotSpot Client 1.5 and higher.
  • Serial Collector can be specified using the "-XX:+UseSerialGC" command line option.

优点:

  • 相比于其他收集器更简单高效,因为单线程没有线程切换、交互的开销。

缺点:

  • Stop The World对于客户端来说可以忍受,但服务端往往不能

二、ParNew收集器

Serial收集器的多线程版本,除了使用多个线程执行垃圾收集这一点之外,其余行为和Serial收集器一致。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定。默认开启的线程数与CPU的数量相同,如果CPU核数较多,可通过-XX:ParallelGCThreads=<N>来指定线程个数。

ParNew收集器运行过程图示:

 

 

三、Parallel Scanvange收集器

也称Throughput Garbage Collector,是一个新生代收集器,它是使用停止复制算法的多线程收集器,这些都和ParNew收集器相同,但Parallel Scanvange收集器的目标是达到一个可控制的Throughput(吞吐量),其提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis=<N>(N为毫秒)以及直接设置吞吐量大小的-XX:GCTimeRatio=<N>(0 < N < 100),

 

-XX:MaxGCPauseMillis=<N>:N为大于0的毫秒数,收集器尽可能让垃圾收集过程花费少于N ms,你把该参数设小以后,会导致throughput也变小,因为N变小后,JVM会相应地地新生代的大小调小,从而使单次收集时间变短,但是这也就相应地导致触发GC的次数会增加,从而导致整个JVM周期内用在GC上的时间会边长,吞吐量也就变低了。所以说该收集器是以牺牲吞吐量和新生代空间来换取GC停顿时间的缩短的。

 

-XX:GCTimeRatio=<N>:即User time : GC time,N必须大于0小于100,例如N = 19,则允许的最大GC时间就占总时间的5%(1 / (1 + 19)* 100%)

 

自适应调节策略:Parallel Scanvange收集器还提供一个开关参数-XX:+UseAdaptiveSizePolicy,当打开该开关参数时,JVM会根据垃圾收集的性能信息动态调整新生代的大小、Eden和Survivor的比例(该比例是通过参数-XX:SurvivorRatio设置的)、晋升老年代对象大小(该值是通过参数-XX:PretenuredSizeThreshold指定的)。

 

工作过程如下图:

 

 

四、Serial Old收集器

 是Serial收集器的老年代版本,也是单线程收集器。

 工作过程如下图:

 

五、Parallel Old收集器

是Parallel Scanvange收集器的老年代版本,使用多线程和标记整理算法。 

 

工作过程如下图:

 

 

六、CMS收集器

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

 

  • 初始标记(CMS Initial mark):标记GC Roots能直接关联到的对象。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程。
  • 重新标记(CMS remark):修正并发标记阶段因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录。
  • 并发清除(CMS concurrent sweep):执行清除过程

耗时的两个阶段并发标记和并发清除都是收集线程和用户线程并发执行的,所以其停顿时间是很短的。

 

缺点:

  • CMS收集器无法回收“浮动垃圾”

由于CMS并发清理阶段用户线程还在运行,伴随程序的运行自热会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留待下一次GC时将其清理掉。这一部分垃圾称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,即需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分内存空间提供并发收集时的程序运作使用。在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。

 

  • 内存碎片的问题:

CMS收集器是使用“标记-清除”算法收集的,这会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,往往出现老年代还有很大的剩余内存空间但却找不到足够大的一块来容下一个较大的对象,从而来分配不得不提前触发一次Full  GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认该开关参数就是开着的),用于在Full  GC时增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数(默认为0)设置执行多少次不带压缩的Full  GC之后,跟着来一次带压缩的Full GC。

 工作过程如下图:



 

七、G1收集器(Garbage-First)

 

G1是面向服务端应用的垃圾收集器,有如下特点:

 

    • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的“标记-清理”算法不同,G1从整体看来是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上看是基于“复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另外一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器特征了。

 在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域

(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

 

  G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验        值),在后台维护一个优先列表,每次根据允许的收集时间,优先回价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限        的时间内获可以获取尽可能高的收集效率。
  G1把内存“化整为零”的思路,理解起来似乎很容易理解,但其中的实现细节却远远没有现象中简单,否则也不会从04年Sun实验室发表第一篇G1的论文拖至今将近8年时间都还没有开发出G1的商用版。笔者举个一          个细节为例:把Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?听起来顺理成章,再仔细想想就很容易发现问题所在:Region不可能是孤立的。一个对象分配在某个Region中,它并非只        能被本Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保障准确性?这个问题其实并非在G1中才有,只是        在G1中更加突出了而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象也面临过相同的问题,如果回收新生代时也不得不同时扫描老          年代的话,Minor GC的效率可能下降不少。 
  在G1收集器中Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发         现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查引是否老年代中的对象引用了新生代中          的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有         遗漏。

G1收集过程:

1.初始标记:标记GC Roots能直接关联到的对象,并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短

2.并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行

3.最终标记:修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的                      数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行

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

 

工作过程如下图:


The first focus of G1 is to provide a solution for users running applications that require large heaps with limited GC latency. This means heap sizes of around 6GB or larger, and stable and predictable pause time below 0.5 seconds.

推荐使用场景:

1.More than 50% of the Java heap is occupied with live data // 堆中超过50%时存活的对象

2.The rate of object allocation rate or promotion varies significantly // 对象分配频率或年代提升频率变化很大的

3.Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second) //短停顿的


 

参考:

《深入理解Java虚拟机》

http://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html

http://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

猜你喜欢

转载自lixiaohui.iteye.com/blog/2330749