JVM--GC

一、垃圾回收

  垃圾回收一般发生在堆或方法区中,也就是线程共享的部分,堆和方法区的内存分配和垃圾回收都是通过垃圾回收器去实现的。

  不同的垃圾回收器对应不同的垃圾回收算法。

(一)判断算法

  1、判断哪些对象需要回收

    判断哪些对象需要回收主要有引用计数法和根搜索算法。

    引用计数法:给对象添加一个引用计数器,每一次被引用该计数器就会加一,当引用失效时,该计数器就会减一,当计数器的值为0时,就可以进行垃圾回收。其优点就是实现简单、判断效率高;缺点是很难解决循环引用的问题,解决不了循环依赖,就会产生内存溢出,这也是java语言没有使用该算法的原因。

    根搜索算法:也叫做可达性算法,通过GCRoot作为根节点开始向下搜索,搜索走过的路程称为引用链,当一个对象到GCRoot没有任何一条引用链时,则证明该对象没有被使用。

    可以作为GCRoot的对象:

      虚拟机栈中,栈帧的本地变量表引用的对象。

      方法区中,类静态属性引用的对象,常量引用的对象。

      本地方法栈中,JNl引用的对象。总结就是堆中的对象不能作为GCRoot。

    一般来说GC扫描跟对象集合的时候是非常快的,因为当前所有活动的线程数是固定而有限的,所有活动的线程正在执行的方法数量也是固定的,所有类的静态字段和JNI引用总的来说相对数量比较少,所以说在扫描GC Root时要做的GC暂停的时间非常短,且和整个堆的大小、堆中对象的数量没有直接关系,而是由当前存活对象的数量来决定的。

  2、垃圾回收过程

    可达性分析后如果对象是不可达的,也并非直接就进行回收,确定回收需要两次标记:

      第一次标记:如果对象可达性分析后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记;

      第二次标记:第一次标记后,接着会进行一次筛选。筛选条件:此对象是否有必要执行 finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

    第二次标记成功的对象将真的会被回收,如果失败则继续存活

  3、方法区回收

    方法去也是有垃圾回收的,主要回收废弃的常量和无用的类,由于回收性价比极低,因此就算满足回收条件也不一定会回收。

    废弃常量:例如字符串常量池,没有对象引用即可回收,同时常量池中类、方法、字段的符号引用也与此类似。

    无用的类:需要满足三个条件:(1)该类的所有实例都已经被收回;(2)该类的ClassLoader已经被收回;(3)该类对应的Class对象不存在任何的引用,不能通过反射产生该类的实例对象

  4、对象引用

    在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(StrongReference)、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference)四种,这四种引用强度依次逐渐减弱。

        

 

 

     (1)强引用

      代码中普遍存在,只要强引用还在,就不会被GC。 

Object obj = new Object();

    (2)软引用

      非必须引用,内存溢出之前进行回收,如内存还不够,才会抛异常。 

Object obj = new Object(); 
SoftReference<Object> sf = new SoftReference<Object>(obj); 
obj = null; Object o = sf.get();//有时候会返回null System.out.println("o = " + o);

    (3)弱引用

      非必须引用,只要有GC,就会被回收。 

      弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用的作用:监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。

Object obj = new Object(); 
WeakReference<Object> wf = new WeakReference<Object>(obj); 
obj = null; 
//System.gc(); 
Object o = wf.get(); //有时候会返回null 
boolean enqueued = wf.isEnqueued(); //返回是否被垃圾回收器标记为即将回收的垃圾 
System.out.println("o = " + o); 
System.out.println("enqueued = " + enqueued);

    (4)虚引用

      虚引用是最弱的一种引用关系。垃圾回收时直接回收

      一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。 

      虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。

      虚引用的作用:跟踪对象被垃圾回收的状态,仅仅是提供一种确保对象被回收后,做某些事情的机制。类似事件监听机制

Object obj = new Object(); 
PhantomReference<Object> pf = new PhantomReference<Object>(obj, new ReferenceQueue<>()); 
obj=null; 
Object o = pf.get();//永远返回null 
boolean enqueued = pf.isEnqueued();//返回是否从内存中已经删除 
System.out.println("o = " + o); System.out.println("enqueued = " + enqueued);    

