<深入Java虚拟机>之1.3 各个版本垃圾收集器

   摘要:首先明确一点,目前Java没有一个各方面都很好的万能垃圾收集器(G1也不行),本文只是对比各个版本的垃圾收集器。


<深入Java虚拟机>之1.3 各个版本垃圾收集器_第1张图片

一. Serial 收集器

   在JDK 1.3之前是新生代唯一的收集器,是个单线程的,只使用一个CPU一个线程。当他开始GC的时候,必须停止其他线程才能“开工”---“Stop The World”。采用的是Copying算法,它的优点是实现简单高效,但是缺点是会给用户带来很大停顿。

二. ParNew 收集器

   Serial的多线程版本,使用并行,多个线程进行垃圾收集,是新生代的收集器,采用Copying算法。

三. Parallel Scavenge 收集器

   是新生代的并行的多线程的收集器,采用Copying算法。它与其他收集器最大的区别是它只关心吞吐。其他的收集器关心GC时,用户线程的停顿时间。

Throughout(吞吐量) = CPU运行用户代码的时间 / (CPU运行用户代码的时间 + GC的时间)

   “用户线程的停顿时间” 短代表, 用户端响应速度快,用户体验好。

   “吞吐量” 高代表,利用CPU使用率高,后台运算效率高。

   Parallel Scavenge 收集器有两个参数保障吞吐量:

   ️. 最大GC停顿时间 -XX:MaxGCPauseMillis

   ️. 直接设置吞吐量大小 -XX:GCTimeRatio

   有GC自适应的调节策略。


四. Serial Old 收集器

   老年代的Serial收集器版本,单线程,使用Mark-Compact(标记-整理)算法。两大用途:

   ️. JDK1.5之前和Parallel Scavenge搭配使用

   ️. 最为CMS 收集器de后备方案,在CMS发生Concurrent Mode Failure时使用。        (Concurrent Mode Failure)等下CMS会讲到


五. Parallel Old 收集器

   Parallel Scavenge的老年代版本,多线程,使用Mark-Compact(标记-整理)算法。

   配合Parallel Scavenge组成新,老生代的收集器组合。(用户吞吐量注重的场合)


六. CMS 收集器

   CMS(Concurrent Mark Sweep)收集器是以用户最短响应时间为目标的老年代收集器。很多互联网B/S业务系统服务端用这个。多线程的Mark-Sweep(标记-清除)算法。

   总体来说分成4个步骤:

   6.1 初始标记

   6.2 并发标记

   6.3 重新标记

   6.4 并发清除

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

   6.1 初始标记阶段需要STW

   该阶段进行可达性分析,标记GC ROOT能直接关联到的对象。注意是间接关联的对象在下一阶段标记。

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

   6.2 并发标记阶段是和用户线程并发执行的过程。

   该阶段进行GC ROOT TRACING,在第一个阶段被暂停的线程重新开始运行。由前阶段标记过的对象出发,所有可到达的对象都在本阶段中标记。

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

   6.3 重新标记

   是为了修正并发标记阶段用户线程继续运作导致的标记变动,对那部分进行标记纪录。会暂停所有用户线程。此阶段标记从新生代晋升的对象新分配到老年代的对象以及在并发阶段被修改了的对象

   此阶段比较复杂,从初学者容易忽略或者说不理解的地方抛出一个问题大家思考下

   如何确定老年代的对象是活着的?

   答案是扫描新生命代!


<深入Java虚拟机>之1.3 各个版本垃圾收集器_第2张图片
绿色标记为存活,通过扫描新生代

   这也是为什么CMS虽然是老年代的gc,但仍要扫描新生代的原因。(注意初始标记也会扫描新生代)

   ok,新生代的机制讲完了,下面讲讲老年代。

   老年代的机制与一个叫CARD TABLE的东西(这个东西其实就是个数组,数组中每个位置存的是一个byte)密不可分。

   CMS将老年代的空间分成大小为512bytes的块,card table中的每个元素对应着一个块。并发标记时,如果某个对象的引用发生了变化,就标记该对象所在的块为dirty card。重标记阶段就会重新扫描该块,将该对象引用的对象标识为可达。

   举个例子:并发标记时对象的状态:


<深入Java虚拟机>之1.3 各个版本垃圾收集器_第3张图片
但随后current obj的引用发生了变化:


<深入Java虚拟机>之1.3 各个版本垃圾收集器_第4张图片
current obj所在的块被标记为了dirty card。

   之后那些通过current obj变得可达的对象也被标记了,变成下面这样:


<深入Java虚拟机>之1.3 各个版本垃圾收集器_第5张图片
同时dirty card标志也被清除。

老年代的机制就是这样。

不过card table还有其他作用

   进行Minor GC时,如果有老年代引用新生代,怎么识别?

   (有研究表明,在所有的引用中,老年代引用新生代这种场景不足1%.

   当有老年代引用新生代,对应的card table被标识为相应的值(card table中是一个byte,有八位,约定好每一位的含义就可区分哪个是引用新生代,哪个是并发标记阶段修改过的)。

  所以,Minor GC通过扫描card table就可以很快的识别老年代引用新生代。hotspot 虚拟机使用字节码解释器、JIT编译器、 write barrier维护 card table。

   当字节码解释器或者JIT编译器更新了引用,就会触发write barrier操作card table.

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

   6.5 并发清理,用户线程重新激活,同时清理那些无效的对象。

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

   CMS有什么问题?

   ️. 并发意味着多线程抢占CPU资源,即GC线程与用户线程抢占CPU。这可能会造成用户线程执行效率下降。

CMS默认的回收线程数是(CPU个数+3)/4。这个公式的意思是当CPU大于4个时,保证回收线程占用至少25%的CPU资源,这样用户线程占用75%的CPU,这是可以接受的。但是,如果CPU资源很少,这就可能导致用户程序的执行速度忽然降低了50%,50%已经是很明显的降低了。

   ️. 并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾

由于垃圾回收阶段用户线程仍在执行,必需预留出内存空间给用户线程使用。因此不能像其他回收器那样,等到老年代满了再进行GC。

CMS 提供了CMSInitiatingOccupancyFraction参数来设置老年代空间使用百分比,达到百分比就进行垃圾回收。这个参数默认是92%,参数选择需要看具体的应用场景。设置的太小会导致频繁的CMS GC,产生大量的停顿;反过来想,设置的太高会发生什么?假设现在设置为99%,还剩1%的空间可以使用。在并发清理阶段,若用户线程需要使用的空间大于1%,就会产生Concurrent  Mode Failure错误,意思就是说并发模式失败。这时,虚拟机就会启动备案:使用Serial Old收集器重新对老年代进行垃圾回收.如此一来,停顿时间变得更长。

   其实CMS有动态检查机制。CMS会根据历史记录,预测老年代还需要多久填满及进行一次回收所需要的时间。在老年代空间用完之前,CMS可以根据自己的预测自动执行垃圾回收。这个特性可以使用参数UseCMSInitiatingOccupancyOnly来关闭。

   ️. 还有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。

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

   七. G1 收集器

   G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征. 在JDK 7 update 4 及以上版本中得到完全支持, 是个多线程并行的采用Mark-Compact算法的新生代/老生代通用收集器。为了取代CMS。有更可控、可预期的GC停顿周期的优势。

   上一代的垃圾收集器(串行serial, 并行parallel, 以及CMS)都把堆内存划分为固定大小的三个部分: 年轻代(young generation), 年老代(old generation), 以及持久代(permanent generation).


<深入Java虚拟机>之1.3 各个版本垃圾收集器_第6张图片
内存中的每个对象都存放在这三个区域中的一个.

   G1 收集器采用一种不同的方式来管理堆内存:


<深入Java虚拟机>之1.3 各个版本垃圾收集器_第7张图片
将Java堆分成很多小的region

   G1之所以能建立起可预测的停顿时间模型是因为G1能够跟踪各个region里面回收的价值大小,维护一个优先队列,优先回收价值最大的region。region之间的引用和新老生代的相互引用G1用remembered set来记录避免全局扫描。每个region都有remembered set对应。发现引用有变化会产生write barrier暂时中断更新引用,检查引用的对象是不是在不同region之中,是的话就通过cardtable把相关引用信息纪录到引用对象所属的region和remember set中。当进行gc时候,GC roots枚举范围加入了remembered set,保障不会全局扫描,G1清理的步骤和CMS有很多相似可以参照CMS阅读。


   JDK7默认仍然采用CMS、那么G1相对于CMS的区别在:

1. G1在压缩空间方面有优势

2. G1通过将内存空间分成区域(Region)的方式避免内存碎片问题

3. Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活

4. G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象

5. G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做

6. G1会在Young GC中使用、而CMS只能在O区使用

就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:

服务端多核CPU、JVM内存占用较大的应用(至少大于4G)

应用在运行过程中会产生大量内存碎片、需要经常压缩空间

想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象

你可能感兴趣的:(<深入Java虚拟机>之1.3 各个版本垃圾收集器)