深入理解java虚拟机—— 超详细虚拟机垃圾收集器原理,GC 垃圾收集器算法的深度解析,项目中如何选择合适的垃圾收集器

 

目录

一、收集器的功能

1、哪些内存需要回收?

2、什么时候回收?

3、如何回收?

标记-清除(Mark-Sweep)算法

复制算法算法

标记-整理算法

分代收集算法

二、垃圾收集器有那些

并发垃圾收集和并行垃圾收集的区别

Minor GC和Major GC的区别

虚拟机经典回收器,7大垃圾回收器:

1、Serial 收集器

        2、ParNew收集器

        3、Parallel Scavenge收集器

        4、Serial Old收集器

        5、Parallel Old收集器

        6、CMS收集器

        7、G1收集器

G1内存布局:


 

 

先看一张图片:

有7中收集器  -- Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。

 

(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物。事实上,GC的历史远比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当List还在胚胎时期时,人们就在思考GC需要完成的3件事情:

虽然目前动态分配与内存回收技术已经相当成熟,一切看起来都进入了“自动化”时代,但只有了其内在,才能让我们写好每一句代码。接下来就以上这三个问题逐一揭晓。

 

一、收集器的功能

 

1、哪些内存需要回收?

从JVM区域结构看,可将这些区域划分为“静态内存”和“动态内存”两类。程序计数器、虚拟机栈、本地方法3个区域是“静态”的,因为这几个区域的内存分配和回收都具备确定性,都随着线程而生,随着线程而灭。但Java堆和方法区不一样,内存分配都存在不确定性,只有在程序处于运行期间才能知道会创建哪些对象,这部分内存和回收都是动态的,垃圾收集器所关注的是这部分内存。

“静态”区域的内存分配和回收已经很明确了,但“动态”区域到底是如何确定哪些内存该回收,哪些内存不该回收呢?我们都知道,Java是一种面向对象语言,在“动态”内存区域堆中的内存分配都是以“对象”为单位存在(方法区后面再说)。如果要回收,肯定是回收哪些没有用的对象了,那么又怎么确定对象已经没用呢?请参考  我的上篇文章    虚拟机垃圾收集器最新垃圾回收器 如何回收何时回收对象

 

2、什么时候回收?

判断对象是否存活一般有两种方式:

引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。

 

3、如何回收?

垃圾收集器是回收以上描述无用对象的具体实现,而垃圾收集算法又是垃圾收集器的方法论。所以,在介绍垃圾收集器之前,先了解一下这些垃圾收集算法,有助于理解后续的垃圾收集器的实现。

 

  • 标记-清除(Mark-Sweep)算法

标记-清除(Mark-Sweep)算法是一种基础的收集算法。

算法思路

       "标记-清除"算法,分为两个阶段:

1、标记-清除

标记:

        首先标记出所有需要回收的对象;

(1)、第一次标记

       在可达性分析后发现对象到GC Roots没有任何引用链相连时,被第一次标记;

       并且进行一次筛选:此对象是否必要执行finalize()方法;

       对有必要执行finalize()方法的对象,被放入F-Queue队列中;    

(2)、第二次标记

       GC将对F-Queue队列中的对象进行第二次小规模标记;

       在其finalize()方法中重新与引用链上任何一个对象建立关联,第二次标记时会将其移出"即将回收"的集合;

       对第一次被标记,且第二次还被标记(如果需要,但没有移出"即将回收"的集合),就可以认为对象已死,可以进行回收。


详细步骤参考《 虚拟机垃圾收集器最新垃圾回收器 如何回收何时回收对象

清除:

   两次标记后,还在"即将回收"集合的对象将被统一回收;

 

2、优点

       基于最基础的可达性分析算法,它是最基础的收集算法;

       而后续的收集算法都是基于这种思路并对其不足进行改进得到的;

3、缺点

       主要有两个缺点:

(1)、效率问题

       标记和清除两个过程的效率都不高;

(2)、空间问题

       标记清除后会产生大量不连续的内存碎片;

       这会导致分配大内存对象时,无法找到足够的连续内存;

       从而需要提前触发另一次垃圾收集动作;
 

4、应用场景

      针对 老年代 的 CMS 收集器;

 

标记过程如下:

 

标记——清除过程图:

 

  • 复制算法算法

 "复制"(Copying)收集算法,为了解决标记-清除算法的效率问题;

1、算法思路

       (A)、把内存划分为大小相等的两块(待改良使用),每次只使用其中一块;

       (B)、当一块内存用完了,就将还存活的对象复制到另一块上(而后使用这一块);

       (C)、再把已使用过的那块内存空间一次清理掉,而后重复步骤2;   

2、优点

       这使得每次都是只对整个半区进行内存回收;

       内存分配时也不用考虑内存碎片等问题(可使用"指针碰撞"的方式分配内存);

      实现简单,运行高效;

       (关于"指针碰撞"请参考  虚拟机为新生对象分配内存的两种分方式 、《Java对象在HotSpot虚拟机中的创建过程》)

3、缺点

(A)、空间浪费

      可用内存缩减为原来的一半,太过浪费(解决:可以改良,不按1:1比例划分);

(B)、效率随对象存活率升高而变低(存活对象越多,复制效率越低,需要复制对象变多)

      当对象存活率较高时,需要进行较多复制操作,效率将会变低(解决:后面的标记-整理算法);

4、应用场景

      现在商业JVM都采用这种算法(通过改良缺点 (A) )来回收新生代;

            现在的商业虚拟机都采用这种收集算法来回收新生代,有企业分析的得出其实并不需求将内存按1:1的比例划分,因为新生代              中的对象大部分都是“朝生夕死”的。所以,HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1。一块Eden和两块                          Survivor,每次使用一块Eden和一块Survivor,也就是说只有10%是浪费的。如果另一块Survivor都无法存放上次垃圾回收的              对象时,那这些对象将通过“担保机制”进入老年代了。

      如Serial收集器、ParNew收集器、Parallel Scavenge收集器、、G1(从局部看);

复制算法执行过程:

HotSpot虚拟机的改良   复制算法算法

 

(A)、弱代理论

       分代垃圾收集基于弱代理论(weak generational hypothesis),具体描述如下:

       (1)、大多数分配了内存的对象并不会存活太长时间,在处于年轻代时就会死掉;

       (2)、很少有对象会从老年代变成年轻代;

            其中IBM研究表明:新生代中98%的对象都是"朝生夕死";

            所以并不需要按1:1比例来划分内存(解决了缺点1);

(B)、HotSpot虚拟机新生代内存布局及算法

      (1)、将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;

      (2)、每次使用Eden和其中一块Survivor;

      (3)、当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;

      (4)、而后清理掉Eden和使用过的Survivor空间;

      (5)、后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;

默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;

(C)、分配担保

       如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制(Handle Promotion)进入老年代;

       分配担保在以后讲解垃圾收集器执行规则时再详解;

 

  • 标记-整理算法

 "标记-整理"(Mark-Compact)算法是根据老年代的特点提出的。

1、算法思路

(1)、标记

      标记过程与"标记-清除"算法一样;

(2)、整理

       但后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动;

       然后直接清理掉端边界以外的内存;

2、优点

(A)、不会像复制算法,效率随对象存活率升高而变低

       老年代特点:

       对象存活率高,没有额外的空间可以分配担保;

       所以老年代一般不能直接选用复制算法算法;

       而选用标记-整理算法;

(B)、不会像标记-清除算法,产生内存碎片

       因为清除前,进行了整理,存活对象都集中到空间一侧;

3、缺点

       主要是效率问题:除像标记-清除算法的标记过程外,还多了需要整理的过程,效率更低;

4、应用场景

       很多垃圾收集器采用这种算法来回收老年代;

      如Serial Old收集器、G1(从整体看);

标记整理过程如下:

 

  • 分代收集算法

 "分代收集"(Generational Collection)算法结合不同的收集算法处理不同区域。

1、算法思路

       基于前面说的弱代理论,其实并没有什么新的思想;

       只是根据对象存活周期的不同将内存划分为几块;

       这样就可以根据各个年代的特点采用最适当的收集算法;

       一般把Java堆分为新生代和老年代;

(A)、新生代

       每次垃圾收集都有大批对象死去,只有少量存活

       所以可采用复制算法;

(B)、老年代

       对象存活率高,没有额外的空间可以分配担保;

      使用"标记-清理"或"标记-整理"算法;
 

2、优点      

       可以根据各个年代的特点采用最适当的收集算法;

3、缺点      

       仍然不能控制每次垃圾收集的时间;
4、应用场景

      目前几乎所有商业虚拟机的垃圾收集器都采用分代收集算法;

      如HotSpot虚拟机中全部垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(也保留);
 

 

二、垃圾收集器有那些

 

JDK7/8后,HotSpot虚拟机所有收集器及组合(连线)

图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。接下来将逐一介绍这些收集器的目标、特性、原理和使用场景,并重点分析CMS和G1这两款相对复杂而又广泛使用的收集器,深入了解它们的部分运作细节。 

 

(1)、图中展示了7种不同分代的收集器:

       Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

(2)、而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

      新生代收集器:Serial、ParNew、Parallel Scavenge;

      老年代收集器:Serial Old、Parallel Old、CMS;

      整堆收集器:G1;

(3)、两个收集器间有连线,表明它们可以搭配使用:

       [Serial/Serial Old] 、[Serial/CMS] 、[ParNew/Serial Old] 、[ParNew/CMS] 、[Parallel Scavenge/Serial Old]、 [Parallel Scavenge/Parallel Old] 、 [G1] ;

(4)、其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案(后面介绍);
 

并发垃圾收集和并行垃圾收集的区别

(1)、并行(Parallel)

       指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

       如ParNew、Parallel Scavenge、Parallel Old;

(2)、并发(Concurrent)

       指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行);

      用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;    

       如CMS、G1(也有并行);
 

