深入理解Java虚拟机(第3版)学习笔记——垃圾收集器与内存分配策略(超详细)

关于垃圾收集的思考:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

对于一些线程安全的内存区域(程序计数器,虚拟机栈,本地方法栈)随着线程的产生而产生、消亡而消亡具有确定性,无需考虑这些区域的垃圾回收。

堆和方法区:接口的多个实现类的大小不确定,程序中创建了多少对象也不确定,这部分内存的分配和回收是动态的。

判断对象是否可回收的两种方法

引用计数算法

概念:在对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加一;当引用失效时,计数器就减一;在任何时刻计数器为零时就表示该对象已经是“垃圾对象”,可以进行回收。

问题:很难解决对象之间的相互引用问题。

jvm虚拟机并没有使用此方法来判断对象是否可回收。

可达性分析算法

基本思路:通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,就说明该对象为垃圾对象。

如下图,对象object 5、object 6、object 7虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。

image-20220718084426339

GC Roots对象

包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
  • 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

根据用户所选用的垃圾收集器以及当前回收的内存区域不 同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。

4中引用

在jdk1.2之前,对于对象只有引用和未被引用两种情况。如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用

在一些情况下无法满足要求,例如:譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象.

在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

  • 强引用:是普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。
  • 软引用:用来描述一些还有用,但非必须的对象。对于只被软引用关联的对象,在系统发生内存溢出异常前会对这些对象列入回收范围内进行第二次回收,如果第二次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
  • 弱引用:其强度比软引用更弱,只要垃圾收集器开始工作,这些对象无论内存是否足够都会被回收。在JDK 1.2版之后提供了WeakReference类来实现弱引用。
  • 虚引用:称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供 了PhantomReference类来实现虚引用

生存还是死亡?

可达性分析算法中判定为不可达的对象,也不是“非死不可”的,要真正宣告一个对象死亡,至少要经历两次标记过程:

  1. 对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记
  2. 随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为确有必要执行finalize()方法,那么会被放在一个F-Queue的队列中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize() 方法。
  3. 在被放入F-Queue队列中后,对象还存在一次自救的机会,稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,那在第二次标记时它将被移出“即将回收”的集合;否则就一定会被回收(finalize方法虚拟机只会执行一次)。

现如今,已经不推荐使用finalize()方法。

回收方法区

方法区的垃圾收集主要回收两部内容:废弃的常量和不再使用的类型。

回收废弃常量与回收 Java堆中的对象非常类似。如果一个字符串“java”曾经入池,但此时系统中没有任何一个字符串对象的值是“java”,即没有任何地方引用这个字面量,如果在这时发生内存回收,而且垃圾收集器判断确有必要的话,这个“java”常量就将会被系统清理出常量池(一般的垃圾收集器不会清除)。常量池中其他类(接 口)、方法、字段的符号引用也与此类似。

判断一个不再被使用的类,需要同时满足以下三个条件:

  1. 该类的所有对象实例已经被回收
  2. 加载该类的加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
  3. 该类对应的Class对象没有被任何地方使用,

关于是否要对类型进行回收,需要通过相应的虚拟机参数进行控制,也可以通过相应的虚拟机参数来查看类加载和卸载的信息。

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力

垃圾收集算法

分代收集理论

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消 亡。
  3. 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极 少数。

在新生代上建立一个全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用

此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指, 读者需按上下文区分到底是指老年代的收集还是整堆收集。
  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。

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

标记清除算法

概念:算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来。标记过程就是对象是否属于垃圾的判定过程(可达性分析算法进行标记)。

主要缺点:

  1. 效率不稳定:如果堆中有大量的对象,则会导致标记和清除两个过程的执行效率都随对象数量增长而降低。
  2. 内存空间的碎片化问题:标记、清除之后会产生大量不连续的内存碎片,大对象进入后可能会发现内存不足而频繁的触发垃圾收集动作

image-20220718095829117

标记复制算法(新生代使用较多)

概念:最初默认的是将内存区域划分为两块,每次只使用其中的一块,当发生垃圾收集时,将其中还存活的对象复制到另一块内存区域中去,再清除掉原来的那块内存区域。

image-20220718100511584

缺点:将可用内存缩小为了原来的一半,空间浪费未免太多了一 点

Appel回收:

由于新生代的每次回收会将大量的对象回收掉,Appel式回收将新生代分为较大的Eden空间(80%)和两块较小的Survior(10%、10%)空间。

发生垃圾搜集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的那块Survivor空间。当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。

优点:每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。

标记整理算法

