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

一、概述

垃圾收集(Garbage Collection,简称GC),这项技术并不是Java语言的伴生产物,早在java语言出来之前,就已经有语言开始使用动态分配内存和垃圾回收了,例如Lisp的语言就是第一门使用内存动态分配和垃圾收集技术的语言

垃圾收集主要考虑考虑以下三个问题

  • 哪些内存需要回收
  • 何时回收
  • 怎么回收

对于java虚拟机中的五大区域来说,程序计数器、虚拟机栈、本地方法栈三个区域是线程私有的,随线程而生随线程而灭。而且每一个栈帧基本上在类的结构被确定下来后其大小也确定下来,所以这三个区域的分配和回收都具有确定性,不需要过多的考虑

而方法区和堆内存分配是不确定的,所以垃圾收集器主要关注的正是这部分·

二、对象已死?

即讨论哪些内存需要回收

2.1、引用计数算法

HotSpot并没有采用这种算法

原理:在对象中添加一个引用计数器,当对象被引用时,计数器+1,当引用失效时,计数器-1.任何时刻计数器为0的对象就是不能被使用的对象

优点:判定效率高

缺点:简单的引用技术法存在缺陷,譬如对象之间循环引用的问题

循环引用:

package com.perfume;/*** @author 沈洋 邮箱:1845973183@qq.com* @create 26-05-2021-13:10**/
public class ReferenceCountingGC {public Object instance = null;private static final int _1MB = 1024*1024;/*** 这个成员属性的唯一意义就是占一点内存,以便能在Gc日志中清除的看到是否被回收。*/private byte[] bigSize = new byte[2*_1MB];public static void testGC(){ReferenceCountingGC objA = new ReferenceCountingGC();ReferenceCountingGC objB = new ReferenceCountingGC();objA.instance=objB;objB.instance=objA;objA=null;objB=null;//此时发生GC,A,B对象是否会被回收System.gc();}
}

对于这种情况,如果使用引用计数器算法,A,B对象将不会被回收。因为A,B对象分别被对方引用,所以两个对象的引用计数为2.

当objA=null和objB=null执行时,两个对象的引用技数变为1。但实际上这个时候程序中已经没有办法使用AB两个对象了。

2.2、可达性分析算法

原理:通过一系列称为GC Roots的根对象作为起始节点集,从这些结点开始,根据引用关系向下搜索。搜索过程所走的路径称为**”引用链“**如果某个对象到GC Roots之间没有任何引用链相连,即图论中结点不可达。证没对象不可被使用。

GC ROOTS

在Java技术体系中,固定可作为GC Roots的对象包括以下几种

  • 在虚拟机栈(栈中的本地变量表)中引用的对象,譬如当前正在运行的方法所使用到的参数、局部变量、临时变量
  • 方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量,即类中static修饰的引用变量
  • 方法区中常量引用的对象,譬如字符串常量里的引用。
  • 本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象常驻的异常类型对象(比如NullPointeException等)、系统类加载器
  • 所有被同步锁持有的对象(synchronized关键字)持有的对象
  • 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存

除了这些固定的GC Roots集合以外。根据垃圾收集器的不同,回收区域不同有些对象还可以临时加入集合中。

比如分代收集局部回收都只是对堆中一个区域进行垃圾收集。因为堆中的区域不是孤立封闭的,这个时候就需要考虑当前回收的区域是否被堆中其他区域引用。这个时候就要考虑将关联区域中的对象加入GC Roots集合中,进行可达性的分析。

2.3、引用分类

无论是通过可达性分析算法判断对象是否引用链可达,还是引用计数器判断对象的引用数量。对象在这种定义下只有两种状态,”被引用“、”未被引用“

但当我们需要描述一类对象:在内存足够时,保留该对象;内存不足时可以抛弃该对象。例如缓存功能

在JDK1.2后,对引用的概念进行了补充。分为四种强度不一的引用:强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)四种,四种引用强度依次递减

  • **强引用:**传统的引用定义,即Object object = new Object(); 这种引用关系,无论任何情况,只要引用关系还存在就不会被回收
  • 软引用:用来描述一些还有用,但非必须的对象。只被软引用关联的对象在系统发生内存溢出时,垃圾收集器会将这部分对象列入二次收集范围内,并进行二次收集。如果二次收集后内存仍不足,才抛出内存溢出异常。JDK1.2提供了SoftReference对象来实现软引用
  • 弱引用:也是用来描述非必须对象,但这种引用比软引用对象强度更低。只被软引用的对象仅能生存到下一次垃圾收集发生。垃圾回收发生后,不论内存是否足够,这种对象都会被回收。JDK1.2后提供了WeakReference类来实现弱引用。
  • 虚引用:虚引用完全不影响对象生存时间,甚至不能通过虚引用获取实例唯一的作用就是虚引用的对象在被收集时,收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用

2.4、生存还是死亡