Minor GC和Major GC的区别

(A)、Minor GC

       又称新生代GC,指发生在新生代的垃圾收集动作;

       因为Java对象大多是朝生夕灭,所以Minor GC非常频繁,一般回收速度也比较快;

(B)、Major GC

       又称Full GC或老年代GC,指发生在老年代的GC;

       出现Major GC经常会伴随至少一次的Minor GC(不是绝对,Parallel Sacvenge收集器就可以选择设置Major GC策略);

      Major GC速度一般比Minor GC慢10倍以上
 

 

没有最好的收集器,更没有万能的收集;

选择的只能是适合具体应用场景的收集器。

 

垃圾回收期的演变历程

 

1、Serial 收集器

介绍

    Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。

特点

  1. 这个收集器是一个单线程工作的收集器。
  2. 它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。会发生 Stop The World”
  3. 默认新生代收集器。
  4. 采用复制算法;

 Serial/Serial Old组合收集器运行示意图如下:

应用场景

      依然是HotSpot在Client模式下默认的新生代收集器

      也有优于其他收集器的地方:

      简单高效(与其他收集器的单线程相比);

      对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;

      在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十ms至一百多ms),        只要不频繁发生,这是可以接受的
 

设置参数

      "-XX:+UseSerialGC":添加该参数来显式的使用串行垃圾收集器;

