垃圾收集器和内存分配策略

程序计数器,栈和本地方法栈随线程而灭,但是堆和方法区所需要的内存不确定,只有到程序运行期间才能指定要创建哪些对象,这部分的内存分配和回收是动态的

1.垃圾收集器在对堆进行回收时,需要确定哪些对象还“存活”,

  • 引用计数算法(java中没有用来管理内存),虽然简单,但是难以解决对象之间相互循环引用的问题
  • 根搜索算法:

    由于引用计数算法的缺陷,所以JVM一般会采用一种新的算法,叫做根搜索算法。它的处理方式就是,设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可以被回收的。

        就拿上图来说,ObjectD和ObjectE是互相关联的,但是由于GC roots到这两个对象不可达,所以最终D和E还是会被当做GC的对象,上图若是采用引用计数法,则A-E五个对象都不会被回收,说到GC roots(GC根),在JAVA语言中,可以当做GC roots的对象有以下几种:

    1、虚拟机栈中的引用的对象。

    2、方法区中的类静态属性引用的对象。

    3、方法区中的常量引用的对象。

    4、本地方法栈中JNI的引用的对象。

    第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。

根搜索算法解决的是垃圾搜集的基本问题,也就是上面提到的第一个问题,也是最关键的问题,就是哪些对象可以被回收,不过垃圾收集显然还需要解决后两个问题,什么时候回收以及如何回收,在根搜索算法的基础上,现代虚拟机的实现当中,垃圾搜集的算法主要有三种,分别是标记-清除算法、复制算法、标记-整理算法,这三种算法都扩充了根搜索算法,不过它们理解起来还是非常好理解的。

 根搜索算法(并没有解决如何回收和何时回收两个问题),它可以解决我们应该回收哪些对象的问题,但是它显然还不能承担垃圾搜集的重任,因为我们在程序(程序也就是指我们运行在JVM上的JAVA程序)运行期间如果想进行垃圾回收,就必须让GC线程与程序当中的线程互相配合,才能在不影响程序运行的前提下,顺利的将垃圾进行回收。因此出现了下面几种基于根搜索算法的算法。

2.垃圾收集算法:引用计数法,标记-清除算法、复制算法(新生代)、标记-整理算法(老生代),分代算法(复制算法+标记-整理算法),分区算法

 

(1) 引用计数法

引用计数法是最经典的一种垃圾回收算法。其实现很简单,对于一个A对象,只要有任何一个对象引用了A,则A的引用计算器就加1,当引用失效时,引用计数器减1.只要A的引用计数器值为0,则对象A就不可能再被使用。

虽然其思想实现都很简单(为每一个对象配备一个整型的计数器),但是该算法却存在两个严重的问题

1)  无法处理循环引用的问题,因此在Java的垃圾回收器中,没有使用该算法

2)  引用计数器要求在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作,对系统性能会有一定的影响。

 

一个简单的循环引用问题描述:

对象A和对象B,对象A中含有对象B的引用,对象B中含有对象A的引用。此时对象A和B的引用计数器都不为0,但是系统中却不存在任何第三个对象引用A和B。也就是说A和B是应该被回收的垃圾对象,但由于垃圾对象间的互相引用使得垃圾回收器无法识别,从而引起内存泄漏(由于某种原因不能回收垃圾对象占用的内存空间)。

如下图:不可达对象出现循环引用,它的引用计数器不为0,

 

 

注意:由于引用计数器算法存在循环引用以及性能的问题,java虚拟机并未使用此算法作为垃圾回收算法。

【可达对象】:通过根对象的进行引用搜索,最终可以到达的对象。

【不可达对象】:通过根对象进行引用搜索,最终没有被引用到的对象。

 

2)标记清除法

标记清除法是现代垃圾回收算法的思想基础。

标记清除法将垃圾回收分为两个阶段:标记阶段和清除阶段。

在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象,因此未被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有未被标记的对象。这种方法可以解决循环引用的问题,只有两个对象不可达,即使它们互相引用也无济于事。也是会被判定位不可达对象。

标记清除算法可能产生的最大的问题就是空间碎片

如下图所示,简单描述了使用标记清除法对一块连续的内存空间进行回收。

从根节点开始(在这里仅显示了两个根节点),所有的有引用关系的对象均被标记为存活对象(箭头表示引用)。从根节点起,不可达对象均为垃圾对象。在标记操作完成后,系统回收所有不可达对象。

 

 

从上图可以看出,回收后的内存空间不再连续。在对象的对空间分配过程中,尤其是大对象的内存分配,不连续内存空间的工作效率要低于连续空间的,这也是该算法的缺点。

注意:标记清除算法先通过根节点标记所有可达对象,然后清除所有不可达对象,完成垃圾回收。后面会讲到标记压缩算法,注意两者的区别。。。。。。

 