被可达性分析算法判断为不可达的对象也并不是一定会被回收。对象可以在finalize方法中与GCRoots集合中的结点建立引用链,从而逃脱回收。但这种方式并不推荐使用,而且finalize方法只有一次执行机会,也就是说一个对象最多只有一次逃逸机会。

下面是垃圾收集器在进行可达性分析后,一个对象被标注为不可达后的过程

  • 在可达性分析中,判断为不可达的对象会被标记第一次,随后对这些对象进行筛选。

  • 如果对象没有覆盖finalize()方法或者此前以及在JVM中执行过该方法。则判断为没有必要执行finalize方法。这种对象将会被执行回收。

  • 如果对象被判定为需要执行finalize方法,则会将该对象放入F-Queue队列中(稍后由虚拟机自动创建的一个低调度优先级的线程Finalizer执行它们的finalize方法)

    • 虚拟机在执行finalize方法时只保证会执行,并不保证这个方法一定执行完。因为如果有对象的finalize方法是死循环,那这样可能会导致内存回收子系统崩溃。所以虚拟机并不保证这个方法会执行完。
  • 执行完finalize方法的对象如果还是没有和GCRoots建立连接,将会被标记第二次然后被回收。

  • 执行完finalize方法的对象如果在执行方法后于GCRoots中的结点建立了连接。将会被移除即将回收的集合,从而逃脱回收。

一个对象如何在finalize方法中自我拯救

package com.perfume;/*** 此代码演示两点* 1、对象可以在被GC时自我拯救* 2. 这种拯救机会只有一次,因为一个对象的finalize方法最多被系统调用一次* @author 沈洋 邮箱:1845973183@qq.com* @create 26-05-2021-15:24**/
public class FinalizeEscapeGC {public static FinalizeEscapeGC SAVE_HOOK =null;public void isAlive(){System.out.println("yes i am still alive");}@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("finalize method executed");//方法区中的静态变量引用了这个对象,从而使对象在GC ROOTS中可达。FinalizeEscapeGC.SAVE_HOOK = this;}public static void main(String[] args) throws InterruptedException {SAVE_HOOK = new FinalizeEscapeGC();//断开连接,使对象被GCSAVE_HOOK = null;//提醒执行GCSystem.gc();//因为Finalizer方法优先级很低,暂停0.5秒,以等待它执行Thread.sleep(500);if(SAVE_HOOK!=null){SAVE_HOOK.isAlive();}else System.out.println("no,i am dead");//下面代码与上面完全一致,但这次却不能逃脱GC,因为一个对象的finalize方法只执行一次SAVE_HOOK = null;//提醒执行GCSystem.gc();//因为Finalizer方法优先级很低,暂停0.5秒,以等待它执行Thread.sleep(500);if(SAVE_HOOK!=null){SAVE_HOOK.isAlive();}else System.out.println("no,i am dead");}
}

**总结:**这种方式实际上没有任何必要,不建议使用

2.5、回收方法区

方法区也存在内存回收,但这个区域一般回收性价比不高。并且《Java虚拟机规范》中没有要求必须实现方法区的内存回收。

方法区的垃圾收集主要回收两个部分内容

  • 废弃常量

    回收常量和回收堆中的对象相似,就是判断系统中是否还有地方引用这个常量。(需要垃圾收集器判断是否有必要收集,并不是一个常量没有被引用就会被回收)

  • 不再使用的类型

    判断一个类型是否被回收的条件比较苛刻,主要满足以下三点。

    不过这种收集在大量使用反射、动态代理、CGLIB等字节码框架中使用。避免方法区造成过大的内存压力。

    • 该类的所有实例都已经被回收。
    • 加载该类的类加载器已经被回收(这个条件一般很难实现)
    • 该类对应的Class对象没有在任何地方被引用,即无法通过反射访问该类。

三、垃圾收集算法

垃圾收集算法主要有两大类:引用技术式垃圾收集(Reference Counting GC)、追踪式垃圾收集(Tracing GC)

主流JVM未采用引用技术式垃圾收集算法,所以不做过多讨论

3.1、分代收集理论

分代收集理论是建立在两个假说上面的

  • 弱分代假说:绝大多数对象都是朝生夕灭的
  • 强分代假说:熬过多次垃圾收集的对象就越难消灭