Stop TheWorld说明:

      JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;

      会带给用户不良的体验;

      从JDK1.3到现在,从Serial收集器--Parallel收集器--CMS--G1,用户线程停顿时间不断缩短,但仍然无法完全消除
 

 

2、ParNew收集器

 介绍

    ParNew收集器实质上是 Serial收集器的多线程并行版本

特点:

    除了多线程外,其余的行为、特点和Serial收集器一样;

 如Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:                                HandlePromotionFailure   等)、收集算法[复制算法]、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致

    ParNew 、Serial  这两种收集器也共用了相当多的代码。

ParNew/Serial Old组合收集器运行示意图如下:

应用场景:

      在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作

      但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销。

设置参数:

      "-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器;

      "-XX:+UseParNewGC":强制指定使用ParNew;    

      "-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同;

为什么只有ParNew能与CMS收集器配合:

      CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基        本上)同时工作;

      CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作;

      因为 Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的       框架代码;

      关于CMS收集器后面会详细介绍。

 

注意 :  从ParNew收集器开始,后面还将会接触到若干款涉及“并发”和“并行”概念的收集器。在大家可能产生疑惑之前,有必要先解释清                 楚这两个名词。并行和并发都是并发编程中的专业名词,在谈论垃圾收集器的上下文语境中,它们可以理解为:

并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。

并发(Concurrent):并发描述的是垃圾收集器线程用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响

3、Parallel Scavenge收集器

介绍: 

  Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector)

特点:

  有一些特点与ParNew收集器相似

      新生代收集器;

      采用复制算法

      多线程收集;

  主要特点是:它的关注点与其他收集器不同

      CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间

      而Parallel Scavenge收集器的目标则是 达到一个可控制的 吞吐量(Throughput)

         【Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地 缩短垃圾收集时 用户             线 程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器             用于运行用户代码的时间与处理器  消耗时间  的比值 】 

      关于吞吐量与收集器关注点说明详见本节后面;
 

应用场景:

      高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间;

      当应用程序运行在具有多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台进行计算,而不需要与用户进行太多交互;

      例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序;
 

设置参数:

 Parallel Scavenge收集器提供两个参数用于精确控制吞吐量:

(1)、"-XX:MaxGCPauseMillis"

      控制最大垃圾收集停顿时间,大于0的毫秒数;

      MaxGCPauseMillis设置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降;

      因为可能导致垃圾收集发生得更频繁;不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得系统的垃圾收        集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调得小一些,收集300MB新        生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集          一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

(2)、"-XX:GCTimeRatio"

      设置垃圾收集时间占总时间的比率,0<n<100的整数;

      GCTimeRatio相当于设置吞吐量大小;

      垃圾收集执行时间占应用程序执行时间的比例的计算方法是:

      1 / (1 + n)

      例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%--1/(1+19);

      默认值是1%--1/(1+99),即n=99;
 

(3)、"-XX:+UseAdptiveSizePolicy"

      开启这个参数后,就不用手工指定一些细节参数,如:

      新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-                                              XX:PretenureSizeThreshold)等;  

      JVM会根据当前系统运行情况收集性能监控信息,动态调整这些参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式        称为GC自适应的调节策略(GC Ergonomiscs);    


     这是一种值得推荐的方式

      (1)、只需设置好内存数据大小(如"-Xmx"设置最大堆);

      (2)、然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"给JVM设置一个优化目标;

      (3)、那些具体细节参数的调节就由JVM自适应完成;        

      这也是Parallel Scavenge收集器与ParNew收集器一个重要区别; 
 

吞吐量与收集器关注点说明:

1、吞吐量(Throughput)

      CPU用于运行用户代码的时间CPU总消耗时间的比值;

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

      高吞吐量即减少垃圾收集时间,让用户代码获得更长的运行时间;

【 如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。】

2、垃圾收集器期望的目标(关注点)

  (1)、停顿时间    

        停顿时间越短就适合需要与用户交互的程序;

        良好的响应速度能提升用户体验;

  (2)、吞吐量

        高吞吐量则可以高效率地利用CPU时间,尽快完成运算的任务;

        主要适合在后台计算而不需要太多交互的任务;

  (3)、覆盖区(Footprint)

        在达到前面两个目标的情况下,尽量减少堆的内存空间;

        可以获得更好的空间局部性;
 

 

4、Serial Old收集器

介绍: 

    Serial Old是 Serial收集器 的老年代版本

特点:

      针对老年代

      采用"标记-整理"算法(还有压缩,Mark-Sweep-Compact);

      单线程收集;

 Serial/Serial Old收集器运行示意图如下:

应用场景

      主要意义也是供 客户端模式 下的HotSpot虚拟机使用;

      而在Server模式有两大用途:

      (A)、在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);

      (B)、作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用(后面详解);
 

5、Parallel Old收集器

介绍: 

    Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;

      JDK1.6中才开始提供;

特点:

      针对老年代

      采用"标记-整理"算法;

      多线程收集;

Parallel Scavenge/Parallel Old收集器运行示意图如下:

应用场景:

      JDK1.6及之后用来代替老年代的Serial Old收集器;

      特别是在Server模式,多CPU的情况下;

      这样在  注重吞吐量以及CPU资源敏感的场景 ,就有了Parallel Scavenge加Parallel Old收集器的"给力"应用组合;