(3) 复制算法

算法思想:将原有的内存空间分为两块相同的存储空间,每次只使用一块,在垃圾回收时,将正在使用的内存块中存活对象复制到未使用的那一块内存空间中,之后清除正在使用的内存块中的所有对象,完成垃圾回收。

如果系统中的垃圾对象很多,复制算法需要复制的存活对象就会相对较少(适用场景)。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。而且,由于存活对象在垃圾回收过程中是一起被赋值到另一块内存空间中的,因此,可确保回收的内存空间是没有碎片的。(优点)

但是复制算法的代价是将系统内存空间折半,只使用一半空间,而且如果内存空间中垃圾对象少的话,复制对象也是很耗时的,因此,单纯的复制算法也是不可取的。(缺点)

 

图解算法回收流程:

A、B两块相同的内存空间(原有内存空间折半得到的两块相同大小内存空间AB),A在进行垃圾回收,将存活的对象复制到B中,B中的空间在复制后保持连续。完成复制后,清空A。并将空间B设置为当前使用内存空间。

 

 

在java中的新生代串行垃圾回收器中,使用了复制算法的思想,新生代分为eden空间、from空间和to空间3个部分,其中from和to空间可以看做用于复制的两块大小相同、可互换角色的内存空间块(同一时间只能有一个被当做当前内存空间使用,另一个在垃圾回收时才发挥作用),from和to空间也称为survivor空间,用于存放未被回收的对象。

新生代对象】:存放年轻对象的堆空间,年轻对象指刚刚创建,或者经历垃圾回收次数不多的对象。

老年代对象】:存放老年对象的堆空间。即为经历多次垃圾回收依然存活的对象。

 

     在垃圾回收时,eden空间中存活的对象会被复制到未使用的survivor空间中(图中的to),正在使用的survivor空间(图中的from)中的年轻对象也会被复制到to空间中(大对象或者老年对象会直接进入老年代,如果to空间已满,则对象也会进入老年代)。此时eden和from空间中剩余对象就是垃圾对象,直接清空,to空间则存放此次回收后存活下来的对象。

优点:这种复制算法保证了内存空间的连续性,又避免了大量的空间浪费。

注意:复制算法比较适用于新生代。因为在新生代中,垃圾对象通常会多于存活对象,算法的效果会比较好。

 

(4) 标记压缩算法

复制算法的高效性是建立在存活对象少、垃圾对象多的情况下,这种情况在新生代比较常见,

但是在老年代中,大部分对象都是存活的对象,如果还是有复制算法的话,成本会比较高。因此,基于老年代这种特性,应该使用其他的回收算法。

 

标记压缩算法是老年代的回收算法,它在标记清除算法的基础上做了优化。(回忆一下,标记清除算法的缺点,垃圾回收后内存空间不再连续,影响了内存空间的使用效率。。。)

和标记清除算法一样,标记压缩算法也首先从根节点开始,对所有可达的对象做一次标记,

但之后,它并不是简单的清理未标记的对象,而是将所有的存活对象压缩到内存空间的一端,之后,清理边界外所有的空间。

这样做避免的碎片的产生,又不需要两块相同的内存空间,因此性价比高。

 

图解其算法工作过程:

通过根节点标记出所有的可达对象后,沿着虚线进行对象的移动,将所有的可达对象移到一端,并保持他们之间的引用关系,最后,清理边界外的空间。

 

 

标记压缩算法的最终效果等同于标记清除算法执行完成后,再进行一次内存碎片的整理,因此也称之为标记清除压缩算法。

 

(5) 分代算法

前面介绍的垃圾回收算法中,并没有一种算法可以完全替代其他算法,各自具有自己的特点和优势,因此需要根据垃圾对象的特性选择合适的垃圾回收算法。

 

分代算法思想:将内存空间根据对象的特点不同进行划分,选择合适的垃圾回收算法,以提高垃圾回收的效率。

 

通常,java虚拟机会将所有的新建对象都放入称为新生代的内存空间。

新生代的特点是:对象朝生夕灭,大约90%的对象会很快回收,因此,新生代比较适合使用复制算法。

当一个对象经过几次垃圾回收后依然存活,对象就会放入老年代的内存空间,在老年代中,几乎所有的对象都是经过几次垃圾回收后依然得以存活的,因此,认为这些对象在一段时间内,甚至在程序的整个生命周期将是常驻内存的。

老年代的存活率是很高的,如果依然使用复制算法回收老年代,将需要复制大量的对象。这种做法是不可取的,根据分代的思想,对老年代的回收使用标记清除或者标记压缩算法可以提高垃圾回收效率。

 

