JVM中垃圾回收算法及垃圾收集器

目录

1 垃圾收集器分类

1.1 次收集器

1.2 全收集器

1.3 内存分配和回收

1.3.1 静态内存分配和回收

1.3.2 动态内存分配和回收

1.3.3 栈内存分配

2 常见垃圾回收算法

2.1 引用计数(Reference Counting)

2.1.1 算法分析

2.1.2 优缺点

2.2 标记-清除(Mark-Sweep)或tracing算法(Tracing Collector)  

2.2.1 根搜索算法(GC ROOT)

2.3 标记-整理(Mark-Compact)

2.4 复制(Copying)

2.5 generation算法(Generational Collector)

2.5.1 年轻代(Young Generation)

2.5.2 年老代(Old Generation)

2.5.3 持久代(Permanent Generation)

3 垃圾收集器

3.1 串行收集器

3.1.1 串行收集器(Serial)

3.1.2 串行收集器(Serial Old)

3.2 Parallel 并行收集器

3.2.1 并行收集器(ParNew)

3.2.2 ParallelGC并行收集器

3.2.3 Parallel Scavenge收集器

3.2.3 Parallel Old收集器

3.3 CMS收集器

3.4 G1收集器

3.4.1 G1简介

3.4.2 G1回收步骤

3.4.3 关键命令行开关

3.5 Shenandoah 的性能

3.6 ZGC收集器

3.7 选择收集器


1 垃圾收集器分类

垃圾回收系统是 java 虚拟机的重要组成部分,垃圾回收器可以对 栈 堆进行回收。其中, java 堆是垃圾收集器的工作重点
和 C/C++不同, java 中所有的对象空间释放都是隐式的,也就是说, java 中没有类似 free()或者 delete()这样的函数释放指定的内存区域。对于不再使用的垃圾对象,垃圾回收系统会在后台默默工作,默默查找、标识并释放垃圾对象,完成包括 java 堆、方法区和直接内存中的全自动化管理。

按分区对待的方式分,有三类:增量垃圾回收(实时垃圾回收算法,即:在应用进行的同时进行垃圾回收),分代复制垃圾回收,标记垃圾回收

按系统线程分:

  • 串行收集:串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。
  • 并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上CPU数目越多,越能体现出并行收集器的优势。
  • 并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越长。

1.1 次收集器

Scavenge GC, 指发生在新生代的 GC,因为新生代的 Java 对象大多都是朝生夕死,所以Scavenge GC 非常频繁,一般回收速度也比较快。当 Eden 空间不足以为对象分配内存时,会触发 Scavenge GC。

一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。

当年轻代堆空间紧张时会被触发相对于全收集而言,收集间隔较短,这里的年轻代满指的是Eden代满,Survivor满不会引发GC

1.2 全收集器

Full GC,对整个堆进行回收(包括年轻代、老年代和持久代),出现了 Full GC 一般会伴随着至少一次的 Minor GC(老年代的对象大部分是 Scavenge GC 过程中从新生代进入老年代),比如:分配担保失败。 Full GC 的速度一般会比 Scavenge GC 慢 10 倍以上。当老年代内存不足或者显式调用 System.gc()方法时,会触发 Full GC。

当老年代或者持久代堆空间满了,会触发全收集操作可以使用 System.gc()方法来显式的启动全收集,全收集一般根据堆大小的不同,需要的时间不尽相同,但一般会比较长。