设置参数:

      "-XX:+UseParallelOldGC":指定使用Parallel Old收集器;
 

Parallel Old 收集器是直到JDK 6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了ParallelScavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样,由於单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比  ParNew加CMS  的组合来得优秀。

 

6、CMS收集器

介绍:

    并发 "标记-清除"(Concurrent Mark Sweep,CMS)收集器也称为并发低停顿收集器(Concurrent Low Pause Collector)或低延        迟(low-latency)垃圾收集器;

    在前面ParNew收集器曾简单介绍过其特点;

 

特点:

      针对老年代

      基于"标记-清除"算法(不进行压缩操作,产生内存碎片);            

      以获取最短回收停顿时间为目标

      并发收集、低停顿;

      需要更多的内存(看后面的缺点);

 是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器

 第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;

 

应用场景:

      与用户交互较多的场景;        

      希望系统停顿时间最短,注重服务的响应速度;

      以给用户带来较好的体验;

      如常见WEB、B/S系统的服务器上的应用

 

设置参数:

      "-XX:+UseConcMarkSweepGC":指定使用CMS收集器;

 

CMS收集器运作过程:

      比前面几种收集器更复杂,可以分为4个步骤:

(1)、初始标记(CMS initial mark)       -- 需要 “Stop The World”

      仅标记一下GC Roots能直接关联到的对象;

      速度很快;

      但需要"Stop The World";

(2)、并发标记(CMS concurrent mark)--耗时长 可并发

     【并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以         与垃圾收集线程一起并发运行;】      

      进行GC Roots Tracing 的过程;

      刚才产生的集合中标记出存活对象;

      用户应用程序也在运行;

      并不能保证可以标记出所有的存活对象;

(3)、重新标记(CMS remark)           -- 需要 “Stop The World”

      为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;

      需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

      采用多线程并行执行来提升效率;

(4)、并发清除(CMS concurrent sweep) --耗时长  可并发

      回收所有的垃圾对象;

 

初始标记、重新标记这两个步骤仍然需要“Stop The World”

 整个过程中耗时最长的并发标记并发清除都可以与用户线程一起工作;

 所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;

 CMS收集器运行示意图如下:

通过图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。

CMS收集器3个明显的缺点:

(1)、对CPU资源非常敏感

      并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。

      CMS的默认收集线程数量是=(CPU数量+3)/4;  【此处如何得出占用不超过25%的处理器运算资源 ?
      如果处理器核心数在4个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器运算资源,并且会随着处理器核心数量的          增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。

     如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。

为了缓解这种情况:

      增量式并发收集器:

        针对这种情况,曾出现了"增量式并发收集器"(Incremental Concurrent Mark Sweep/i-CMS);

        类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间;

        但效果并不理想,JDK1.6后就官方不再提倡用户使用。
 

(2)、无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败

   

      浮动垃圾(Floating Garbage):

        在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;

        这使得并发清除时需要预留一定的内存空间,不能像其他收集器在老年代几乎填满再进行收集;

        也要可以认为CMS所需要的空间比其他垃圾收集器大;

        "-XX:CMSInitiatingOccupancyFraction":设置CMS预留内存空间;

        JDK1.5默认值为68%;

        JDK1.6变为大约92%;             

     "Concurrent Mode Failure"失败:

        如果CMS预留内存空间无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败;

        这时JVM启用后备预案:临时启用Serail Old收集器,而导致另一次Full GC的产生;

        这样的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置得太大。
 

(3)、产生大量内存碎片

      由于CMS基于"标记-清除"算法,清除后不进行压缩操作;

      前面   "标记-清除"算法  介绍时曾说过:

      产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。
 

     解决方法:                

        "-XX:+UseCMSCompactAtFullCollection"

            使得CMS出现上面这种情况时不进行Full GC,而开启内存碎片的合并整理过程;

            但合并整理过程无法并发,停顿时间会变长;

            默认开启(但不会进行,结合下面的CMSFullGCsBeforeCompaction);

       "-XX:+CMSFullGCsBeforeCompaction"

           设置执行多少次不压缩的Full GC后,来一次压缩整理;

           为减少合并整理过程的停顿时间;

           默认为0,也就是说每次都执行Full GC,不会进行压缩整理;
  