注意:分代的思想被现有的虚拟机广泛使用,几乎所有的垃圾回收器都区分新生代和老年代。

 

对于新生代和老年代来说,通常新生代回收的频率很高,但是每次回收的时间都很短,而老年代回收的频率比较低,但是被消耗很多的时间。为了支持高频率的新生代回收,虚拟机可能使用一种叫做卡表的数据结构,卡表为一个比特位集合,每一个比特位可以用来表示老年代的某一区域中的所有对象是否持有新生代对象的引用,

这样以来,新生代GC时,可以不用花大量时间扫描所有老年代对象,来确定每一个对象的引用关系,而可以先扫描卡表,只有当卡表的标记为1时,才需要扫描给定区域的老年代对象,而卡表为0的所在区域的老年代对象,一定不含有新生代对象的引用。

 

如下图表示:

卡表中每一位表示老年代4KB的空间,卡表记录为0的老年代区域没有任何对象指向新生代,只有卡表为1的区域才有对象包含新生代对象的引用,因此在新生代GC时,只需要扫面卡表为1所在的老年代空间,使用这种方式,可以大大加快新生代的回收速度。

 

 

(6) 分区算法

算法思想:分区算法将整个堆空间划分为连续的不同小区间,

如图所示:

 

每一个小区间都独立使用,独立回收。

算法优点是:可以控制一次回收多少个小区间

通常,相同的条件下,堆空间越大,一次GC所需的时间就越长,从而产生的停顿时间就越长。为了更好的控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一个GC的停顿时间。

 

 

垃圾收集器:

如果说收集算法是内存回收具体的方法论,那么垃圾收集器就是内存回收的具体实现

 

      垃圾收集器是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法、火车算法)的具体实现,不同商家、不同版本的JVM所提供的垃圾收集器可能会有很在差别,本文主要介绍HotSpot虚拟机中的垃圾收集器。

1-1、垃圾收集器组合

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

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

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

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

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

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

      整堆收集器:G1;

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

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

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

1-2、并发垃圾收集和并行垃圾收集的区别

(A)、并行(Parallel)

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

       如ParNew、Parallel Scavenge、Parallel Old

(B)、并发(Concurrent)

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

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

       如CMS、G1(也有并行);

1-3、Minor GC和Full GC的区别

(A)、Minor GC

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

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

(B)、Full GC

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

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

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

        

下面将介绍这些收集器的特性、基本原理和使用场景,并重点分析CMS和G1这两款相对复杂的收集器;但需要明确一个观点:

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

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

2、Serial收集器

       Serial(串行)垃圾收集器是最基本、发展历史最悠久的收集器;

       JDK1.3.1前是HotSpot新生代收集的唯一选择;

1、特点

      针对新生代;

      采用复制算法;

      单线程收集;

       进行垃圾收集时,必须暂停所有工作线程,直到完成;            

       即会"Stop The World";

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

2、应用场景

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

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

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

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

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

3、设置参数

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

4、Stop TheWorld说明

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

      会带给用户不良的体验;

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

      更多"Stop The World"信息请参考:《Java虚拟机垃圾回收(一) 基础》"2-2、可达性分析算法"

更多Serial收集器请参考:

      《Memory Management in the Java HotSpot™ Virtual Machine》 4.3节 Serial Collector(内存管理白皮书):http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

      《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》 第5节 Available Collectors(官方的垃圾收集调优指南):http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref27

3、ParNew收集器

      ParNew垃圾收集器是Serial收集器的多线程版本

1、特点

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

      如Serial收集器可用控制参数、收集算法、Stop The World、内存分配规则、回收策略等;

      两个收集器共用了不少代码;

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

2、应用场景

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

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

3、设置参数

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

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

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

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

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

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

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

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

4、Parallel Scavenge收集器

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

1、特点

(A)、有一些特点与ParNew收集器相似

      新生代收集器;

      采用复制算法;

      多线程收集;

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

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

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

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

2、应用场景

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

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

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

3、设置参数

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

(A)、"-XX:MaxGCPauseMillis"

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

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

      因为可能导致垃圾收集发生得更频繁;

(B)、"-XX:GCTimeRatio"

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

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

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

      1 / (1 + n)

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

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

垃圾收集所花费的时间是年轻一代和老年代收集的总时间;

如果没有满足吞吐量目标,则增加代的内存大小以尽量增加用户程序运行的时间;

      此外,还有一个值得关注的参数:

(C)、"-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收集器一个重要区别;    

      更多目标调优和GC自适应的调节策略说明请参考:            

      《Memory Management in the Java HotSpot™ Virtual Machine》 5节 Ergonomics -- Automatic Selections and Behavior Tuning:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

      《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》 第2节 Ergonomics:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html#ergonomics

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

