JVM中CMS、G1GC、ZGC对比

HotSoprt是默认虚拟机

主要支持以下语言:

  1. Java:这是 JVM 最为常见和广泛支持的语言。
  2. Kotlin:一种在 Java 生态系统中越来越流行的静态类型编程语言,与 Java 具有良好的互操作性。
  3. Scala:一种融合了面向对象编程和函数式编程特点的语言。
  4. Groovy:一种动态类型的脚本语言,基于 Java 平台。
  5. Clojure:一种运行在 JVM 上的函数式编程语言。
运行时数据区

方法区

(多线程共享)

(JDK7前:方法区用永久代实现)

(JDK8开始:类变量+class对象,放置Java堆中)

虚拟机栈

(每个线程私有,互不影响)

本地方法栈

(每个线程私有,互不影响)

(多线程共享)

程序计数器

(每个线程私有,互不影响)

(记录正在执行的虚拟机字节码指令地址)

(如果正在执行本地方法此值为空)

GC种类

新生代垃圾回收

(Minor GC)

老年代垃圾回收

(Major or FUll GC)

永久代/元空间垃圾回收

(PermGen Gc or Metaspace GC)

针对新生代Eden+survivor
通常适用复制算法转移内存

针对老年代

一般用标记-清除/标记-压缩算法

元空间由操作系统负责,一般不会触发GC

永久代→元空间

永久代元空间
版本JDK8之前JDK8及之后
存储内容

类的元数据

运行时常量

方法区中的静态变量

类的元数据

运行时常量

方法区的静态变量

方法、字节码、常量池

主要区别

堆中

固定大小

本地内存,不在堆中,减少内存溢出风险
默认初始较小,会自己动态调整

内存配置

-XX:PermSize=64m(默认物理内存的1/64)

-XX:MaxPermSize=128m(默认物理内存的1/4)

-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

用的什么编译器?

JIT编译器C1编译快,优化低 适用客户端
C2编译满、优化高 适用服务器端。JDK11引入Graal与C1、C2共同适用
shark早期为mac OS X上执行,之后逐渐停止维护

如何指定GC算法启动参数?

示例: -XX:+UseParalleGC   或  -XX:UseG1GC

对象循环引用如何回收?

只要不被GCRoot关联引用即可,用的可达性分析算法,非引用计数算法。

GC日志打印区别?

JDK9之前:-XX:PrintGC -XX:PrintGCDetails

JDK9之后:-Xlog:gc -Xlog:gc*

什么是安全点?

所有线程安全暂停,可安全枚举所有根状态。

什么是浮动垃圾?

原本A引用B,但在A标记为黑色后,A对B的引用断开,没有指向B的引用后,B应该为白色区域,但已经标记灰色区域。这种情况,B第一次GC不会被回收,这种情况称为浮动垃圾。

如何避免像netty这类使堆外内存过大导致OOM?

设置堆外内存上限:通过JVM参数-XX:MaxDirectMemorySize设置堆外内存的上限,超过该值时触发Full GC。

对象的访问方式

句柄直接指针
示意图
好处reference中存储的是稳定的句柄地址,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不需要被修改速度更快,节省了一次指针定位需要的时间开销,由于JAVA对象访问十分频繁,这类开销积小成多后也是一项非常可观的执行成本。Sun HotSpot虚拟机使用的就是这种访问方式。
实现区别JAVA堆中将会划分出来一块内存作为句柄池,reference中就是存储了对象的句柄地址,而句柄中包含了对象实例数据类型数据各自的具体地址信息。相比较句柄的访问方式,JAVA堆中不会单独划分内存,reference中直接存储了对象地址,而对象中包含了对象类型数据的地址信息。
需随对象移动而变

回收算法

回收算法标记——清除标记——整理标记——复制
示意图
缺点对象太多时内存碎片化严重,不易找到连续空间,浪费内存因有整理步骤,消耗更多时间GC时占用双倍内存空间
优点相对更快准确式的垃圾回收方式,内存碎片少相对更快,结果准确,内存碎片少

算法对比

CMSG1GCZGC

JDK

选择

JDK6+
推荐JDK8

JDK9+
推荐JDK11

JDK15+
推荐JKD21

GC

Root

扫描内容

  1. JVM栈中的引用
  2. 静态引用
  3. JNI引用
  4. 被同步锁引用
  5. JVM内部系统级引用