Full GC触发机制:

  • 调用System.gc时,系统建议执行Full GC,但是不必然执行
  • 老年代空间不足
  • 方法区空间不足
  • 通过Minor GC后进入老年代的平均大小大于老年代的可用内存
  • 由Eden区、survivor space1(From Space)区向survivor space2(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载

1.3 内存分配和回收

Java语目本身来说,通常显式的内存申请有两种:一种是静态内存分配,另一种是动态内存分配

1.3.1 静态内存分配和回收

在Java中静态内存分配是指在Java被编译时就已经能够确定需要的内存空间,当程序被加载时系统把内存一次性分配给它。这些内存不会在程序执行时发生变化,直到程序执行结束时内存才被回收。在Java的类和方法中的局部变量包括原生数据类型(int、long、char等)和对象的引用都是静态分配内存的,如下面这段代码

public void staticData(int arg){  
   String s="String";  
   long 1=1;  
   Long lg=lL;  
   Object o = new Object();  
   Integer i = 0;  
}  

其中参数arg、1是原生的数据类型,s、o和i是指向对象的引用。在Javac编译时就已经确定了这些变量的静态内存空间。其中arg会分配4个字节,long会分配8个字节,String、Long、Object和Integer是对象的类型,它们的引用会占用4个字节空间,所以这个方法占用的静态内存空间是4+4+8+4+4+4=28字节。

静态内存空间当这段代码运行结束时回收,这些静态内存空间是在Java栈上分配的,当这个方法运行结束时,对应的栈帧也就撤销,所以分配的静态内存空间也就回收了

1.3.2 动态内存分配和回收

在前面的例子中变量lg和i存储与值虽然与1和arg变量一样,但是它们存储的位置是不一样的,后者是原生数据类型,它们存储在Java栈中,方法执行结束就会消失,而前者是对象类型,它们存储在Java堆中,它们是可以被共享的,也不一定随着方法执行结束而消失。变量1和lg的内存空间大小显然也是不—样的,1在Java栈中被分配8个字节空间,而lg被分配4个字节的地址指针空间,这个地址指针指向这个对象在堆中的地址。很显然在堆中long类型数字1肯定不只8个字节,所以Long代表的数字肯定比long类型占用的空间要大很多。

在Java中对象的内存空间是动态分配的,所谓的动态分配就是在程序执行时才知道要分配的存储空间大小,而不是在编译时就能够确定的。lg代表的Long对象,只有JVM在解析Long类时才知道在这个类中有哪些信息,这些信息都是哪些类型,然后再为这些信息分配相应的存储空间存储相应的值。而这个对象什么时候被回收也是不确定的,只有等到这个对象不再使用时才会被回收。

由于内存的分配是在对象创建时发生的,而内存的回收是以对象不再引用为前提的。这种动态内存的分配和回收是和Java中一些数据类型关联的,Java程序员根本不需要关注内存的分配和回收,只需关注这些数据类型的使用就行了。

1.3.3 栈内存分配

栈式内存分配也可称为动态存储分配,是由一个类似于堆栈的运行栈来实现的。和静态内存分配相反,在栈式内存方案中,程对数据区的需求在编译时是完全未知的,只有到运行时才能知道,但是规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才能够为其分配内存。和我们所熟知的数据结构中的栈一样,栈式内存分配按照先进后出的原则进行分配。

在编写程序时除了在编译时能确定数据的存储空间和在程序入口处能知道存储空间外,还有一种情况就是当程序真正运行到相应代码时才会知道空间大小,在这种情况下我们就需要堆这种分配策略。

2 常见垃圾回收算法

2.1 引用计数(Reference Counting)

比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的问题

2.1.1 算法分析

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

2.1.2 优缺点

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
          
        object1.object = object2;
        object2.object = object1;
          
        object1 = null;
        object2 = null;
    }
}

最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们 

2.2 标记-清除(Mark-Sweep)或tracing算法(Tracing Collector)  

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。 简图如下:

2.2.1 根搜索算法(GC ROOT)

根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

从程序运行的角度来说,一些在程序运行过程中始终保持存活,不死亡的对象可以作为GC Root

java中可作为GC Root的对象有:

  • 虚拟机栈中引用的对象(本地变量表,局部变量)有些对象是直接在操作栈中持有的,所以操作栈肯定也包含根对象集合
  • 在常量池中的对象引用:每个类都会包含一个常量池,这些常用池中也会包含很多对象引用,如表示类名的字符串就保存在堆中,那么常量池中只会持有这个字符串对象的引用

  • 堆中静态属性引用的对象(静态字段)

  • 本地方法栈中引用的对象(Native对象,JNI引用)有些对象被传入本地方法中,但是这些对象还没有被释放

  • 堆中常量引用的对象
  • 类的Class对象:当每个类被JVM加载时都会创建一个代表这个类的唯一数据类型的Class对象,而这个Class对象也同样存放在堆中,当这个类不再被使用时,在方法区中类数据和这个Class对象同样需要被回收

  • 活动线程