(二)回收算法

  垃圾回收算法有标记清除算法、复制回收算法、标记整理算法、分代回收算法等。

  1、标记清除算法

    标记清除算法是最基本的算法,分为标记和清楚两个阶段,首先标记出需要GC Root可达的对象,在标记完成后,统一回收掉被没有被标记的对象。

    缺点是效率低(标记和清除效率都不高)及存在内存碎片(会产生大量不连续的内存碎片,导致大对象不能存储,提前触发GC)。

  2、复制回收算法

    为了提高效率,其将内存分为两块大小相等的区域,每次只使用其中的一块,当这一块的内存用完了,就将还存活的对象复制到另外一块上,然后再把已使用的内存清除掉。

    现代商业虚拟机都是使用复制回收算法来对新生代进行垃圾回收的,每次回收时会将Enden区和其中一个S区中存货的对象复制到另一个S区,然后清空Ende区和该S区,在Hotspot中,Eden区和S区的比例为8:2,S0:S1为1:1,因此在新生代中使用的:未使用的比例为9:1,浪费了十分之一的内存,但是却大大提高了效率。

    当S区内存不够使用时,则会依赖老年代进行内存担保。

    优点是效率高、没有磁盘碎片,缺点是有空间浪费。

  3、标记整理算法

    标记过程仍然使用标记清除算法,在清除完毕后,将还存活的对象都向一端移动,然后清理掉边界以外的内存。

    老年代没有区域对其进行内存担保,因此其使用标记整理算法。

    缺点:性能较低,因为除了拷贝对象以外,还需要对象内存空间进行压缩,所以性能较低。优点是没有空间碎片。

  4、分代回收算法

    目前现在的商用虚拟机都是采用这种算法,跟队对象的存活周期将内存划分为几个不同的区域,然后根据每个区域的特点进行不同的垃圾回收。

    新生代:由于每次回收都有大量对象被回收,因此使用复制回收算法。

    老年代:对象存活率高,无其他区域对其进行内存担保,就必须采用标记清除和标记整理算法。

(三)内存担保机制

  内存担保是在内存分配时,新生代内存不足时,把新生代存活的对象搬到老年代,然后将腾出来的内存用于存放最新的对象。

  在不同的GC机制下,担保机制也略有不同,在Serial+Serial Old组合下,发现存不下就直接启动内存担保,而在Parallel scavenge+Serial Old组合下, 先要看下要分配的内存是都大于Eden区的一半,如果大于,就直接把对象放在老年代,如果不大于,才会开启内存担保。

(四)GC方式

  GC方式分为minorGC、majorGC和fullGC三种。

  minorGC:新生代的垃圾回收,很快就回收了,使用的是一些垃圾回收比较快的算法,例如复制回收算法。

  majorGC:老年代的垃圾回收,比minorGC慢10倍,因此要尽量避免majorGC。majorGC不是fullGC,majorGC只针对堆,而fuuGC不但针对堆,还针对方法区。

  fullGC:整个堆和方法区的垃圾回收;老年代不够用,没有其他内存区域对其进行内存担保,会触发fullGC;方法区不够用,没有其他内存区域对其进行担保,会触发fullGC;当新生代无法被老年代成功担保时,也会发生fullGC

二、垃圾收集器

(一)垃圾回收器概述

  有 8 种不同的垃圾回收器,它们分别用于不同分代的垃圾回收。

    新生代回收器:Serial、ParNew、Parallel Scavenge

    老年代回收器:Serial Old、Parallel Old、CMS

    整堆回收器:G1、ZGC

  两个垃圾回收器之间有连线表示它们可以搭配使用,可选的搭配方案如下: 

        

  为了达到最大性能,基于分代管理和回收算法,并结合回收的时机,JVM实现的垃圾回收器有:串行回收、并行回收、并发标记回收(CMS)和垃圾优先回收(G1)。

(二)串行收集器

  基本描述:

    串行收集器:使用单线程进行垃圾回收的收集器,每次回收时,串行收集器只有一个工作线程,对于并行能力较弱的计算机来说,串行收集器性能会更好。

    串行收集器可以在新生代和老年代中使用,根据作用于不同的堆空间,分为新生代串行收集器和老年代收集器。

    配置参数 -XX:+UseSerialGC :年轻串行(Serial),老年串行(Serial Old)

  1、Serial收集器:年轻串行

    收集区域:新生代

    收集方式:串行

    收集算法:复制算法

    串行收集器的组合:Serial+SerialOld

    优点:简单有效;对于单CPU环境,Serial不需要线程切换的开销,可以获得高效的收集效率;在应用桌面的程序中,可用内存一般不大,可以在短时间内完成垃圾收集。

    应用场景:主要用于client模式、单CPU、桌面应用程序。

    参数配置:-XX:UseSerialGC

        

 

 

   2、Serial Old收集器:老年串行

    Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。  

    收集区域:老年代

    收集方式:串行

    收集算法:标记整理算法

    应用场景:主要用于client模式

  什么是Safe point?(GC:保洁阿姨打扫、应用程序:人弄乱屋子、如果弄乱速度 > 打扫速度怎么办?STW、在哪里STW?沙发上,床上,桌子上还是砧板上)

    Safepoint挂起线程的点主要有:循环的末尾、方法返回前、调用方法的call之后、抛出异常的位置