由于空间不再连续,CMS需要使用可用"空闲列表"内存分配方式,这比简单实用"碰撞指针"分配内存消耗大;

 总体来看,与Parallel Old垃圾收集器相比,CMS减少了执行老年代垃圾收集时应用暂停的时间;

 但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量而且需要占用更大的堆空间;

 

7、G1收集器

JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。

G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的HumongousRegion之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待

介绍:

 G1(Garbage-First)是JDK7-u4才推出商用的收集器;

特点:

(1)、并行与并发

      能充分利用多CPU、多核环境下的硬件优势;

      可以并行来缩短"Stop The World"停顿时间;

      也可以并发让垃圾收集与用户程序同时进行;

(2)、分代收集,收集范围包括新生代和老年代    

      能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;

      能够采用不同方式处理不同时期的对象;

                

      虽然保留分代概念,但Java堆的内存布局有很大差别;

      将整个堆划分为多个大小相等的独立区域(Region);

      新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合;
 

(3)、结合多种垃圾收集算法,空间整合,不产生碎片

      从整体看,是基于  标记-整理  算法;

      从局部(两个Region间)看,是基于  复制算法

      这是一种类似火车算法的实现;

 

      都不会产生内存碎片,有利于长时间运行;

(4)、可预测的停顿:低停顿的同时实现高吞吐量

      G1除了追求低停顿处,还能建立可预测的停顿时间模型;

      可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒;
 

 

应用场景:

      面向服务端应用,针对具有大内存、多处理器的机器;

      最主要的应用是为需要低GC延迟,并具有大堆的应用程序提供解决方案;

      如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;

            

      用来替换掉JDK1.5中的CMS收集器;

      在下面的情况时,使用G1可能比CMS好:

      (1)、超过50%的Java堆被活动数据占用;

      (2)、对象分配频率或年代提升频率变化很大;

      (3)、GC停顿时间过长(长于0.5至1秒)。
 

设置参数

      "-XX:+UseG1GC":指定使用G1收集器;

      "-XX:InitiatingHeapOccupancyPercent":当整个Java堆的占用率达到参数值时,开始并发标记阶段;默认为45;

      "-XX:MaxGCPauseMillis":为G1设置暂停时间目标,默认值为200毫秒;

      "-XX:G1HeapRegionSize":设置每个Region大小,范围1MB到32MB;目标是在最小Java堆时可以拥有约2048个Region;
 

为什么G1收集器可以实现可预测的停顿

      G1可以建立可预测的停顿时间模型,是因为:

      可以有计划地避免在Java堆的进行全区域的垃圾收集;

      G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表;

      每次根据允许的收集时间,优先回收价值最大的Region(名称Garbage-First的由来);

      这就保证了在有限的时间内可以获取尽可能高的收集效率;
 

一个对象被不同区域引用的问题

      一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个              Java堆才能保证准确?

      在其他的分代收集器,也存在这样的问题(而G1更突出):

      回收新生代也不得不同时扫描老年代?

      这样的话会降低Minor GC的效率;

      解决方法:

           无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描

           每个Region都有一个对应的Remembered Set;

           每次Reference类型数据写操作时,都会产生一个Write Barrier暂时中断操作;

           然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);

           如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对应的Remembered Set中;

           当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;

           就可以保证不进行全局扫描,也不会有遗漏。

 

G1收集器运作过程

不计算维护Remembered Set的操作,可以分为4个步骤(与CMS较为相似)。

(1)、初始标记(Initial Marking)

      仅标记一下GC Roots能直接关联到的对象;

      且修改TAMS(Next Top at Mark Start),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象;

      需要"Stop The World",但速度很快;

(2)、并发标记(Concurrent Marking)

      进行GC Roots Tracing的过程;

      刚才产生的集合中标记出存活对象;

      耗时较长,但应用程序也在运行;

      并不能保证可以标记出所有的存活对象;