Java对象的实例存储在jvm的堆区,对于GC线程来说,这些对象有三种状态。

  1. 可触及状态:程序中还有变量引用,那么此对象为可触及状态。
  2. 可复活状态:当程序中已经没有变量引用这个对象,那么此对象由可触及状态转为可复活状态。CG线程将在一定的时间准备调用此对象的finalize方法(finalize方法继承或重写子Object的finalize方法详解),finalize方法内的代码有可能将对象转为可触及状态,否则对象转化为不可触及状态。
  3. 不可触及状态:只有当对象处于不可触及状态时,GC线程才能回收此对象的内存

 标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片 

2.3 标记-整理(Mark-Compact)

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。 简图如下:


 那么标记-整理是如何“压缩”到堆中的一块呢,如下所示:

 

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表

2.4 复制(Copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小, 同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。 简图如下:


该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾收集就从根集中扫描活动对象,并将每个活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行

2.5 generation算法(Generational Collector)

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

Java 堆内存开关

Java提供了大量的内存开关(参数),我们可以用它来设置内存大小和它们的比例。下面是一些常用的开关:

JVM 开关

JVM 开关描述

-Xms

设置JVM启动时堆的初始化大小。

-Xmx

设置堆最大值。

-Xmn

设置年轻代的空间大小,剩下的为老年代的空间大小。

-XX:PermGen

设置永久代内存的初始化大小。

-XX:MaxPermGen

设置永久代的最大值。

-XX:SurvivorRatio

提供Eden区和survivor区的空间比例。比如,如果年轻代的大小为10m并且VM开关是-XX:SurvivorRatio=2,那么将会保留5m内存给Eden区和每个Survivor区分配2.5m内存。默认比例是8。

-XX:NewRatio

提供年老代和年轻代的比例大小。默认值是2。

大多数时候,上面的选项已经足够使用了。但是如果你还想了解其他的选项,那么请查看JVM选项官方网页

2.5.1 年轻代(Young Generation)

所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复默。仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加 1, GC 分代年龄存储在对象的 header中

当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

2.5.2 年老代(Old Generation)

1.在年轻代中经历了N次(默认为15)垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

2.5.3 持久代(Permanent Generation)

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类

3 垃圾收集器

新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge

老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

3.1 串行收集器

3.1.1 串行收集器(Serial)

Serial(复制算法):新生代单线程收集器,标记和清理都是单线程,优点是简单高效

Serial 收集器是 Hotspot 运行在 Client 模式下的默认新生代收集器, 它的特点是: 只用一个 CPU(计算核心) /一条收集线程去完成 GC 工作, 且在进行垃圾收集时必须暂停其他所有的工作线程(“Stop The World” -后面简称 STW)。可以使用-XX:+UseSerialGC 打开。虽然是单线程收集, 但它却简单而高效, 在 VM 管理内存不大的情况下(收集几十 M~一两百 M 的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内。

3.1.2 串行收集器(Serial Old

Serial Old(标记-整理算法):老年代单线程收集器,Serial收集器的老年代版本,进行垃圾收集时,必须暂停所有工作线程

3.2 Parallel 并行收集器

Parallel GC 根据Minor GCFull GC的不同分为三种,分别是 ParNewGC、ParallelGC和 ParalleloldGC

注意:并行收集器,只是针对垃圾回收是多线程,依然会暂停用户应用线程 

3.2.1 并行收集器(ParNew)

ParNew收集器(停止-复制算法):新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现

可以通过-XX:+UseParNewGC参数来指定,ParNew 收集器其实是前面 Serial 的多线程版本, 除使用多条线程进行 GC 外, 包括 Serial可用的所有控制参数、收集算法、 STW、对象分配规则、回收策略等都与 Serial 完全一样(也是 VM 启用 CMS 收集器-XX: +UseConcMarkSweepGC 的默认新生代收集器)。由于存在线程切换的开销, ParNew 在单 CPU 的环境中比不上 Serial, 且在通过超线程技术实现的两个 CPU 的环境中也不能 100%保证能超越 Serial. 但随着可用的 CPU 数量的增加,收集效率肯定也会大大增加(ParNew 收集线程数与 CPU 的数量相同, 因此在 CPU 数量过大的环境中, 可用-XX:ParallelGCThreads=<N>参数控制 GC 线程数)。

3.2.2 ParallelGC并行收集器

在JDK8等版本中,是server模式JVM的默认GC选择,也被称为吞吐量优先的GC,算法和Serial GC相似,特点是老生代和新生代GC并行进行,更加高效, 在年轻代使用标记-复制算法 , 在老年代使用标记-清除-整理算法。年轻代和老年代的垃圾回收都会触发STW事件,暂停所有的应用线程来执行垃圾收集。两者在执行标记和 复制/整理阶段时都使用多个线程, 因此得名“(Parallel)”。通过并行执行, 使得GC时间大幅减少

ParallelGC:在Server下默认的GC方式,可以通过-XX:+UseParallelGC参数来强制指定,并行回收的线程数可以通过 -XX:ParallelGCThreads来指定,这个值有个计算公式,如果CPU和核数小于8,线程数可以和核数一样,如果大于8,值为3+ (cpuC〇re*5)/8。
可以通过-Xmn来控制Young区的大小,如-XmanlOm,即设置Young区的大小为10MB。在Young区内的Eden、From Space和To Space的大小控制可以通过SurvivorRatio参数来完成,如设置成-XX:SurvivorRatio=8,表示Eden区与From Space的大小为8:1,如果Young区的总大小为10 MB,那么Eden、sO和si的大小分别为8 MB、1 MB和1 MB。但在默认情况下以-XX:InitialSurivivorRatio设置的为准,这个值默认也为8,表示的是Young:s0为8:1。
当在Eden区中申请内存空间时,如果Eden区不够,那么看当前申请的空间是否大于等于Eden的一半,如果大于则这次申请的空间直接在Old中分配, 如果小于则触发MinorGC。在触发GC之前首先会检查每次晋升到Old区的平均大小是否大于Old区的剩余空间,若大于则再触发Full GC。在这次触发GC后仍然会按照这个规则重新检查一次。也就是如果满足上面这个规则,Full GC会执行两次 

  • 使用-XX:+UseParallelGC参数来启用Parallel Scavenge和PSMarkSweep(Serial Old)收集器组合进行垃圾收集
  • 使用-XX:+UserParallelOldGC参数来启用Parallel scavenge和Parallel Old收集器组合收集

3.2.3 Parallel Scavenge收集器

Parallel Scavenge收集器(停止-复制算法):并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量最大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量最大化

与 ParNew 类似, Parallel Scavenge 也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge 更关注系统吞吐量:系统吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用 CPU时间,尽快地完成程序的运算任务.

与 ParNew 类似,只不过Parallel Scavenge算法并没有使用分代式GC框架——generational GC framework,ParNew算法使用了generational GC framework框架,所以ParNew算法可以用在CMS里,而Parallel Scavenge不可以。

Parallel Scavenge 提供了如下参数设置系统吞吐量:

Parallel Scavenge 参数描述
-XX:MaxGCPauseMillis(毫秒数) 收集器将尽力保证内存回收花费的时间不超过
设定值, 但如果太小将会导致 GC 的频率增加.
-XX:GCTimeRatio(整数:0 < GCTimeRatio < 100) 是垃圾收集时间占总时间的
比率
XX:+UseAdaptiveSizePo
licy
启用 GC 自适应的调节策略: 不再需要手工指定-Xmn、
-XX:SurvivorRatio、 -XX:PretenureSizeThreshold 等细节参数, VM
会根据当前系统的运行情况收集性能监控信息, 动态调整这些
参数以提供最合适的停顿时间或最大的吞吐量

3.2.3 Parallel Old收集器

Parallel Old收集器(标记-整理算法):Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先,作用于老年代,多线程

可以通过-XX:+UseParalleloldGC参数来强制指定,并行回收的线程数可以通过-XX:ParallelGCThreads来指定,这个 数字的值有个计算公式,如果CPU和核数小于8,线程数可以和核数一样,如果大于8,值为3+(cpu core*5)/8

它与ParallelGC有何不同呢?
其实不同之处在Full GC上,前者Full GC进行的动作为清空整个Heap堆中的垃圾对象,清除Perm区中已经被卸载的类信息,并进行压缩。而后者是清除Heap堆中的部分垃圾对象,并进行部分的空间压缩

3.3 CMS收集器

CMS(Concurrent Mark Sweep)收集器(标记-清理算法):作用于老年代多线程,高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择,尽可能地缩短垃圾收集时用户线程的停顿时间,让垃圾收集和用户线程并行执行,从而减少应用停顿时间,提升用户体验。

可通过-XX:UseConcMarkSweepGC来指定,并发的线程数默认为4 (并行GC线程数+3),也可通过ParallelCMSThreads来指定

CMS GC与之前讨论的GC不太一样,它既不是上面所说的Minor GC,也不是Full GC,它是基于这两种GC之间的一种GC。它的触发规则是检查old区或者Perm区的使用率,当达到一定比例时就会触发CMS GC,触发时会回收Old区中的内存空间
这个比例可以通过CMSInitiatingOccupancyFraction参数来指定,默认是92%,这个默认值是通过((100-MinHeapFreeRatio)+(double) (CMSTriggerRatio*MinHeapFreeRatio)/l 00.0)/100.0 计算出来的,其中的 MinHeapFreeRatio 为 40、CMSTriggerRatio 为 80。

如果让 Perm 区也使用 CMS GC可以通过-XX:+CMSClassUnloadingEnabled来设定,Perm区的比例默认值也是92%,这个值可以通过 CMSInitiatingPermOccupancyFraction设定。这个默认值也是通过一个公式计算出来的:((100- MinHeapFreeRatio)+(double)(CMSTriggerPermRatio*MinHeapFreeRatio)/100.0) /100.0,其中 MinHeapFreeRatio 为 40, CMSTriggerPermRatio 为 80.
触发CMS GC时回收的只是Old区或者Perm区的垃圾对象,在回收时和前面所说的Minor GC和Full GC基本没有关系。
在这个模式下的Minor GC触发规则和回收规则与Serial Collector基本一致,不同之处只是GC回收的线程是多线程而已。​​​​​

触发Full GC是在这两种情况下发生的:

  • 一种是Eden分配失败,Minor GC后分配到To Space, To Space不够再分配到Old区,Old区不够则触发Full GC;
  • 另外一种情况是,当CMS GC正在进行时向Old申请内存失败则会直接触发Full GC。

这里还需要特别提醒一下,在Hotspot 1.6中使用这种GC方式时在程序中显式地调用了 System.gc,且设置了 ExplicitGCInvokesConcurrent参数,那么使用NIO时可能会引发内存泄漏

CMS GC何时执行JVM还会有一些时机选择,如当前的CPU是否繁忙等因素,因此它会有一个计算规则,并根据这个规则来动态调整。但是这也会给JVM带来另外的开销.如果要去掉这个动态调整功能,禁止JVM自行触发CMS GC,可以通过配置参数-XX:+UseCMSInitiatingOccupancyOnly 来实现。

当然在获得低停顿的好处时是付出了吞吐量的代价,通常与 Parallel 系收集器相比吞吐率下降 10%-40%。

CMS收集器的处理整个过程有如下步骤:

  1. 初始标记:找到 GC Roots,但是需要 Stop The World
  2. 并发标记:标记所有从 GC Roots 可达的对象。
  3. 并发预清理:检查对象引用更新和在并发标记阶段晋升到老年代的对象并进行标记。
  4. 重新标记:标记预清理阶段更新的对象引用,需要Stop The World
  5. 并发清理:回收死亡对象的内存。
  6. 并发重置:重置数据结构为下次运行作准备。

其中步骤1(初始标记)和步骤4( 重新标记)仍然需要 Stop The World,只是相对来说时间较短。

低停顿是 CMS 收集器是的优点,但它也并不完美,它有 3 个明显缺点:

  1. 对CPU资源敏感:由于和用户线程并发执行,所以存在 CPU 争抢的问题。
    Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生并发模式失败,此时整个应用将会暂停,进行垃圾回收
  2. 无法回收浮动垃圾:由于CMS并发清理阶段用户线程还在工作,这个时候产生的垃圾,CMS无法在本次收集中处理掉它们,只能留在下一次GC时再将其处理掉,这部分垃圾称为“浮动垃圾”
  3. 产生内存垃圾碎片:CMS仅进行了标记、清除而未进行整理,容易产生大量内存空间碎片。

CMS 默认启动的回收线程是 (CPU数量 + 3) / 4,也就是 CPU 在 4 个以上时并发回收线程使用的 CPU 资源不少于 25%。 在并发清理时新产生的垃圾称为浮动垃圾(Floating Garbage),本次无法收集,当浮动垃圾过多导致预留的内存无法满足程序需要时触发, 就可能出现 Concurrent Mode Failure 导致启用 Serial Old 收集器作为后备进行 Full GC

3.4 G1收集器

3.4.1 G1简介

G1可谓博采众家之长,力求到达一种完美。吸取了增量收集优点,把整个堆划分为一个一个等大小的区域(region)。内存的回收和划分都以region为单位;同时,也吸取了CMS的特点,把这个垃圾回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1也认同分代垃圾回收的思想,认为不同对象的生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。为了达到对回收时间的可预计性,G1在扫描了region以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的region,以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种方式被称为Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收

G1,Java堆并行收集器,采用标记整理算法,不同于之前的收集器一个重要特点是:G1回收的范围是整个Java堆(包括新生代和老年代),而前面收集器仅限于新生代或老年代

有如下特点:

  • 支持很大的堆
  • 高吞吐量
    --支持多CPU和垃圾回收线程
    --在主线程暂停的情况下,使用并行收集
    --在主线程运行的情况下,使用并发收集
  • 实时目标:可配置在N毫秒内最多只占用M毫秒的时间进行垃圾回收

Garbage First (G1) Collector一种新的收集器,是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征,在 jdk7u4 开始正式支持,它有如下特点:多分区+优先收集

1. 多分区的堆组织方式

G1也是分代收集器,但其组织堆的方式与其他收集器完全不同。它根据不同的用途将堆分为大量(~2000)固定大小的区域(region)。 相同用途的堆也并不连续,G1 依然保留了新生代和老年代的概念,但新生代和老年代不再是物理上隔离的了,它们都是一部分 region 的集合,如图所示。

如果一个对象大小超过了普通区域大小的50%,那么它会被分配到一个大区域(humongous)里面。 

2. 优先的收集方式

G1 的收集方式追求低停顿,并且建立可预测的停顿时间模型(在 M 毫秒的时间片段内,GC 的时间不得超过 N 毫秒,N < M)。 G1 通过有计划的避免在整个堆中进行全区域扫描进行垃圾收集,它通过跟踪各个 region 中垃圾的价值大小(回收获得的空间及回收所花费的时间的经验值,活跃对象小), 在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的region,这也正式 Garbage-First 名称的由来。 而对 region 的收集采用的是 Stop-The-World 的方式,增量的将存活的对象复制到一个空 region 里面,这种方式不会产生内存碎片问题。

关于 GC 的折衷权衡点来总结下,当谈论垃圾收集的时候,我们主要考虑三个收集器的指标:

  1. 吞吐量:花费在 GC 上的时间占整个应用程序工作的比例
  2. 延迟:因为垃圾回收,而引起的响应暂停的时间
  3. 内存:系统使用内存来存储状态,在管理的时候它们常常需要复制和移动。

上述三个指标,吞吐量越大也越好,延迟越低约好,内存复制和移动产生的碎片越少越好。 但可惜这三个目标很难同时满足,很多时候我们都是根据应用类型在其中做出权衡取舍

建议使用G1收集器的场景,使用时是:

如果应用有以下一个或多个特点,当下运行着CMS或ParallelOldGC垃圾收集器的应用把收集器切换到G1收集器的话,会从中受益的:

  • Full GC持续时间太长或者太频繁
  • 对象分配比率或者提升有显著的变化
  • 不期望的长时间垃圾收集或者压缩暂停(大于0.5到1秒)

3.4.2 G1回收步骤

  • 初始标记(Initial Marking)
    G1对于每个region都保存了两个标识用的bitmap,一个为previous marking bitmap,一个为next marking bitmap,bitmap中包含了一个bit的地址信息来指向对象的起始点。
    开始Initial Marking之前,首先并发的清空next marking bitmap,然后停止所有应用线程,并扫描标识出每个region中root可直接访问到的对象,将region中top的值放入next top at mark start(TAMS)中,之后恢复所有应用线程。
    触发这个步骤执行的条件为:
    G1定义了一个JVM Heap大小的百分比的阀值,称为h,另外还有一个H,H的值为(1-h)*Heap Size,目前这个h的值是固定的,后续G1也许会将其改为动态的,根据jvm的运行情况来动态的调整,在分代方式下,G1还定义了一个u以及soft limit,soft limit的值为H-u*Heap Size,当Heap中使用的内存超过了soft limit值时,就会在一次clean up执行完毕后在应用允许的GC暂停时间范围内尽快的执行此步骤;
    在pure方式下,G1将marking与clean up组成一个环,以便clean up能充分的使用marking的信息,当clean up开始回收时,首先回收能够带来最多内存空间的regions,当经过多次的clean up,回收到没多少空间的regions时,G1重新初始化一个新的marking与clean up构成的环。
  • 并发标记(Concurrent Marking)
    按照之前Initial Marking扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期间应用线程并发修改的对象的以来关系则记录到remembered set logs中,新创建的对象则放入比top值更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改top值。
  • 最终标记暂停(Final Marking Pause)
    当应用线程的remembered set logs未满时,是不会放入filled RS buffers中的,在这样的情况下,这些remebered set logs中记录的card的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线程中存在的remembered set logs的内容进行处理,并相应的修改remembered sets,这一步需要暂停应用,并行的运行。
  • 存活对象计算及清除(Live Data Counting and Cleanup)
    值得注意的是,在G1中,并不是说Final Marking Pause执行完了,就肯定执行Cleanup这步的,由于这步需要暂停应用,G1为了能够达到准实时的要求,需要根据用户指定的最大的GC造成的暂停时间来合理的规划什么时候执行Cleanup,另外还有几种情况也是会触发这个步骤的执行的:
    G1采用的是复制方法来进行收集,必须保证每次的to space的空间都是够的,因此G1采取的策略是当已经使用的内存空间达到了H时,就执行Cleanup这个步骤;
    对于full-young和partially-young的分代模式的G1而言,则还有情况会触发Cleanup的执行,full-young模式下,G1根据应用可接受的暂停时间、回收young regions需要消耗的时间来估算出一个yound regions的数量值,当JVM中分配对象的young regions的数量达到此值时,Cleanup就会执行;partially-young模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行Cleanup,并最大限度的去执行non-young regions的Cleanup。

3.4.3 关键命令行开关

  1. -XX:+UseG1GC 让 JVM 使用 G1 垃圾收集器.
  2. -XX:MaxGCPauseMillis=200   设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽力去达成这个目标. 所以有时候这个目标并不能达成. 默认值为 200 毫秒.
  3. -XX:InitiatingHeapOccupancyPercent=45  启动并发GC时的堆内存占用百分比. G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。值为 0 则表示“一直执行GC循环)'. 默认值为 45 (例如, 全部的 45% 或者使用了45%).
  4. -XX:NewRatio=n 新生代与老生代(new/old generation)的大小比例(Ratio). 默认值为 2.
  5. -XX:SurvivorRatio=n eden/survivor 空间大小的比例(Ratio). 默认值为 8.
  6. -XX:MaxTenuringThreshold=n 提升年老代的最大临界值(tenuring threshold). 默认值为 15.
  7. -XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.
  8. -XX:ConcGCThreads=n 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同.
  9. -XX:G1ReservePercent=n 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.
  10. -XX:G1HeapRegionSize=n 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb

3.5 Shenandoah 的性能

译注: Shenandoah: 谢南多厄河; 情人渡,水手谣;

超低延迟垃圾收集器(Ultra-Low-Pause-Time Garbage Collector). 它的设计目标是管理大型的多核服务器上,超大型的堆内存: 管理 100GB 及以上的堆容量, GC暂停时间小于 10ms。 当然,也是需要和吞吐量进行权衡的: 没有GC暂停的时候,算法的实现对吞吐量的性能损失不能超过10%

它也构建在前面所提到的很多算法的基础上, 例如并发标记和增量收集。但其中有很多东西是不同的。它不再将堆内存划分成多个代, 而是只采用单个空间. 没错, Shenandoah 并不是一款分代垃圾收集器。这也就不再需要 card tables 和 remembered sets. 它还使用转发指针(forwarding pointers), 以及Brooks 风格的读屏障(Brooks style read barrier), 以允许对存活对象的并发复制, 从而减少GC暂停的次数和时间。

3.6 ZGC收集器

这是JDK11发布的一款垃圾收集器,是一个可扩展的低延迟垃圾收集器,JDK17的默认垃圾收集器,能用就用,性能绝对可以。

有如下特性:

  • 暂停时间不超过10毫秒
  • 暂停时间不会随堆或实时设置大小而增加
  • 处理堆范围从几百M到几TB

3.7 选择收集器

点此了解垃圾收集器的选择

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值