(三)并行回收

  1、Paralell Scavenge收集器(新生代)

    配置参数: -XX:+UseParallelGC,目标是达到一个可控制的吞吐量(Throughput)。

    收集区域:新生代

    收集算法:复制算法

    因为与吞吐量关系密切,也被称为吞吐量收集器,并行收集器的组合:Paralell Scavenge + Serial Old,并行收集器是以关注吞吐量为目标的垃圾收集器,也是server模式下的默认收集器配置。

    特点:吞吐量优先收集器、新生代使用并行回收收集器,采用复制算法,老年代使用串行收集器 

    应用场景:提高吞吐量为目标,减少垃圾收集时间,让用户代码获得更长的执行时间。适用于多CPU,对于停顿时间没有特别要求的后台任务。

        

  2、Paralell Old收集器(老年代)

    配置参数: -XX:+UseParallelOldGC

 

    收集区域:老年代

    收集方式:并行方式

    回收算法:标记整理

    收集组合:Paralell Scavenge + Paralell Old,Paralell Old是Paralell Scavenge的老年代版本,

    应用场景:在注重吞吐量,CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。 

        

  3、ParNew收集器(新生代)

    配置参数: -XX:+UseParNewGC

    配置参数: -XX:ParallelGCThreads=n 设置并行收集器收集时使用的并行收集线程数。一般最好和计算机的CPU相当

    收集区域:新生代

    收集方式:并行方式

    收集算法:复制算法

    特点:新生代并行(ParNew),老年代串行(Serial Old) ,Serial收集器的多线程版本,单CPU性能并不如Serial,因为存在线程交互的开销

    应用场景:在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有他能与CMS收集器配合工作;ParNew是Serial收集器的多线程版本。

        

 

 

 

  4、CMS收集器(老年代)

    配置参数: -XX:+UseConcMarkSweepGC 应用CMS收集器。 

    收集区域:老年代

    收集方式:并行方式

    收集算法:标记清除

      优点:并发收集、低停顿

  5、G1收集器

    收集区域:整个堆

    收集方式:并行方式

    收集算法:标记清楚和标记整理。G1收集器并没有在物理内存中使用分代划分,其在物理内存中划分了2048个分区(Region,可以通过-XX:G1HeapRegionSize进行设置),同时其还保留了分代的逻辑。

    JDK7和JDK8默认使用ParalellGC进行垃圾回收,JDK9默认使用G1收集器,如果在JDK7和JDK8开启G1收集器,使用:-XX:+UseG1GC

    G1不会等到内存耗尽或者快要耗尽时才开始进行垃圾回收,而是在内部采用了启发式算法,在老年代找出具有高收集效益的区进行收集。

    G1里面有一个card卡片的概念。G1将堆内存划分为多个内存大小为512Byte的区域,一个区域称为一个card,分配对象会占用物理上连续的几个card;CardTable是一个字节数组,维护者所有的card,card中对象的引用发生变更时,card在cardTable中的值就会被标记为dirty,就称这个card被脏化了。

      G1收集器并没有在物理内存中使用分代划分,其在物理内存中划分了2048个分区(Region,可以通过-XX:G1HeapRegionSize进行设置),同时其还保留了分代的逻辑,但是年轻代和老年代不再是物理上的隔离,他们是一部分Region的集合,这些Region可以是不连续的,每个Region可能随着G1的运行在不同代之间切换。 这些Region分区被分为Eden Region、Survivor Region、Old Region、Humongous Region(巨型分区,当一个对象的大小大于一个Region大小的50%时,他就会独占一个或多个Region,巨型对象会直接分配在老年代,该对象所占用的连续分区被称为巨型分区)。 

    G1的年轻代并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲Region加入到年轻代空间,整个年轻代的空间会在初始空间(-XX:G1NewSizePercent,默认为堆空间大小的5%)和最大空间(-XX:G1MaxNewSizePercent,默认为堆空间的60%)之间动态变化,且有参数目标暂停时间(-XX:MaxGCpauselMillis,默认为200毫秒)、需要扩容的大小及分区的已记忆集合计算得到,当然,G1可以直接设置年轻代的大小(-XX:NewRatio、-XX:Xmn),如果直接设置年轻代大小,则暂停目标将失去意义。

    G1 Young GC

    Young GC主要对Eden区进行垃圾回收,其会在Eden区空间耗尽的情况下触发,这种情况下,Eden空间存活的对象就会被移动到Survivor空间中,如果Survivo空间的空间不足,则这些对象就会被直接晋升到老年代;Survivor空间的对象会被移动到新的Survivor中,也有部分会晋升到老年代,最终Eden空间的数据为空,GC停止工作,应用程序继续执行。

    G1 Mix GC

    Mix GC不仅进行正常的新生代垃圾回收,同时也回收部分后台扫描线程标记的老年代分区。

    可以使用以下三种方式的一种进行配置并行回收,这三个配置是等价的:(1)-XX:+UseParallelGC  (2)-XX:+UseParallelOldGC  (3)-XX:+UseParallelGC -XX:+UseParallelOldGC

    年轻代和老年代的垃圾回收都会触发 STW 事件。在年轻代使用 标记-复制(mark-copy)算法,在老年代使用 标记-清除-整理(mark-sweepcompact)算法。-XX:ParallelGCThreads=N 来指定 GC 线程数, 其默认值为 CPU 核心数。并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量。因为对系统资源的有效使用,能达到更高的吞吐量。

    在JDK 1.6 ~ 1.8 中,默认的垃圾回收器都是并行GC

    其实并行回收还是和串行回收一样,在标记和回收两个阶段都需要STW,区别是串行的垃圾回收无论CPU几核,都是由一个线程进行标记和清理,而并行回收是可以让所有的CPU内核都参与标记和回收,因此标记和回收会更快,STW会更短,另外在多核CPU中,两次并行收集之间,没有GC线程在运行,不会消耗任何的系统资源。

    使用并行GC运行程序:

java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseParallelGC -jar gateway-server-0.0.1-SNAPSHOT.jar

    分析堆栈信息:

# jhsdb jmap --heap --pid 16337
Attaching to process ID 16337, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.2+9-LTS