概念:在标记阶段与“标记 清除”算法一样,但是后续是让所有存活的对象都向内存空间的一端移动,然后直接清理掉边界以外的内存。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dsrd14QI-1658641801821)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220718101553848.png)]

标记整理算法是移动式的算法。

Stop The World:如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动操作 必须全程暂停用户应用程序才能进行,这就更加让使用者不得不小心翼翼地权衡其弊端了,像这样的停顿被最初的虚拟机设计者形象地描述为“Stop The World” 。

总结

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

从垃圾收集的停顿时间来看,不移动对象停顿时间会更短

从整个程序的吞吐量来看,移动对象会更划算。

HotSpot的算法实现细节

根节点枚举

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

(枚举所有根节点)。

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

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

一旦类加载动作完成,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译过程中,也会在特定的位置记录下栈里和寄存器哪些位置是引用。这样收集器扫描的时候就直接得知了这些信息,而不用从GC Roots去扫描。

安全点

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

解决办法

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

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

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

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

安全区域

问题

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

解决办法

引入了安全区域的概念

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

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

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

  2. 线程离开安全区域

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

记忆集与卡表

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

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

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

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

实现原理

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

  • 字节精度

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

  • 对象精度

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

  • 卡精度

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

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

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

实现方式

  • 一个卡页中包含多个对象,只要卡页中存在有一个或多个跨代指针,则卡页被标识为(dirty),称为元素变脏
  • 在垃圾收集的时候只需要筛选出卡表中变脏的元素,然后添加到GC Roots中即可。

写屏障

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

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

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

面临的问题

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

解决

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

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

并发的可达性分析

按照是否访问过这个条件将可达性分析算法中遍历遇到的对象标记成以下三种颜色:

  • 白色:表示对象未被垃圾收集器访问过
  • 黑色:表示对象已经被垃圾收集器访问过,并且这个对象的所有引用都已经被扫描过了
  • 灰色:表示对象已经被垃圾收集器访问过,但至少还有一个引用没有被扫描

可达性分析过程可以看作为对象图上一股以灰色波峰的波纹从黑向白推进的过程。

用户线程如何与收集器并发工作呢?

出现两种问题:

  • 原来应该消亡的对象被标记为存活
  • 原来存活的对象被标记为消亡

当且仅当下面两个条件都满足时,会产生对象消失的问题:

  • 赋值器插入了一条从黑对象到白色对象的新引用
  • 赋值器删除了灰色对象到白色对象的直接或间接引用

增量更新破环的是第一个条件:当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次

原始快照破坏的是第二个条件:当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描 一次。简单来说,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svok7Z10-1658641801822)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220719191410386.png)]

CMS主要使用增量更新来做并发标记

G1、Shenandoah使用原始快照来做并发标记

经典垃圾收集器

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1LKboA9l-1658641801823)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220718102835682.png)]

Serial收集器(新生代)

单线程收集器:不仅仅是说明它只会使用一个处理器或收集线程去完成收集工作,“单线程”强调的是在进行垃圾收集时,必须暂停其它工作,直到它收集结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HVF2stNU-1658641801824)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220718103445498.png)]

概括

  • 最基础、历史最久的新生代垃圾收集器
  • 简单、高效,单线程收集器,回收时无需线程切换
  • 使用标记 复制算法进行回收
  • 收集时会暂停其它线程
  • 至今仍是客户端模式默认的新生代垃圾收集器

ParNew收集器

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SUvwMJke-1658641801825)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220718105059868.png)]

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

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

Parallel Scavenge收集器(注重吞吐量)

  • 新生代收集器

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

  • 收集时暂停所有线程(STW)

  • 多线程收集

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

    image-20220718110129271

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

    • 控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数:设置一个大于0的毫秒数,收集器将尽可能把垃圾收集时间控制在这个时间内(但相对的就会牺牲一些别的性能例如以空间换时间,减少了每次收集时间增加了收集次数)
    • 直接设置吞吐量大小的-XX:GCTimeRatio参数:设置一个正整数N,表示垃圾收集时间不超过运行时间的1/(1+N),默认N为99.
  • 存在自适应调节策略

    • -XX:+UseAdaptiveSizePolicy是一个开关参数,开启后虚拟机会根据系统当前的收集性能信息,动态的调整参数以达到最大的吞吐量

高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。

Serial Old收集器

  • Serial收集器的老年代版

  • 单线程收集器

  • 收集时暂停所有线程

  • 使用标记 整理算法进行回收

  • 主要用途

    • 客户端模式下的HotSpot虚拟机老年代收集器
    • JDK5及之前与Parallel Scavenge收集器搭配使用
    • 作为CMS收集器发生失败后的后备预案

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vkWsMakq-1658641801826)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220718111112666.png)]