(3)、最终标记(Final Marking)

      为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;

      上一阶段对象的变化记录在线程的Remembered Set Log;

      这里把Remembered Set Log合并到Remembered Set中;

                    

      需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;

      采用多线程并行执行来提升效率;

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

      首先排序各个Region的回收价值和成本;

      然后根据用户期望的GC停顿时间来制定回收计划;

      最后按计划回收一些价值高的Region中垃圾对象;

      可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

                    

      回收时采用"复制"算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;

      可以并发进行,降低停顿时间,并增加吞吐量;
 

  G1收集器运行示意图如下:

其实本也有想过设计成与用户程序一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回收一部分Region,停顿时间是用户可控制的,所以并不迫切去实现,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。通过图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段。

G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的,换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才能担当起“全功能收集器”的重任与期望。

 

G1内存布局:

 

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。

 

图中: ERegion扮演新生代的Eden空间;    S : Region扮演Survivor空间 ;     H:Humongous区域作为老年代的一部分

 

G1开创的基于Region的堆内存布局,上图就是Region的堆内存布局,虽然G1仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间Survivor空间,或者老年代空间

Humongous区域,专门用来存储大对象 ,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。

 

  • 独立区域(Region)
  • 每个Region赋予角色(新生代的Eden空间、Survivor空间、老年代空间)
  • 每个角色的Region采用不同的垃圾回收算法
  • TAMS(Top at Mark Strar)指针
  • 每个Region单独划分出的一块空间
  • 用于在并发回收过程中新对象的分配
  • Humongous Regoin
  • 专门存储大对象(G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象

 

G1收集器关于停顿时间:

1、可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间。

2、可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。

3、设置一个合理的停顿时长:

不过,这里设置的“期望值”必须是符合实际的,不能异想天开,毕竟G1是要冻结用户线程来复制对象的,这个停顿时间再怎么低也得有个限度。它 默认的停顿 目标为200毫秒,一般来说,回收阶段占到几十到一百甚至接近两百毫秒都很正常,但如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为100-200毫秒或者200-300会是比较合理的。

 

G1收集器是垃圾收集技术发展的里程碑

开创了收集器 面向局部收集的设计思路 和 基于Region的内存布局形式。从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为 追求能够应付应用的内存分配速率(Allocation Rate),而不追求一次把整个Java堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。这种新的收集器设计思路从工程实现上看是从G1开始兴起的,所以说G1是收集器技术发展的一个里程碑。

 

G1收集器 和 CMS收集器对比:

1、G1可以指定最大停顿时间

2、G1分Region的内存布局,CMS整块内存

3、G1收集器的Mixed GC模式,与其他收集器的不同。

在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(MinorGC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

4、收集算法角度上:

G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现;CMS使用“标记-清除”算法实现。

5、内存角度上:

在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。 

   介绍:  关于G1和CMS内存占用的区别   :   虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不

需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的

6、执行负载的角度上

它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。

 

结论:

针对于最终哪款收集器要更好、要好上多少,往往是针对具体场景才能做的定量比较。目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,

 

 

 

 

 

 

 

 

 

 

 

 

 

补充:

只要运行java-version,便可以知道该JVM是运行在Client模式还是Server模式。

如下:

首先,我们来看下:

(1)32位 jdk 1.8中,默认的java -version的输出结果:

可以看到有Client VM,即运行的是客户端模式。

(2)64位 jdk 1.8中,默认的java -version的输出结果:


可以看到有Server VM,即运行的是服务端模式。

补充知识点:

如果是64位的jdk 1.8 amd64,只能运行在Server模式下。而32位的jdk 1.8 i386,默认是运行在client模式下,可以通过修改jdk/jre/lib/i386/jvm.cfg文件里面的设置来指定默认的启动模式。默认32位jdk 1.8 i386为:

-client IF_SERVER_CLASS -server
-server KNOWN
-minimal KNOWN


将第一行注释,之后有:

#-client IF_SERVER_CLASS -server
-server KNOWN
-minimal KNOWN
 

 

 

 

参考文献
《深入理解Java虚拟机:JVM高级特性与最佳实践》
《Java虚拟机规范 Java SE 8版》
《Java并发编程的艺术》
《How to Handle Java Finalization's Memory-Retention Issues》
《Effective Java》第二版 第2章 第7条:避免使用终结方法;
《Thinking in Java》第四版 5.5 清理:终结处理和垃圾回收;
《Java语言规范》12.6 类实例的终结;
 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章