using thread-local object allocation.
Parallel GC with 13 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 357564416 (341.0MB)
   MaxNewSize               = 357564416 (341.0MB)
   OldSize                  = 716177408 (683.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 268435456 (256.0MB)
   used     = 17662936 (16.844688415527344MB)
   free     = 250772520 (239.15531158447266MB)
   6.579956412315369% used
From Space:
   capacity = 44564480 (42.5MB)
   used     = 9766432 (9.313995361328125MB)
   free     = 34798048 (33.186004638671875MB)
   21.915283203125% used
To Space:
   capacity = 44564480 (42.5MB)
   used     = 0 (0.0MB)
   free     = 44564480 (42.5MB)
   0.0% used
PS Old Generation
   capacity = 716177408 (683.0MB)
   used     = 10667488 (10.173309326171875MB)
   free     = 705509920 (672.8266906738281MB)
   1.489503561664989% used

  3、并发标记回收

    并发标记回收(CMS)的整个回收期划分为多个阶段:初始标记、并发标记、重新标记、并发清除等。

    在初始标记和重新标记阶段需要SWT,在并发标记和并发清除阶段可以和应用程序一起执行,这个算法通常用于老年代,新生代可以使用并发回收。

  4、垃圾优先回收

    连续的内存将导致垃圾回收时间过长,停顿时间不可控,因此G1将堆拆分为一系列的分区,这样在一段时间内,大部分的垃圾收集器都只针对一部分分区,而不是整个堆或整个老年代。

三、详解CMS

  CMS GC全称:Mostly Concurrent Mark and Sweep Garbage Collector,中文可以解释为最大可能性的并发标记清除垃圾回收算法。可以使用下面参数开启:-XX:+UseConcMarkSweepGC。其对年轻代采用并行 STW 方式的 mark-copy (标记-复制)算法,对老年代主要使用并发 mark-sweep (标记-清除)算法。

  CMS GC 的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,主要通过两种手段来达成此目标:

    1、不对老年代进行整理,而是使用空闲列表(free-lists)来管理内存空间的回收。

    2、在 mark-and-sweep (标记-清除) 阶段的大部分工作和应用线程一起并发执行。也就是说,在这些阶段并没有明显的应用线程暂停。但值得注意的是,它仍然和应用线程争抢CPU 时。

  默认情况下,CMS 使用的并发线程数等于 CPU 核心数的 1/4。如果服务器是多核 CPU,并且主要调优目标是降低 GC 停顿导致的系统延迟,那么使用 CMS 是个很明智的选择。进行老年代的并发回收时,可能会伴随着多次年轻代的 minor GC。

  那么CMS和Paralell最大的区别是Paralell在垃圾回收时使用所有的CPU内核进行回收,且都要STW,并且回收后会对空间进行整理(压缩),而CMS进行垃圾回收时使用1/4的CPU内核进行回收,在大多时候可以和业务线程并发执行,且垃圾回收后不会对空间进行整理,而是使用空闲列表记录可用空间,这些区别带来的结果就是并行垃圾回收器的吞吐量更好,而CMS的业务停顿时间更短。

        

  CMS垃圾回收有六个阶段:

    阶段 1: Initial Mark(初始标记)。这个阶段伴随着 STW 暂停。初始标记主要做三件事,标记所有的根对象(GC Root),标记根对象直接引用的对象,标记被年轻代中所有存活对象所引用的对象(老年代单独回收),在JVM内部维护了一个Remember Set,简称RSet,专门来记录跨代的引用。

    阶段 2: Concurrent Mark(并发标记)。在此阶段,CMS GC 遍历老年代,标记所有的存活对象,从前一阶段 “Initial Mark” 找到的根对象开始算起。 “并发标记”阶段,就是与应用程序同时运行,不用暂停的阶段。

    阶段 3: Concurrent Preclean(并发预清理)。此阶段同样是与应用线程并发执行的,不需要停止应用线程。 因为前一阶段【并发标记】与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化,JVM 会通过“Card(卡片)”的方式将发生了改变的区域标记为“脏”区,这就是所谓的 卡片标记(Card Marking)。

    阶段 4: Final Remark(最终标记)。最终标记阶段是此次 GC 事件中的第二次(也是最后一次)STW停顿。本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能 GC 线程跟不上应用程序的修改速度。所以需要一次 STW 暂停来处理各种复杂的情况。通常 CMS 会尝试在年轻代尽可能空的情况下执行 Final Remark阶段,以免连续触发多次 STW 事件。

    阶段 5: Concurrent Sweep(并发清除)。此阶段与应用程序并发执行,不需要 STW 停顿。JVM 在此阶段删除不再使用的对象,并回收他们占用的内存空间。

    阶段 6: Concurrent Reset(并发重置)。CMS 垃圾收集器在减少停顿时间上做了很多复杂而有用的工作,用于垃圾回收的并发线程执行的同时,并不需要暂停应用线程。 当然,CMS 也有一些缺点,其中最大的问题就是老年代内存碎片问题(因为不压缩),在某些情况下 GC会造成不可预测的暂停时间,特别是堆内存较大的情况下。此阶段与应用程序并发执行,重置 CMS 算法相关的内部数据,为下一次 GC 循环做准备。

  使用CMS运行程序:其中 -UseAdaptiveSizePolicy表示关闭自适应参数。

java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -jar gateway-server-0.0.1-SNAPSHOT.jar

  打印的堆栈信息:可以看到打印了jdk版本、使用的垃圾回收器,堆占用信息,以及各个分代中的详细信息。例如在堆占用信息中,描述了最小空闲堆的比例、最大空闲堆的比例、最大堆空间、年轻代大小、最大年轻代大小、老年代大小、新生代和老年代比例,Eden区和S区的比例、元空间大小等等,在具体分代信息中,描述了总内存、已使用内存和空闲内存。

  另外一个需要注意的点,由于下面的样例使用的是JDK11打印的堆栈信息,年轻代的大小为341M,而如果使用JDK8,则会是332M,这是因为在JDK8的实现中是使用64M乘以GC线程数(我电脑上默认为4)再乘以一个固定的数(13/10)而得来的,即332M,这就说明,年轻代分配的内存大小,和GC线程数有关,因此在执行参数中调整并行GC的线程数,就可以调整年轻代的大小。

# jhsdb jmap --heap --pid 16826
Attaching to process ID 16826, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.2+9-LTS

using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 357892096 (341.3125MB)
   MaxNewSize               = 357892096 (341.3125MB)
   OldSize                  = 715849728 (682.6875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 322109440 (307.1875MB)
   used     = 196597928 (187.49039459228516MB)
   free     = 125511512 (119.69710540771484MB)
   61.034512990367496% used
Eden Space:
   capacity = 286326784 (273.0625MB)
   used     = 174855048 (166.75476837158203MB)
   free     = 111471736 (106.30773162841797MB)
   61.068351887052245% used
From Space:
   capacity = 35782656 (34.125MB)
   used     = 21742880 (20.735626220703125MB)
   free     = 14039776 (13.389373779296875MB)
   60.76373984088828% used
To Space:
   capacity = 35782656 (34.125MB)
   used     = 0 (0.0MB)
   free     = 35782656 (34.125MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 715849728 (682.6875MB)
   used     = 0 (0.0MB)
   free     = 715849728 (682.6875MB)
   0.0% used

四、详解G1

(一)G1 特点

  G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。G1 GC 最主要的设计目标是:将 STW 停顿的时间和分布,变成可预期且可配置的。事实上,G1 GC 是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。为了达成可预期停顿时间的指标,G1 GC 有一些独特的实现。

  首先,堆不再分成年轻代和老年代,而是划分为多个(通常是2048个)可以存放对象的小块堆区域(smaller heap regions)。每个小块,可能一会被定义成 Eden 区,一会被指定为 Survivor区或者Old 区。在逻辑上,所有的 Eden 区和 Survivor 区合起来就是年轻代,所有的 Old 区拼在一起那就是老年代。

  参数配置:-XX:+UseG1GC -XX:MaxGCPauseMillis=50 含义:使用G1GC,停顿时间50ms

  这样划分之后,使得 G1 不必每次都去收集整个堆空间,而是以增量的方式来进行处理: 每次只处理一部分内存块,称为此次 GC 的回收集(collection set)。每次 GC 暂停都会收集所有年轻代的内存块,但一般只包含部分老年代的内存块。G1 的另一项创新是,在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是: 垃圾最多的小块会被优先收集。这也是 G1 名称的由来。

  G1最大堆内存是 32MB*2048=64G ,G1最小堆内存 1MB*2048=2GB ,低于此值建议使用其它收集器。 

  特点:

    1. 并行与并发:充分利用多核环境下的硬件优势

    2. 多代收集:不需要其他收集器配合就能独立管理整个GC堆

    3. 空间整合:“标记-整理”算法实现的收集器,局部上基于“复制”算法不会产生内存空间碎片

    4. 可预测的停顿:能让使用者明确指定消耗在垃圾收集上的时间。当然,更短的GC时间的代价是回收空间的效率降低。

(二)G1参数配置:

    -XX:+UseG1GC:启用 G1 GC;

    -XX:G1NewSizePercent:初始年轻代占整个 Java Heap 的大小,默认值为 5%;

    -XX:G1MaxNewSizePercent:最大年轻代占整个 Java Heap 的大小,默认值为 60%;

    -XX:G1HeapRegionSize:设置每个 Region 的大小,单位 MB,需要为 1、2、4、8、16、32 中的某个值,默认是堆内存的1/2000。如果这个值设置比较大,那么大对象就可以进入 Region 了;

    -XX:ConcGCThreads:与 Java 应用一起执行的 GC 线程数量,默认是 Java 线程的 1/4,减少这个参数的数值可能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回收机制耗时加长;

    -XX:+InitiatingHeapOccupancyPercent(简称 IHOP):G1 内部并行回收循环启动的阈值,默认为 Java Heap的 45%。这个可以理解为老年代使用大于等于 45% 的时候,JVM 会启动垃圾回收。这个值非常重要,它决定了在什么时间启动老年代的并行回收;

    -XX:G1HeapWastePercent:G1停止回收的最小内存大小,默认是堆大小的 5%。GC 会收集所有的 Region 中的对象,但是如果下降到了 5%,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以遗留少量的下次处理,这样也降低了单次消耗的时间;

    -XX:G1MixedGCCountTarget:设置并行循环之后需要有多少个混合 GC 启动,默认值是 8 个。老年代 Regions的回收时间通常比年轻代的收集时间要长一些。所以如果混合收集器比较多,可以允许 G1 延长老年代的收集时间。

    -XX:+G1PrintRegionLivenessInfo:这个参数需要和 -XX:+UnlockDiagnosticVMOptions 配合启动,打印 JVM 的调试信息,每个Region 里的对象存活信息。

    -XX:G1ReservePercent:G1 为了保留一些空间用于年代之间的提升,默认值是堆空间的 10%。因为大量执行回收的地方在年轻代(存活时间较短),所以如果你的应用里面有比较大的堆内存空间、比较多的大对象存活,这里需要保留一些内存。

    -XX:+G1SummarizeRSetStats:这也是一个 VM 的调试信息。如果启用,会在 VM 退出的时候打印出 Rsets 的详细总结信息。如果启用 -XX:G1SummaryRSetStatsPeriod 参数,就会阶段性地打印 Rsets 信息。

    -XX:+G1TraceConcRefinement:这个也是一个 VM 的调试信息,如果启用,并行回收阶段的日志就会被详细打印出来。

    -XX:+GCTimeRatio:这个参数就是计算花在 Java 应用线程上和花在 GC 线程上的时间比率,默认是 9,跟新生代内存的分配比例一致。这个参数主要的目的是让用户可以控制花在应用上的时间,G1 的计算公式是 100/(1+GCTimeRatio)。这样如果参数设置为9,则最多 10% 的时间会花在 GC 工作上面。Parallel GC 的默认值是 99,表示 1% 的时间被用在 GC 上面,这是因为 Parallel GC 贯穿整个 GC,而 G1 则根据 Region 来进行划分,不需要全局性扫描整个内存堆。

    -XX:+UseStringDeduplication:手动开启 Java String 对象的去重工作,这个是 JDK8u20 版本之后新增的参数,主要用于相同String 避免重复申请内存,节约 Region 的使用。

    -XX:MaxGCPauseMills:预期 G1 每次执行 GC 操作的暂停时间,单位是毫秒,默认值是 200 毫秒,G1 会尽量保证控制在这个范围内。

(三)G1 GC 的处理步骤

        

    G1收集器的运作大致可划分为以下几个步骤:

      1. 初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短

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

      3. 最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录

      4. 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划

    G1中有三种模式垃圾回收模式,Young GC、Mixed GC 和 Full GC,在不同的条件下被触发。

    1、年轻代模式转移暂停(Evacuation Pause)

      G1 GC 会通过前面一段时间的运行情况来不断的调整自己的回收策略和行为,以此来比较稳定地控制暂停时间。在应用程序刚启动时,G1 还没有采集到什么足够的信息,这时候就处于初始的 fully-young模式。当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。如果还没有存活区,则任意选择一部分空闲的内存块作为存活区。拷贝的过程称为转移(Evacuation),这和前面介绍的其他年轻代收集器是一样的工作原理。

    2、并发标记(Concurrent Marking)

      同时我们也可以看到,G1 GC 的很多概念建立在 CMS 的基础上,所以下面的内容需要对 CMS 有一定的理解。G1 并发标记的过程与 CMS 基本上是一样的。G1 的并发标记通过 Snapshot-At-The-Beginning(起始快照)的方式,在标记阶段开始时记下所有的存活对象。即使在标记的同时又有一些变成了垃圾。通过对象的存活信息,可以构建出每个小堆块的存活状态,以便回收集能高效地进行选择。这些信息在接下来的阶段会用来执行老年代区域的垃圾收集。有两种情况是可以完全并发执行的:一、如果在标记阶段确定某个小堆块中没有存活对象,只包含垃圾;二、在 STW 转移暂停期间,同时包含垃圾和存活对象的老年代小堆块。当堆内存的总体使用比例达到一定数值,就会触发并发标记。这个默认比例是 45%,但也可以通过 JVM参数InitiatingHeapOccupancyPercent 来设置。和 CMS 一样,G1 的并发标记也是由多个阶段组成,其中一些阶段是完全并发的,还有一些阶段则会暂停应用线程。

      阶段 1: Initial Mark(初始标记)。此阶段标记所有从 GC 根对象直接可达的对象。

      阶段 2: Root Region Scan(Root区扫描)。此阶段标记所有从 "根区域" 可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。

      阶段 3: Concurrent Mark(并发标记)。此阶段和 CMS 的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。

      阶段 4: Remark(再次标记)。和 CMS 类似,这是一次 STW 停顿(因为不是并发的阶段),以完成标记过程。 G1 收集器会短暂地停止应用线程,停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标记开始时未被标记的存活对象。

      阶段 5: Cleanup(清理)。最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC的效率,维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的:例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的 STW 暂停。

    3、转移暂停: 混合模式(Evacuation Pause (mixed))

      并发标记完成之后,G1将执行一次混合收集(mixed collection),就是不只清理年轻代,还将一部分老年代区域也加入到回收集中。混合模式的转移暂停不一定紧跟并发标记阶段。有很多规则和历史数据会影响混合模式的启动时机。比如,假若在老年代中可以并发地腾出很多的小堆块,就没有必要启动混合模式。因此,在并发标记与混合转移暂停之间,很可能会存在多次 young 模式的转移暂停。具体添加到回收集的老年代小堆块的大小及顺序,也是基于许多规则来判定的。其中包括指定的软实时性能指标,存活性,以及在并发标记期间收集的 GC 效率等数据,外加一些可配置的 JVM 选项。混合收集的过程,很大程度上和前面的 fully-young gc 是一样的。

(四)G1内存划分

  G1垃圾收集器相对比其他收集器而言,最大的区别在于它取消了新生代、老年代的物理划分,取而代之的是将堆划分为若干个区域(Region),这些区域中包含了有逻辑上的新生代、老年代区域。

  好处:不用单独的空间对每个代进行设置,不用考虑每个代内存如何分配。 

        

 

 

  局部采用复制算法:

    G1新生代垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间

    G1通过将对象从一个区域复制到另外一个区域,完成了清理工作。 相当于在正常的处理过程中,G1完成了堆的压缩,这样就不会有cms内存碎片问题了。

  Humongous区域:在G1中,有一种特殊的区域叫Humongous区域

    如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。 这些巨型对象,默认直接会被分配在老年代。

    但是,如果是一个短期存在的巨型对象,在分区之间来回拷贝,就会对垃圾收集器造成负面影响。 为了解决这个问题,G1划分了Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。 

(五)G1 GC 的注意事项

    特别需要注意的是,某些情况下 G1 触发了 Full GC,这时 G1 会退化使用 Serial 收集器来完成垃圾的清理工作,它仅仅使用单线程来完成 GC 工作,GC 暂停时间将达到秒级别的。

    1、并发模式失败

      G1 启动标记周期,但在 Mix GC 之前,老年代就被填满,这时候 G1 会放弃标记周期。解决办法:增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads 等)。

    2、晋升失败

      没有足够的内存供存活对象或晋升对象使用,由此触发了 Full GC(to-space exhausted/to-space overflow)。解决办法:a) 增加 –XX:G1ReservePercent 选项的值(并相应增加总的堆大小)增加预留内存量。b) 通过减少 –XX:InitiatingHeapOccupancyPercent 提前启动标记周期。c) 也可以通过增加 –XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

    3、巨型对象分配失败

      当巨型对象找不到合适的空间进行分配时,就会启动 Full GC,来释放空间。解决办法:增加内存或者增大 -XX:G1HeapRegionSize