这奠定了垃圾收集器的设计原则:将Java堆分区域,将回收对象根据年龄分配到不同区域

  • 将朝生夕灭的对象集中起来,每次垃圾回收时主要关注这一部分的对象。(只需要去关心哪些对象可以存活
  • 将熬过多次垃圾收集的对象集中起来,使用较低频率去收集。

基于分代收集理论,一般虚拟机都将堆分为**新生代(Young Generation)老年代(Old Generation)**两个区域。

分代回收类型

  • 部分收集(Partial GC):指目标不是完整的收集Java堆的垃圾收集

    • 新生代收集(Minor GC/Young GC):指目标只是新生代的收集
    • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器有单独收集老年代的行为。
    • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器有这种行为。
  • 整堆收集(Full GC):收集整个Java堆和方法区

跨代引用问题

分代收集时并不是简单的将两个区域分开,对象不是孤立的,对象之间存在跨代引用的问题。

比如在收集新生代区域的对象时就需要考虑,哪些对象是被老年代引用了的对象。

所有涉及到部分收集的收集都需要考虑跨代引用的问题,并不仅限于新生代区域的收集

解决办法

  • 这个时候最简单的解决办法是,在收集新生代对象时,再额外遍历老年代的对象以保证可达性分析的正确性。但这样做无疑为内存带来很大的性能负担。

  • **跨代引用假说(Intergenerational Reference Hypothesis):**跨代引用相对于同代引用来说占少数

    **原因:**因为老年代中的对象一般难以消灭,一个新生代的对象被引用后,随着时间推移新生代的对象也将变成老年代对象,从而消除跨代引用。

    所以:我们只需要为新生代区域建立一个全局的数据结构,用来记录老年代中哪些对象存在跨代引用(记忆集,详见四节4.4)。这样在我们对新生代进行回收时,只需要将这一小块区域的对象加入GC Roots中,而 不需要扫描整个老年代。

    注意:需要注意的是,如果老年代中某个对象改变了引用关系,这张表也需要对数据进行修改。

3.2、标记-清除算法

最基础的回收算法,后续算法都是这种算法的改良。

实现过程:根据判定算法判定哪些对象需要回收(或者判定哪些对象需要保留),统一回收所有被标记的对象(或者未被标记的对象)

在进行标记和清除时需要停顿用户线程,只是停顿时间相较后面的标记-整理算法要短。

缺点:

  1. 执行效率不稳定,如果堆中大量对象需要回收时,需要执行大量的标记清除动作,执行效率随之降低。
  2. 内存空间碎片化,标记清除后产生的空闲空间大部分是不连续的,下一次需要分配较大对象空间时,可能会找不到连续空间来分配。无法分配时就会触发Full GC

3.3、标记-复制算法

大部分商用虚拟机都是采用的这种算法(升级版)

普通版

实现过程:将内存按容量一分为二,每一次只使用一块区域。当发生垃圾收后,将存活对象全部复制到另一半区域上继续使用。

缺点

  1. 如果大量对象都是存活的话,就需要执行大量对象的复制,产生内存复制的额外开销
  2. 可用内存只有原来的一半

升级版(Apple式回收)

实现过程:将内存分为三部分:一块较大的Eden空间,两块较小的Survivor空间(HotSpot中默认为8:1:1)。每次分配内存只使用1个Eden+1个Survivor空间,每次回收后将Eden和正在使用的Survivor空间清空,将存活对象复制到空闲的Survivor空间上。当存活对象超过Survivor空间容量时,就会将对象分配到老年代中。

缺点

  1. 对象存活率高时,会进行较多的复制操作,效率降低。

3.4、标记-整理算法

主要使用在老年代中

HotSpot中的Parallel Old收集器主要使用此种收集算法

与标记-清除法相似,区别是标记后不会立刻进行清除,而是将存活对象进行移动整理到一端。

缺点:在老年代中每次回收都会有大量对象需要移动,造成负担。而且与标记-清除算法一样,对象的移动操作都需要全程暂停用户线程(ZGC和Shenandoah收集器使用读屏障来实现清除时暂停用户线程)

对比标记-整理和标记清除算法,两者各有优势。两者的区别主要就是是否移动对象

  • 移动对象

    • 优点:有连续完整的空间分配给大的对象
    • 缺点:移动对象时停顿时间长
  • 不移动对象
    • 优点:减少了移动对象的停顿
    • 缺点:空间碎片化,遇到分配大对象时可能没有连续完整的空间分配

CMS垃圾收集器采用的是两种方式的结合,一般情况使用标记-清除算法.当内存碎片化影响到内存分配时,使用标记-整理算法收集一次.

四、HotSpot的算法实现细节

4.1、根节点枚举

在进行可达性分析时需要在一瞬间暂停用户线程并生成当前时刻的快照然后进行可达性分析

(枚举所有根节点)。

实际上即使生成了快照,如果每一次都需要虚拟机从所有信息中去检查引用,也会造成很大的内存负担。

解决办法:使用一组称为OopMap的数据结构来记录来保存这些GC Roots的引用信息。

4.2、安全点

在任何时刻生成的快照OopMap中的引用都有可能不相同,如果为每一条指令都生成对应的OopMap,将会耗费大量的额外存储空间。

解决办法

在代码中设置一些点为安全点,只有执行到安全点时,才会生成对应的OopMap。这也决定了代码不能再任意位置停下来进行垃圾回收,只能在安全点位置才能进行垃圾回收。

安全点的选取:一般选择那种指令复用的位置作为安全点例如:方法调用、循环跳转、异常跳转等。

如何在发生垃圾收集的时候让所有线程都跑到最近的安全点上?

  • **抢先式中断:**系统发生垃圾收集时,先将所有线程都停下,再检查线程是否在安全点上,如果不在就启动线程继续执行到最近的安全点上。
  • 主动式中断
    • 系统发生垃圾收集时,不主动中断线程,只需要设置一个标志位。每个线程在执行的过程中每次到达安全点时,会采用轮询的方式去判断标志位的状态
    • 如果需要执行垃圾收集,则在当前安全点挂起。
    • 如果不需要执行垃圾收集,则继续向下执行。
    • 这种中断方式要求轮询的指令要足够高效,而在HotSpot中轮询指令仅使用了一条汇编指令来完成
    • 这种安全点只针对非native代码的用户线程(因为本地方法一般不会修改对象的引用)

4.3、安全区域

问题

如果在停顿用户线程生成OopMap时,有线程没有获得CPU,程序长时间得不到执行而无法进入安全区应该如何解决?例如线程Sleep或者Blocked状态

解决办法

这种情况下就需要引入安全区域的概念

安全区可以看做拉伸的安全点,是指在某一段代码片段中,引用关系不会发生变化,在这片区域的任何地方都可以开始垃圾收集。

  1. 线程执行到安全区域,标识自己进入安全区域,

    这样当线程在安全区域的时候,虚拟机发起了垃圾收集时就不需要去管这个线程。

  2. 线程离开安全区域

    检查虚拟机是否执行完了根节点的枚举 ,如果执行完了就离开安全区域。否则一直等待,直到收到信号。

4.4、记忆集与卡表

记忆集的主要作用就是缩减GC ROOTS的扫描范围

在分代收集理论中提到了,使用记忆集的数据结构来解决跨代引用的问题。但其实不只是新生代在收集时需要考虑跨代引用的问题,所有部分区域收集都需要考虑。

卡表其实就是记忆集的一种实现方式

记忆集的实现原理和实现方式

实现原理

因为我们不需要在记忆集中保留对象全部的数据,我们只需要通过记忆集能判断出某一块区域中是否存在跨代引用即可。所以一般采取粗粒度的记录方式。以下三种(卡精度是最常用的

  • 字节精度

    每个记录精度精确到一个机器字长,该字包含跨代指针

  • 对象精度

    每个记录精确到一个对象,对象中含有跨代指针的字段

  • 卡精度

    每个记录精确到一块内存区域,该区域中包含跨代指针

卡精度主要是用一种卡表的方式去实现,一个卡表可以用一个字节数组来实现。

数组中的一个位置就表示一片内存区域,称为卡页。HotSpot中默认卡页大小为512字节,即一个卡表中,每个卡页之间内存地址相差512个字节。

实现方式

  • 一个卡页中包含多个对象,只要卡页中存在有一个或多个跨代指针,则卡页被标识为(dirty),称为元素变脏。

  • 在垃圾收集的时候只需要筛选出卡表中变脏的元素,然后添加到GC Roots中即可。

4.5、写屏障

前面讨论了如何 使用记忆集来减少GC ROOTS的扫描时间,这一节将如何维护记忆集(即决定那些数据何时变脏)

因为Java中即时编译的存在,一些赋值指令经过即时编译之后会直接变成机器指令来执行。所以需要在机器指令的层面来切入发生引用修改的语句。

可以看作是在虚拟机层面对“引用类型字段修改”的AOP切面,大多数收集器采用写后屏障

面临的问题

卡表在高并发的情况下会出现“伪共享”问题

解决

在将卡表变脏之前判断卡页是否 已经变脏了,不过这样就又会增加一次判断的开销。

HotSpot虚拟机需要手动开启对卡表更新的判断。两者各有性能损耗

五、经典垃圾收集器

大部分新生代收集器都是基于 标记-复制算法实现的

大部分老年代收集器都是基于 标记-整理算法实现的

5.1、Serial收集器

  • 最基础、历史最久的新生代垃圾收集器
  • 单线程收集器
  • 使用标记-复制算法进行回收
  • 收集时会暂停所有其他线程
  • 至今仍是客户端模式默认的新生代垃圾收集器

5.2、ParNew收集器

  • 新生代收集器
  • 收集时暂停所有线程
  • 使用**标记-复制算法**进行可达性分析
  • 多线程版本的Serial收集器,但实际上如果是单核的处理机环境上Serial收集器的效率更高,因为ParNew收集器还需要面临线程切换的开销
  • JDK1.7之前和CMS收集器(老年代)组合作为服务端模式下的解决方案。但随着G1的出现(面向全堆的垃圾收集器),ParNew和CMS将不再是服务端模式下的推荐解决方案
  • 现在ParNew只能和CMS搭配使用

5.3、Parall Scavenge 收集器

  • 新生代收集器

  • 使用标记-复制算法进行回收

  • 收集时暂停所有线程

  • 多线程收集

  • 与前面两个收集器主要关注如何缩短垃圾收集时用户线程的停顿时间不同,这个收集器主要关注吞吐量

  • 提供了两个参数用于精确控制吞吐量

    • -XX:MaxGCPauseMillis参数设置一个大于0的毫秒数,收集器将尽可能把垃圾收集时间控制在这个时间内(但相对的就会牺牲一些别的性能例如以空间换时间,减少了每次收集时间增加了收集次数)
    • -XX:GCTimeRatio参数的值设置一个正整数N,表示垃圾收集时间不超过运行时间的1/(1+N),默认N为99.
    • -XX:+UseAdaptiveSizePolicy是一个开关参数,开启后虚拟机会根据系统当前的收集性能信息,动态的调整参数以达到最大的吞吐量

5.4、Serial Old收集器

  • Serial收集器的老年代版
  • 单线程收集器
  • 收集时暂停所有线程
  • 使用标记-整理算法进行回收
  • 主要用途
    • 客户端模式下的HotSpot虚拟机老年代收集器
    • JDK5及之前与Parallel Scavenge收集器搭配使用
    • 作为CMS收集器发生失败后的后备预案

5.5、Parall Old收集器

  • Parall Scavenge收集器的老年代版本
  • 支持多线程并行收集
  • 与Parall Scavenge收集器组合作为吞吐量优先的解决方案

5.6、CMS收集器

  • 目标是缩短系统停顿时间
  • 用于老年代
  • 使用标记-清除算法

工作步骤

  1. 初始标记

    • 暂停用户线程,标记GC Roots直接关联的对象(时间较短)
  2. 并发标记
    • 用户线程并发执行,对GC Roots关联对象图遍历标记
    • 在并发标记时采用增量更新算法来避免用户线程与收集线程不干扰。
  3. 重新标记
    • 暂停用户线程,修正并发标记时期,用户线程导致的修改
  4. 并发清除
    • 用户线程并发执行,对标记死亡的对象进行清除

存在的缺点

  • CMS并发标记和清除时,因为需要使用CPU资源所以肯定会导致用户线程执行效率变慢,降低吞吐量

    在CPU核心数量不足4个时,CMS收集器工作时导致用户线程执行效率下降将十分明显

  • 无法处理浮动垃圾

    • 浮动垃圾:CMS在执行并发标记时,用户线程产生的新废弃对象CMS只能留到下一次执行收集时才能收集

    • CMS在并发标记时,因为用户线程也会并发执行,所以需要留一定的内存空间供用户线程使用。所以CMS不能像其他收集器一样等老年代填满了再开始收集,JDK6之后CMS默认阈值为92%

      -XX:CMSInitiatingOccu-pancyFraction的值可以修改CMS的触发比例

    • CMS的预留空间可能会出现不足,这种情况将启用Serial Old收集器重新收集老年代对象,当然这种情况就会使停顿时间变成。

  • 由于CMS收集器采用的是标记-清除算法,会带来内存空间碎片化问题从而导致Full GC的提前。

5.7、G1收集器(Garbage First)

  • 使用于服务端的垃圾收集器
  • 整体使用 标记-整理算法,局部为标记-复制算法
  • 全功能的收集器,替代了之前服务器端最常见的组合(ParNew+CMS收集器)
  • 建立可停顿预测模型,在N毫秒的时间内使得垃圾回收时间不超过M毫秒
  • 用户可指定期望的停顿时间,期望时间可以通过参数 -XX:MaxGCPauseMillis设置,默认值为200毫秒,推荐值在100-300毫秒之间,设置过低可能会导致回收空间不足提前触发Full GC。
  • 从G1开始,收集器每次收集不追求将空间中的垃圾全部清除,而是追求能匹配上分配器速度保持应用正常运行。
  • 与其他收集器不同,G1的收集对象不再是独立的新生代或者老年代。它可以面向堆中任何部分发起收集。其发起收集的衡量标准为哪个垃圾的数量和回收收益
  • Region内存布局是G1可以回收任何部分内存空间的关键
    • G1将堆内存划分为大小相同的多块空间
    • 每片region都可以扮演不同的分代角色(新生代中的Eden空间、Survivor空间,老年代)
    • Region中还有一类特殊区域Humongous区域,这片区域用来存储大对象的,默认这片区域为老年代
    • 收集器根据Region扮演的角色不同采取不同策略回收
    • 收集器追踪每个Region,为Region维护一个优先级列表,回收所需时间以及回收后获得的空间大小

G1垃圾收集器面临的问题

  • Region之间的跨代引用

    • 同样使用记忆集(哈希表-卡表)来实现
    • 与普通卡表不同,G1中的卡表是双向记录的
    • 缺点:内存占比增加,相当于堆内存的10%~20%
  • 并发标记阶段如何保证收集线程与用户线程不干扰

    • 采用原始快照算法实现
  • 如何建立可靠的可停顿预测模型

    • 为每个Region记录脏卡表数量等数据,计算衰减平均值–衰减平均值与平均值相比更反应最近的平均状态
    • 根据衰减平均值来判断期望时间内的收益

G1收集器运行流程(与CMS相似)

  • 初始标记

    短暂停顿用户线程,标记GC Root直接关联对象

    修改TAMS指针(Region中专门用于用户现场在并发清楚Region时需要分配对象)

  • 并发标记

    不需要停顿用户线程,与用户线程并发执行

    从GC Root开始并发分析可达性,标记需要回收的对象

    最后通过原始快照方式处理并发并发标记期间对象变更

  • 最终标记

    短暂停顿用户线程,处理并发标记期间遗留记录

  • 筛选回收

    与CMS的并发清除不同,筛选回收阶段也需要暂停用户线程,G1设计初衷也不是去追求低延迟

    统计各个Region的回收价值并排序,根据用户期望时间指定选择任意多个Region组成回收集进行回收

    回收过程:暂停用户线程,多个回收线程并行执行,将Region中存活对象复制到空的Region中,并清理整个旧Region

缺点

  • 记忆集(卡表)维护占内存,每一个Region都需要维护一张复杂的卡表
  • 内存分配不足时,会促发Full GC

六、低延迟垃圾收集器

与CMS、G1不同,低延迟处理器在整个回收过程中只有初始标记+最终标记阶段需要短暂暂停用户线程以外,其余阶段都是并发执行

追求更短的用户程序停顿时间,在低延迟下尽可能的不影响吞吐量

两种收集器,都可以将停顿时间限制在十毫秒以内

6.1、Shenandoah收集器

  • 这款收集器并不是Oracle主导研发的,在OracleJDK12中仍拒绝加入Shenandoah,这款收集器暂时只能在OpenJDK中使用
  • 与G1一样,也是将内存区域分为多个Region(包括存储大对象的Humongous Region)
  • 与G1不同的是
    • 在清除整理阶段也是与用户线程并发执行的
    • Region不分代
    • 不像G1一样为每个Region维护卡表,通过维护一张全局的 连接矩阵来实现。连接矩阵大致为一个二维数组,当Region M中有对象引用 Region N中对象时,二维表中 M-N位置标记

Shenandoah工作流程

三个重要阶段,并发标记-并发回收-并发引用更新

  • 初始标记

    与G1、CMS一样,需要暂停用户线程(暂停时间与堆大小无关),标记GC Root直接关联对象

  • 并发标记

    与G1、CMS一样,与用户线程并发执行(执行时间与堆大小相关),遍历整个对象图,标记存活对象

  • 最终标记

    与G1一样,需要暂停用户线程(时间极短),处理剩余的原始快照扫描,计算出Region回收价值并组成回收集

  • 并发清理

    与用户线程并发执行,只清理Region中无任何存活对象的Region。

  • 并发回收

    这个阶段是ShenandoahG1区别最大的地方,在回收阶段Shenandoah需要将Region中存活对象复制到新的Region中,但如何在与用户线程并发情况下移动对象也是最大的问题。

    通过读屏障+Brooks Pointers(转发指针)来实现并发回收

  • 初始引用更新

    有短暂的用户线程停顿,该阶段无具体操作,仅做回收线程的集合点,主要是确保所有回收线程都已经完成Region存活对象的复制。

  • 并发引用更新

    对存活对象进行引用更新,不需要像并发标记一样对全图扫描,只需要线性搜索出引用类型,将旧值修改为新值

  • 最终引用更新

    更新GC Roots中的引用,短暂停顿用户线程

  • 并发清理

    经过并发回收—引用更新后,回收集中的Region已经没有存活对象了,再一次调用并发清理即可完成回收。

并发回收实现细节

  • 在原有对象布局上增加一个转发指针,正常情况下转发指针指向自己,其作用类似句柄

    Shenandoah中同时使用了读屏障、写屏障而这两种操作都是极为麻烦的,尤其是读屏障其使用率极高,也就带来了性能影响。(优化:JDK13后Shenandoah将读屏障改为基于引用访问的屏障,这种屏障只拦截引用数据类型的访问,不会影响原生类型数据的访问)

  • 对于并发读操作来说没有问题,但如果是并发写操作需要确保写操作能正常落到复制后对象上。Shenandoah使用CAS来实现

缺点

  • 高运行负担下吞吐量下降

优点

  • 低延迟

6.2、ZGC收集器

一款处于实验状态的收集器,还没有商业化。

  • 同样也是基于Region布局的(没有设置分代,从优先顺序考虑,并不是分代不好)

  • 使用了读屏障染色指针内存多重映射来实现并发标记-整理算法

  • 与G1的Region不同,ZGC中的Region是动态创建和销毁的,其Region大小也是不同的

    • 小型Region:容量为2MB,每个对象大小小于256KB

    • 中型Region:容量为32MB,每个对象大小在256KB~4MB之间

    • 大型Region:容量为2MB*N,且其中只存放一个对象。

      大型Region总体容量最小可以只有4MB,所以大型Region不一定大于中型Region

  • 因为ZGC中没有设置分代,优点是运行负担小,缺点是分配速率会降低

如何实现并发整理算法

在Shenandoah中主要使用了转发指针+读屏障

而ZGC主要采用了一种名为 染色指针的技术

染色指针

ZGC将对象的一些标记状态直接放到对象的引用指针上,例如在64位的电脑中,取高四位用来存储四个标志信息

染色指针缺点

  • 染色指针不支持32位平台

  • 由于直接使用了指针中的四位,这也导致了染色指针内存收到了限制。

染色指针优点

  • 当Region中的存活对象被移走后,可以直接开始回收该Region,而不用等到将所有引用更新后才开始,其原因主要是染色指针具有自愈的特性
  • 染色指针可以大幅减少内存屏障的使用,提升效率

染色指针需要解决的问题

由于所有的程序最终都会转变为机器码由CPU执行,但是CPU并不能识别

需要解决虚拟机到操作系统之间重定义内存地址

解决:使用多重映射

工作流程

  • 并发标记

    • 和G1、Shenandoah收集器经历的初始标记、并发标记、最终标记一样。
    • 不同点是标记点在引用指针上
  • 并发预备重分配
    • 根据条件统计出本次收集需要清理哪些Region,并组成重分配集。
    • ZGC中没有记忆集,而是通过每次全局扫描Region来代替维护记忆集的开销
  • 并发重分配(核心阶段)
    • 将重分配集上的存活对象复制到新Region上,为重分配集上的每一个Region维护一张转发表
    • 用户线程在并发访问移动对象时,会被内存屏障拦截,然后根据转发表,找到移动后的内存地址。并且修正引用的值,下一次访问时将直接访问到移动后的位置,减少被内存屏障拦截的开销。这就是自愈
    • 因为染色指针的原因,ZGC的一个Region存活对象全部被移动后就可以直接进行收集,而不需要等到所有引用都更新才开始
  • 并发重映射
    • 这个阶段主要就是为了修正堆中还没有被自愈的指针
    • 但因为这个阶段优先级并不高,ZGC将这个阶段延后到下一次收集开始阶段(并发标记),在并发标记遍历图时,同时修正引用
    • 一旦所有引用修正完毕后,释放转发表

七、选择合适的收集器

  1. 如果是数据分析、科学计算类任务,吞吐量为主要关注的
  2. 如果是SLA应用,停顿时间是主要关注点
  3. 客户端应用、嵌入式应用,内存占用是主要关注点

总结

Serial收集器+Serial Old收集器用于客户端模式

ParNew收集器+CMS收集器用于服务端模式

Parall Scavenge收集器+Parall Old收集器用于服务端模式

现在推荐使用的收集器 G1收集器

未来趋势 Shenandoah收集器、ZGC收集器

垃圾收集器相关参数

参数 描述
UserSerialGC 虚拟机运行在Client模式下的默认值,开启后使用Serial+Serial Old的收集器组合进行内存回收
UseConcMarkSweepGC 使用ParNew+CMS+Serial Old组合进行回收,Serial Old作为CMS收集器回收失败的后备收集器
UseParallelGC JDK9之前虚拟机在Server模式下的默认值,使用Parallel收集器组合回收
SurvivorRatio 设置新生代中Eden区与Survivor区比值,默认8:1
PretenureSizePolicy 设置多大的对象直接在老年代中分配
MaxTenuringThreshold 新生代中的对象年龄超过这个值时进入老年代(默认值为15,新生代中的对象每过一次GC年龄+1)
HandlePromotionFailure 是否允许分配担保失败(新生代回收时,存活对象超过老年代的容量,即担保失败)
ParallelGCThreads 并行GC线程数

实战:内存分配与回收策略

对象的内存分配主要是在堆中(主要在新生代中,少部分大对象直接在老年代中分配),还有一部分对象经过即时编译后作为标量直接在栈上分配。

  • 对象优先在Eden分配

    大多数情况下对象都在新生代的Eden区分配,当Eden区空间不足时触发一次MinorGC。

  • 大对象直接进入老年代

  • 长期存活的对象将进入老年代(放在对象头里面的对象年龄计数器),每经历一次Minor GC计数器+1,当年龄达到阈值时进入老年代(默认为15)

  • 动态对象年龄判断:当SUrvivor空间中低于某年龄的对象超过一半,则将年龄大于该值的对象转到老年代中

  • 空间分配担保:每次Minor GC时会将Eden区和一个Survivor区中存

  • 活对象放到另外的Survivor区中,如果Survivor区容量不足以装存活对象则会将对象放到老年代中

JVM——垃圾收集器相关推荐

  1. 直通BAT必考题系列:7种JVM垃圾收集器特点,优劣势、及使用场景

    直通BAT之JVM系列 直通BAT必考题系列:JVM的4种垃圾回收算法.垃圾回收机制与总结 直通BAT必考题系列:深入详解JVM内存模型与JVM参数详细配置 今天继续JVM的垃圾回收器详解,如果说垃圾 ...

  2. JVM垃圾收集器(三)

    JVM垃圾收集器(三) 垃圾回收(GC)线程与应用线程保持相对独立,当系统需要执行垃圾回收任务时,先停止工作线程,然后命令GC线程工作,以串行模式工作的收集器,称为Serial Collector,即 ...

  3. 7种 JVM 垃圾收集器特点、优劣势及使用场景(多图)

    点击上方"IT牧场",选择"设为星标"技术干货每日送达! 一.常见垃圾收集器 现在常见的垃圾收集器有如下几种: 新生代收集器: Serial ParNew Pa ...

  4. JVM垃圾收集器——G1

    导航 引言 一.G1 介绍 1.1 适用场景 1.2 设计初衷 1.3 关注焦点 1.4 工作模式 1.5 堆的逻辑结构 1.6 主要收集目标 1.7 停顿预测模型 1.8 拷贝和压缩 1.9 与 C ...

  5. JVM优化系列-JVM垃圾收集器介绍

    导语   既然是串行顾名思义就是使用单线程的方式进行执行,每次执行回收的时候,串行回收器只有一个工作线程,这样对于并行能力较弱的计算机,串行回收器更多的独占线程专一执行的方面有着良好的实现,也就是说在 ...

  6. jvm垃圾收集器与内存分配策略

    2019独角兽企业重金招聘Python工程师标准>>> 垃圾收集器与内存分配策略: 以下参考周志明的<<深入理解jvm高级特性与最佳实践>>. 判断对象是否存 ...

  7. JVM 垃圾收集器(Garbage Collection)

    判断对象是否存活 在堆里边存放着java世界中几乎所有的对象实例,垃圾收集器在对堆进行回收前,首先需要确定这些对象之中哪些还"存活"着,哪些已经"死去"(即不可 ...

  8. JVM:垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 1.对象已死吗 1).引用计数算法 引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器为0的对象就 ...

  9. JVM 垃圾收集器 学习笔记(《深入理解java虚拟机》之六 垃圾收集)

    目录 新生代收集器 Serial收集器 ParNew收集器 Parallel Scavenge收集器 老年代收集器 Serial Old收集器 Parallel Old收集器 CMS收集器 Remov ...

  10. [JVM] 垃圾收集器与内存分配策略

    目录 简介 哪些内存需要回收 引用计数算法 可达性分析 再次谈引用 生存和死亡 简介 GC(Carbage Coolection),需要完成的3件事情 : 哪些内存需要回收 什么时候回收 如何回收 程 ...

最新文章

  1. Python yield 用法
  2. MetaPhlAn 2:宏基因组进化分析
  3. DOxygen for C++使用说明——添加数学公式
  4. 美团外卖批量投放智能安全头盔:骑手可语音处理订单
  5. 将Access数据库导入到SQLite最简单最实用的方法 -转
  6. CodeWithMosh--mysql 学习笔记(2)
  7. 数据集_汇总 | SLAM、重建、语义相关数据集大全
  8. js ajax 异步 同步 区别
  9. 主席树入门+博客推荐
  10. c语言程序设计谭浩强题库,谭浩强c语言程序设计习题答案
  11. 人脸检测识别相关数据集整理
  12. oracle 报错904,EXP-00008: 遇到 ORACLE 错误 904
  13. vue去除input在360兼容模式下删除图标
  14. Book04--修改软件的艺术:构建易维护代码的9条最佳实践
  15. 利用浏览器制作一款包含3D效果的演示文案
  16. 500万条微博数据来源分析
  17. 异构网络中基于元图的推荐——FMG
  18. Kubernetes:通过轻量化工具 kubespy 实时观察YAML资源变更
  19. 十五万左右纯电SUV怎么选?奇瑞大蚂蚁是真香
  20. 什么是商家转账到零钱

热门文章

  1. 蓝牙耳机哪个牌子性价比高?千元内真无线耳机推荐
  2. mybatis框架(1)
  3. error: possibly undefined macro: AC_PROG_LIBTOOL问题解决
  4. 电影功夫熊猫使用的单词分析
  5. Android Emoji表情开发。两端使同一套的图片,防止两端因为系统内置的Emoji表情不一样,而造成用户体验不一致!
  6. Java软件开发好学吗?学完好找工作吗?
  7. 如何判断一个点是否在空间三维物体内部
  8. mysql更新后变成科学记数,CONVERT函数解决
  9. Nginx搭建文件服务器实现文件上传
  10. nginx+ftp部署文件服务器