Parall Old收集器

  • Parall Scavenge收集器的老年代版本

  • 支持多线程并行收集

  • 与Parall Scavenge收集器组合作为吞吐量优先的解决方案

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

    image-20220718140224794

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中分配对象)

TAMS指针就是把Region中的一部分空间划分出来用于并发回收过程中的新对象分配。)

  • 并发标记

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

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

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

  • 最终标记

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

  • 筛选回收

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tR7i1rf5-1658641801828)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220718140154531.png)]

缺点

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

低延迟垃圾收集器

衡量垃圾收集器的三项最重要的指标:

  • 内存占用
  • 吞吐量
  • 延迟

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3F3GCb6V-1658641801830)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220719083510852.png)]

Shenandoah和ZGC,几乎整个工作过程全 部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿。

Shenandoah收集器

目标是实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器

  • 非Oracle公司的虚拟机团队开发,所以OracleJDK 12 不支持Shenandoah,只有在OpenJDK中存在

  • Shenandoah不仅要进行并发的垃圾标记,还要并发地进行对象清理后的整理动作。

  • 与G1一样,也是将内存区域分为多个Region(包括存储大对象的Humongous Region)

  • 与G1不同的是

    • 在清除整理阶段也是与用户线程并发执行的

    • Region不分代

    • 不像G1一样为每个Region维护卡表,通过维护一张全局的 连接矩阵来实现。连接矩阵大致为一个二维数组,当Region M中有对象引用 Region N中对象时,二维表中 M-N位置标记

      image-20220719085023714

工作流程(九个阶段)

  • 初始标记(Initial Marking):与G1一样,首先标记与GC Roots直接关联的对象,这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关。

  • 并发标记(Concurrent Marking):与G1一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度

  • 最终标记(Final Marking):与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。

  • 并发清理(Concurrent Cleanup):这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。

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

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

  • 初始引用更新(Initial Update Reference):有短暂的用户线程停顿,该阶段无具体操作,仅做回收线程的集合点,主要是确保所有回收线程都已经完成Region存活对象的复制

引用更新:并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址。

  • 并发引用更新(Concurrent Update Reference):对存活对象进行引用更新,不需要像并发标记一样对全图扫描,只需要线性搜索出引用类型,将旧值修改为新值
  • 最终引用更新(Final Update Reference):更新GC Roots中的引用,短暂停顿用户线程
  • 并发清理(Concurrent Cleanup):经过并发回收—引用更新后,回收集中的Region已经没有存活对象了,再一次调用并发清理即可完成回收。

在这里插入图片描述

黄色的区域代表的是被选入回收集的Region,绿色部分就代表还存活的对象,蓝色就是用户线程可以用来分配对象的内存Region了

Brooks Pointers(转发指针)实现细节

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

    image-20220719093752125

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5tQo2mS2-1658641801834)(https://raw.githubusercontent.com/2keke8/MyPhoto/main/img/image-20220719094044645.png)]、

    对象产生副本后,只需要改变转发指针的值即可。

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

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

缺点

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

优点

  • 低延迟

ZGC收集器

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

  • 同样也是基于Region布局的(没有设置分代,从优先顺序考虑,并不是分代不好)
  • 使用了读屏障、染色指针、内存多重映射来实现并发标记-整理算法
  • 与G1的Region不同,ZGC中的Region是动态创建和销毁的,其Region大小也是不同的
    • 小型Region:容量为2MB,每个对象大小小于256KB
    • 中型Region:容量为32MB,每个对象大小在256KB~4MB之间
    • 大型Region:容量为2MB*N,且其中只存放一个对象。
    • 大型Region总体容量最小可以只有4MB,所以大型Region不一定大于中型Region
  • 因为ZGC中没有设置分代,优点是运行负担小,缺点是分配速率会降低

image-20220719101833973

如何实现并发整理算法

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

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

染色指针

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

image-20220719101846533

缺点

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

优点

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

染色指针需要解决的问题

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

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

解决:使用多重映射

工作流程

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

选择合适的收集器

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

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

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

垃圾收集器相关参数

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

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

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

  • 对象优先在Eden分配

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

  • 大对象直接进入老年代:通过-XX:PretenureSizePolicy参数设置

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

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

  • 空间分配担保:每次Minor GC时会将Eden区和一个Survivor区中存活对象放到另外的Survivor区中,如果Survivor区容量不足以装存活对象则会将对象放到老年代中。如果老年代的空间不足以存放新对象,则会首先进行Minor GC,如果进行后还不行,就会进行full GC。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值