(六)G1内存分配演示

  使用G1运行程序

java -Xmx1g -Xms1g -XX:-UseAdaptiveSizePolicy -XX:+UseG1GC -XX:MaxGCPauseMillis=200  -jar gateway-server-0.0.1-SNAPSHOT.jar

  查看堆栈信息:可以看到和CMS的堆栈信息非常类似,只是多了每个Region的大小,有多少个Region(由于系统启动时设置Xmx为1g,因此这里Region为1024),每个分代的Region数等信息。

# jhsdb jmap --heap --pid 17504
Attaching to process ID 17504, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.2+9-LTS

using thread-local object allocation.
Garbage-First (G1) GC with 13 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 1073741824 (1024.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 643825664 (614.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 1048576 (1.0MB)

Heap Usage:
G1 Heap:
   regions  = 1024
   capacity = 1073741824 (1024.0MB)
   used     = 26368096 (25.146575927734375MB)
   free     = 1047373728 (998.8534240722656MB)
   2.45572030544281% used
G1 Young Generation:
Eden Space:
   regions  = 6
   capacity = 655360000 (625.0MB)
   used     = 6291456 (6.0MB)
   free     = 649068544 (619.0MB)
   0.96% used
Survivor Space:
   regions  = 20
   capacity = 20971520 (20.0MB)
   used     = 20971520 (20.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 0
   capacity = 397410304 (379.0MB)
   used     = 0 (0.0MB)
   free     = 397410304 (379.0MB)
   0.0% used

 

五、ZGC/Shenandoah GC详解

  1、ZGC

    -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx16g

    Z Garbage Collector,也称为ZGC,在 JDK11 中引入的一种可扩展的低延迟垃圾收集器,在 JDK15 中发布稳定版。

    ZGC的目标:

      < 1ms 最大暂停时间(jdk < 16 是 10ms,jdk >=16 是 <1ms )

      暂停时间不会随着堆、live-set 或 root-set 的大小而增加

      适用内存大小从 8MB 到16TB 的堆 

    ZGC 最主要的特点包括:

      1. GC 最大停顿时间不超过 10ms

      2. 堆内存支持范围广,小至几百 MB 的堆空间,大至 4TB 的超大堆内存(JDK13 升至 16TB)

      3. 与 G1 相比,应用吞吐量下降不超过 15%

      4. JDK11只支持 Linux/x64 位平台,JDK15 后支持 MacOS 和Windows 系统

      5. 并发、基于 region、压缩、NUMA 感知、使用彩色指针、使用负载屏障 

    ZGC 收集器是一款基于 Region 内存布局的, 不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。ZGC 的核心是一个并发垃圾收集器,这意味着所有繁重的工作都在Java 线程继续执行的同时完成。这极大地限制了垃圾收集对应用程序响应时间的影响。 

-XX:+UseZGC # 启用 ZGC 
-Xmx # 设置最大堆内存 
-Xlog:gc # 打印 GC日志 
-Xlog:gc* # 打印 GC 详细日志

  2、Shenandoah GC

  -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC -Xmx16g

  Shenandoah GC 立项比 ZGC 更早,设计为GC 线程与应用线程并发执行的方式,通过实现垃圾回收过程的并发处理,改善停顿时间,使得 GC 执行线程能够在业务处理线程运行过程中进行堆压缩、标记和整理,从而消除了绝大部分的暂停时间。

  Shenandoah 团队对外宣称 ShenandoahGC 的暂停时间与堆大小无关,无论是 200MB 还是 200 GB的堆内存,都可以保障具有很低的暂停时间(注意:并不像 ZGC 那样保证暂停时间在 10ms 以内)。

六、GC汇总

(一)GC汇总会演进

  Java 目前支持的 GC 算法,一共有 7 类:

    1. 串行 GC(Serial GC): 单线程执行,应用需要暂停;

    2. 并行 GC(ParNew、Parallel Scavenge、Parallel Old): 多线程并行地执行垃圾回收,关注与高吞吐;

    3. CMS(Concurrent Mark-Sweep): 多线程并发标记和清除,关注与降低延迟;

    4. G1(G First): 通过划分多个内存区域做增量整理和回收,进一步降低延迟;

    5. ZGC(Z Garbage Collector): 通过着色指针和读屏障,实现几乎全部的并发执行,几毫秒级别的延迟,线性可扩展;

    6. Epsilon: 实验性的 GC,供性能分析使用;其不做垃圾回收,在程序运行过程中创建的所有对象都被保留,可以做更准确的分析,该GC不会用于生产环境

    7. Shenandoah: G1 的改进版本,跟 ZGC 类似。

  可以看出 GC 算法和实现的演进路线:

    1. 串行 -> 并行: 重复利用多核 CPU 的优势,大幅降低 GC 暂停时间,提升吞吐量。

    2. 并行 -> 并发: 不只开多个 GC 线程并行回收,还将 GC 操作拆分为多个步骤,让很多繁重的任务和应用线程一起并发执行,减少了单次 GC 暂停持续的时间,这能有效降低业务系统的延迟。

    3. CMS -> G1: G1 可以说是在 CMS 基础上进行迭代和优化开发出来的,划分为多个小堆块进行增量回收,这样就更进一步地降低了单次 GC 暂停的时间

    4. G1 -> ZGC::ZGC 号称无停顿垃圾收集器,这又是一次极大的改进。ZGC 和 G1 有一些相似的地方,但是底层的算法和思想又有了全新的突破。      

(二)各个GC对比与组合

  1、GC对比

        

  2、GC组合

        

   对于垃圾回收器的选择:

    单CPU或小内存,可以使用SerialGC,-XX:+UserSerialGC

    多CPU,需要大吞吐量,可以使用ParalellGC或者ParalellOldGC,-XX:+UseParalellGC或-XX:UseParalellOldGC

    多CPU,追求低停顿,可以使用ParNew或CMS,-XX:UseParNewGC或-XX:UseConcMarkSweepGC

(三)GC 如何选择

  在选择GC时,可以使用压测工具对于可选的垃圾回收器进行压测分析,得出结论都做选择,如果没有相关的压测环境,那么以下也是选择GC的一些原则:

    1、如果系统考虑吞吐优先,CPU 资源都用来最大程度处理业务,用 Parallel GC;

    2、如果系统考虑低延迟有限,每次 GC 时间尽量短,用 CMS GC;

    3、如果系统内存堆较大,同时希望整体来看平均 GC 时间可控,使用 G1 GC。对于内存大小的考量:1. 一般 4G 以上,算是比较大,用 G1 的性价比较高。2. 一般超过 8G,比如 16G-64G 内存,非常推荐使用 G1 GC。

    4、如果系统内存特别特别大,例如128G、256G甚至更大,则选择ZGC或shennandoahGC

  但是任何脱离场景谈性能都是耍流氓”。

    例如:目前绝大部分 Java 应用系统,堆内存并不大比如 2G-4G 以内,而且对 10ms 这种低延迟的 GC 暂停不敏感,也就是说处理一个业务步骤,大概几百毫秒都是可以接受的,GC 暂停 100ms 还是 10ms 没多大区别。另一方面,系统的吞吐量反而往往是我们追求的重点,这时候就需要考虑采用并行 GC。如果堆内存再大一些,可以考虑 G1 GC。如果内存非常大(比如超过 16G,甚至是 64G、128G),或者是对延迟非常敏感(比如高频量化交易系统),就需要考虑使用本节提到的新 GC(ZGC/Shenandoah)。

(四)Minor GC 、Major GC和 Full GC 有什么区别?

  新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。Minor GC 非常频繁,回收速度比较快。

  老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集, Major GC 一般比 Minor GC 慢 10 倍以上。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,需按上下文区分到底是指老年代的收集还是整堆收集。

  整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

  混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。 

 

posted @ 2021-07-09 10:53  李聪龙  阅读(201)  评论(0编辑  收藏  举报