GC流程
  1. 初始标记(STW)
  2. 并发标记
  3. 并发预清理(可选步骤,默认开启)
  4. 可中断的并发预清理(可选步骤)
  5. 重新标记(STW)
  6. 并发清除
  7. 并发重置(可选步骤)
  1. 初始标记(STW)
  2. 并发标记
  3. 最终标记(STW)
  4. 存活对象计算
  5. 收尾工作(STW)
  1. 初始标记(STW)
  2. 并发标记
  3. 再标记(STW)最多1ms
  4. 并发转移准备
  5. 初始转移(STW)
  6. 并发转移
初始标记仅遍历被 GC-Roots +  新生代对象     直接引用   的老年代对象,所以很快只需要扫描所有GC Roots,STW时间与GC Roots成正比,一般很短
并发标记对 GC-Roots + 新生代对象    间接引用的    老年代对象进行标记遍历堆中所有对象
重新/最终标记
  1. 遍历被 GC-Roots +  新生代对象     直接引用   的老年代对象(因前面的预清理已经处理了部分,此时工作量变小)
  2. 初始标记后,利用“卡表(索引列表)”、增量更新、原始快照、染色指针、写屏障等记录引用变化的对象,从而重新标记
GC目标
  1. 低停顿
  2. 并发执行
  3. 空间利用率高
  4. 适用于内存有限的场景
  1. 可预测STW时间(方差+标准差)控制STW时间,不追求完全回收
  2. 分区回收,优先回收高价值区域
  3. 标记-复制算法相对于cms的标记-清除减少内存碎片
  4. 6-8G优势明显
  5. 适用于高吞吐量同时,也要控制STW的场景
  1. 将STW时间控制在10ms之内
  2. 停顿时间不会随对象增多而增加
  3. 支持8M~4TB级别的堆
  4. 染色指针将内存空间映射成了3个虚拟地址
  5. 适用绝对低延迟需求
标记算法特点

增量更新

(将黑色对象重新标记为灰色)

(不是一种存储方式,而是一种策略)

原始快照(SATB)

(不需要在重新标记阶段再深度扫描被删除引用对象,但可能造成更多浮动垃圾)

染色指针

(指针存储,43~46位为染色信息,管理内存最大2^42=4TB [18位空闲][是否只能finalize()访问][2][1][0][存储的内存地址])

内存多重映射

(多个虚拟机地址,映射到某个物理地址)

自愈能力

(通过1.并发标记2.并发预备重分配3.并发重分配(“自愈”)4.并发重映射)

ZGC为何要设计m0和m1

m0、m1和remapped 的变化过程如下:

一、初始状态

在 ZGC 启动时,m0 和 m1 都处于未使用状态,remapped 也为初始状态值(通常为 0)。

二、垃圾收集开始

  1. 当进行第一次垃圾收集时,假设选择 m0 作为初始的“from”空间,此时 m0 开始被填充对象。m1 则作为“to”空间等待接收从 m0 转移过来的存活对象。remapped 仍为 0,因为还没有进行对象的重新映射。
  2. 随着程序的运行,垃圾收集器开始识别存活对象。存活的对象会被复制到 m1 中,这个过程中,会对复制后的对象进行地址更新,以便后续能够正确访问。

三、转移过程中

  1. 在对象从 m0 转移到 m1 的过程中,m0 的空间逐渐被释放,m1 不断被填充。当转移完成后,m0 大部分空间被清空,m1 则包含了存活的对象。
  2. 此时,remapped 的值开始发生变化。对于已经成功转移到 m1 的对象,其对应的 remapped 标志会被设置为 1,表示这些对象已经被重新映射到新的位置(m1)。

四、下一次垃圾收集

  1. 当下一次垃圾收集开始时,m0 和 m1 的角色互换。m1 变为“from”空间,m0 变为“to”空间。
  2. 同样,存活对象从 m1 转移到 m0,这个过程中 remapped 的值会再次根据对象的转移和重新映射情况进行更新。

新生代:标记-复制(不直接参与GC)

老年代:标记-整理(主要针对老年代进行GC)

大部分region一样大

新生代:标记-复制(参与GC)

老年代:标记-整理(占用堆空间45%可能触发参与GC)

标记-复制

动态创建删除region

有大中小三种region分别存放对象

(小256KB~2M,中256KB~4M,大4M~2M的倍数)

对象转移

转移是原子性保的,不存在转移时对象内容被更改

但对象引用可能更改,这时候ZGC利用读屏障来让用户获取正确的对象

三色标记

从GC Root开始遍历,记录在对象头中

黑色:已标记,引用都已标记处理

灰色:已标记,还有引用没扫描处理

白色:未标记,不可达