(A)、吞吐量(Throughput)

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

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

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

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

(1)、停顿时间    

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

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

(2)、吞吐量

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

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

(3)、覆盖区(Footprint)

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

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

更多Parallel Scavenge收集器的信息请参考:

      官方的垃圾收集调优指南 第6节:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/parallel.html#parallel_collector

 

上面介绍的都是新生代收集器,接下来开始介绍老年代收集器;

5、Serial Old收集器

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

1、特点

      针对老年代;

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

      单线程收集;

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

2、应用场景

      主要用于Client模式;

      而在Server模式有两大用途:

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

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

更多Serial Old收集器信息请参考:

      内存管理白皮书 4.3.2节:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

6、Parallel Old收集器

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

      JDK1.6中才开始提供;

1、特点

      针对老年代;

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

      多线程收集;

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

2、应用场景

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

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

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

3、设置参数

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

更多Parallel Old收集器收集过程介绍请参考:

      《内存管理白皮书》 4.5.2节:        http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

7、CMS收集器

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

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

1、特点

      针对老年代;

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

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

      并发收集、低停顿;

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

            

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

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

2、应用场景

      与用户交互较多的场景;        

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

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

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

3、设置参数

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

4、CMS收集器运作过程

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

(A)、初始标记(CMS initial mark)

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

      速度很快;

      但需要"Stop The World";

(B)、并发标记(CMS concurrent mark)

      进行GC Roots Tracing的过程;

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

      应用程序也在运行;

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

(C)、重新标记(CMS remark)

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

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

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

(D)、并发清除(CMS concurrent sweep)

      回收所有的垃圾对象;

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

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

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

 

        5、CMS收集器3个明显的缺点

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

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

      CMS的默认收集线程数量是=(CPU数量+3)/4;

      当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

 

      增量式并发收集器:

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

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

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

更多请参考:

      官方的《垃圾收集调优指南》8.8节 Incremental Mode:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#CJAGIIEJ

      《内存管理白皮书》 4.6.3节可以看到一些描述;

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

(1)、浮动垃圾(Floating Garbage)

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

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

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

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

      JDK1.5默认值为68%;

      JDK1.6变为大约92%;               

(2)、"Concurrent Mode Failure"失败

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

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

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

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

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

      前面《Java虚拟机垃圾回收(二) 垃圾回收算法》"标记-清除"算法介绍时曾说过:

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

      解决方法:                

(1)、"-XX:+UseCMSCompactAtFullCollection"

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

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

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

(2)、"-XX:+CMSFullGCsBeforeCompaction"

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

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

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

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

      更多关于内存分配方式请参考:《Java对象在Java虚拟机中的创建过程

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

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

更多CMS收集器信息请参考:

      《垃圾收集调优指南》 8节 Concurrent Mark Sweep (CMS) Collector:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector

      《内存管理白皮书》 4.6节 Concurrent Mark-Sweep (CMS) Collector:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

8、G1收集器

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

1、特点

(A)、并行与并发

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

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

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

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

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

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

                

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

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

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

      更多G1内存布局信息请参考:

      《垃圾收集调优指南》 9节:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection

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

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

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

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

 

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

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

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

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

2、应用场景

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

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

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

            

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

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

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

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

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

      是否一定采用G1呢?也未必:

      如果现在采用的收集器没有出现问题,不用急着去选择G1;

      如果应用程序追求低停顿,可以尝试选择G1;

      是否代替CMS需要实际场景测试才知道。

3、设置参数

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

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

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

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

      更多关于G1参数设置请参考:

      《垃圾收集调优指南》 10.5节:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#important_defaults

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

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

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

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

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

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

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

      一个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;

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

6、G1收集器运作过程

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

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

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

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

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

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

      进行GC Roots Tracing的过程;

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

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

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

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

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

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

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

                    

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

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

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

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

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

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

                    

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

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

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

更多G1收集器信息请参考:

      《垃圾收集调优指南》 9节 Garbage-First Garbage Collector:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection

      《垃圾收集调优指南》 10节 Garbage-First Garbage Collector Tuning:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#g1_gc_tuning

        

      到这里,我们大体了解HotSpot虚拟机中的所有垃圾收集器,后面我们将去了解JVM的一些内存分配与回收策略、JVM垃圾收集相关调优方法……

 

【参考资料】

1、《编译原理》第二版 第7章

2、《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版 第3章

3、《The Java Virtual Machine Specification》Java SE 8 Edition:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

4、《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html

5、《Memory Management in the Java HotSpot™ Virtual Machine》:http://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

6、HotSpot虚拟机参数官方说明:http://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

7、《Thinking in Java》第四版 5.5 清理:终结处理和垃圾回收;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值