漏标问题原本A→B,B→C,在A被标记为黑色后,A建立→C的引用,且B→C的引用断开。此时A已经被标记为黑色,则不会再遍历A的引用,且B→C的引用已经断开。这样会造成C未被标记,被垃圾回收,造成空指针问题。
漏标解决

重新扫描(Remark)阶段:

  • 在 CMS 的重新扫描阶段,会再次检查某些对象,尤其是在并发标记过程中可能发生变化的对象。如果发现了 A 建立对 C 的引用这个变化,会将 C 标记为灰色。灰色表示对象已被发现但它的引用还未被完全追踪。
  • 对于 A,由于已经是黑色,颜色保持不变。
  • 对于 B,如果在断开引用后没有其他变化,其颜色保持不变。但如果在重新扫描过程中发现 B 还有其他引用关系变化,可能会根据具体情况调整颜色。

Remembered Set(记忆集)和 Card Table(卡表)的作用

  1. 当 A 被标记为黑色后,虽然不会再直接遍历 A 的引用,但是由于 G1 使用记忆集和卡表来记录跨区域的引用关系,当 A 建立对 C 的引用时,会更新相关区域的记忆集和卡表信息。
  2. 当 B→C 的引用断开时,同样会在记忆集和卡表中反映出这个变化。
  3. 在并发标记阶段,G1 会不断扫描和标记对象。如果通过记忆集和卡表发现了新的引用关系(A→C),并且 C 尚未被标记,那么会将 C 标记为灰色。灰色表示对象本身已被发现,但它的引用还未被完全追踪。
  4. 对于 A,由于已经是黑色,颜色保持不变。黑色表示对象及其引用的对象都已被标记。
  5. 对于 B,当 B→C 的引用断开后,如果 B 在之前的标记过程中没有新的引用关系产生,并且没有其他因素影响其颜色变化,那么 B 的颜色也保持不变。如果在后续的处理中有其他情况出现,可能会根据具体情况调整颜色

当 A 建立对 C 的引用时,读屏障会被触发。读屏障会检查对象的颜色状态,如果发现新的引用关系且目标对象(C)可能未被正确标记,会进行相应处理。

  1. 如果 A 是黑色且建立了对 C 的引用,读屏障会将 C 置为灰色。这样确保 C 会在后续的标记过程中被进一步处理,防止漏标。
  2. 对于 A,由于已经是黑色,在这个过程中颜色保持不变。
  3. 对于 B,当 B→C 的引用断开后,如果 B 在之前的标记过程中没有新的引用关系产生,并且没有其他因素影响其颜色变化,那么 B 的颜色也保持不变。如果在后续的处理中有其他情况出现,可能会根据具体情况调整颜色。
获取对象状态

每次GC扫描所有区域

对象存活信息在对象头中

每次GC扫描所有Region

对象存活信息在对象头中

对象存活信息记录在染色指针上42~45位

存在寄存器中,比内存更快

并发转移+标记阶段都用到:

读写屏障

只有一份:老年代→新生代 的引用

写后屏障:维护并发标记的正确性、无浮动垃圾

实现方式:基于增量更新

因需要多张表,内存多占至少20%

写前屏障

写后屏障

实现方式:基于SATB

读屏障:从队中读取对象引用时使用,在对象标记和转移过程中,用于确定对象引用地址是否满足条件,从而做出不同逻辑。
空间分布
  • 新生代(默认:新生代1:老年代3)
  • 老年代
  • 永久代-XX:MaxPermSize(JDK8开始使用元空间代替-XX:MaxMetaspaceSize=-1 默认-1表示无限制。且不会再有溢出、因为字符串常量存在的堆中)
  • Eden空间(默认:Eden8:From Survivor1:TO survivor1)
  • From survivor空间
  • To survivor空间(Survivor默认经历15次后进入老年代)
  • 方法区:运行时常量
  • 永久代

堆空间:由无数Region组成,每个region内也分新生代、老年代

  • 元空间
  • 完全新生代
  • 部分新生代
  • 老年代:并非每次GC都进入检测(当老年代空间不够用,会FullGC)

无严格意义的Eden、新生代、老年代等划分

  • 也由无数Rgion组成,但大小、管理回收策略与G1不同
  • 采用了一种基于页面(Page)的内存管理方式,其内存划分相对更加动态和灵活


图例来源于网络

主要参考书籍:

《深入理解java虚拟机》第3版 周志明 著

《深入Java虚拟机》——JVM G1GC的算法与实现 [日]中村成洋 著

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值