JVM 垃圾收集

判断是否可回收

引用计数算法

对象中加一个计数器保存引用该对象的地方的个数

缺点是不能解决循环依赖的问题

可达性分析算法

面试官:你说你熟悉jvm?那你讲一下并发的可达性分析

从GC Roots依次遍历

根节点:

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

如果对象实现了finalize()方法,那么就会有一次避免死亡的机会,不过只有一次

如何判断⼀个类是⽆⽤的类?

  1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
  2. 加载该类的 ClassLoader 已经被回收。
  3. 该类对应的 java.lang.Class 对象没有在任何地⽅被引⽤,⽆法在任何地通过反射访问该类的⽅法。

回收方法区

回收废弃常量以及无用的类,判断无用的类的条件很苛刻

垃圾收集算法

标记-清除算法

标记可清除的内存,然后统一清除,会造成内存碎片

复制算法

将内存分为两个区域,每次只用其中一块,满了之后,将不被清除的移到另一块上,清除这一块

可能需要老年代的分配担保

标记-整理算法

将标记的内存移到一段再清除

回收时机

安全点

需要考虑的问题:

  1. 选定安全点:安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
  2. 停顿线程:如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都“跑”到最近的安全点上再停顿下来。

两种解决方案:

  1. 抢先式中断(Preemptive Suspension)
    抢先式中断不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
  2. 主动式中断(Voluntary Suspension)
    主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。

安全区域

用来处理程序“不执行”时中断

垃圾收集器

7 种 JVM 垃圾收集器,看完我跪了。。

https://gitee.com/hzm_pwj/FigureBed/raw/master/giteeImg/20201219120124.png

CMS收集器

图解 CMS 垃圾回收机制原理,-阿里面试题

初始标记: 暂停所有的其他线程,并记录下GC Roots可达以及新生代对象可达的对象,速度很快 ;

并发标记: 同时开启GC⽤户线程,⽤⼀个闭包结构去记录可达对象。但在这个阶段结束,这 个闭包结构并不能保证包含当前所有的可达对象。因为⽤户线程可能会不断的更新引⽤域,所以 GC线程⽆法保证可达性分析的实时性。所以这个算法⾥会跟踪记录这些发⽣引⽤更新的地⽅。

重新标记: 重新标记阶段就是为了修正并发标记期间因为⽤户程序继续运⾏⽽导致标记产⽣变动的那⼀部分对象的标记记录,这个阶段的停顿时间⼀般会⽐初始标记阶段的时间稍⻓,远远⽐并发标记阶段时间短。

并发清除: 开启⽤户线程,同时GC线程开始对为标记的区域做清扫。

https://gitee.com/hzm_pwj/FigureBed/raw/master/giteeImg/20210304125326.png

CMS 三色标记法

https://blog.csdn.net/RaymondCoder/article/details/106078984

https://blog.csdn.net/weixin_42008012/article/details/106483969

https://blog.csdn.net/qq_32099833/article/details/109558171

把对象在逻辑上分为三种颜色(表示标记的完成度):

黑色:自身和成员变量均已标记完成

灰色:自身被标记,成员变量未被标记

白色:未被标记的对象

标记的过程大致如下:

  • 刚开始,所有的对象都是白色,没有被访问。
  • 将GC Roots直接关联的对象置为灰色。
  • 遍历灰色对象的所有引用,灰色对象本身置为黑色,引用置为灰色。
  • 重复步骤3,直到没有灰色对象为止。
  • 结束时,黑色对象存活,白色对象回收。

因为是并发标记,也就是用户线程还在执行,所以会导致漏标以及错标的情况

https://gitee.com/hzm_pwj/FigureBed/raw/master/giteeImg/20210420115302.png

漏标

当黑色对象对灰色对象的引用断掉的时候,灰色对象后面的对象都可以回收了,但是还是因为不会对黑色对象进行扫描,所以会导致漏标的现象

https://gitee.com/hzm_pwj/FigureBed/raw/master/giteeImg/20210420115245.png

错标

当一个黑色对象和一个灰色对象同时指向同一个对象时,在标记过程中,如果灰色对象对白色对象的应用断掉,那么白色就永远不会遍历到(因为不会再次对黑色对象进行扫描)

G1收集器

G1垃圾收集器 - SegmentFault 思否

一起学习,G1垃圾回收算法 (juejin.cn)

Java Hotspot G1 GC的一些关键技术 - 美团技术团队 (meituan.com)

G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。

空间整合:与CMS的标记-清除算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC

划分规则

Region 划分

  • 新生代(Eden Region)
  • 年轻代(Survivor Region)
  • 老年代(Old Region)
  • 巨型对象(Humongous Region)
  • 未分配(Free Region)

对象划分的规则

  • 对象大小小于一半Region,直接存储到标记为Eden的Region
  • 对象大小大于一半Region但是小于一个Region,存储到标记为Humongous的Region中
  • 对象大小超过一个Region大小,存储到标记为Humongous的多个连续Region中

运作过程

相对于CMS,就是最后的回收部分不一样

初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短

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

最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行

筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

回收策略

young gc:活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

mixed gc:垃圾清除过程,如果发现一个Region中没有存活对象,则把该Region加入到空闲列表中。

full gc:如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc。

索引策略

每个Region都有一个关联的Remembered Set (RS),用于存储指向本区域的外部区域的引用。RS的数据结构其实是Hash Table,key是引用方Region的起始地址,value是一个集合,集合的元素是Card Table的index(索引)。简单来说,RS里面存在的是Region中Live Objects的指针。当Region中数据发生变化时,首先反映到Card Table中的一个或多个Card上,RS通过扫描内部的Card Table得知Region中内存使用情况和存活对象。

ZGC 垃圾回收器

新一代垃圾回收器ZGC的探索与实践 - 美团技术团队 (meituan.com)

Java 虚拟机原理 (六) ZGC 垃圾收集器 | 杜瓦尔 (duval.top)

ZGC是能自动感知NUMA架构并充分利用NUMA架构特性的,将对象尽量分配在访问速度比较快的地方。

回收过程

  • 标记阶段:从GC Roots集合开始,标记所有活跃对象。
    • 初始标记阶段:枚举所有栈帧OopMap保存的GC Roots堆上对象,该阶段是STW的。【GC Roots数量不多、耗时非常短】
    • 并发标记阶段:从GC Roots开始对堆中对象进行可达性分析,找出所有存活对象,该阶段是并发的。
    • 再次标记阶段:重新标记那些在并发标记阶段发生变化的对象,该阶段是STW的。【变化对象少、耗时非常短】
  • 复制阶段:把活跃对象复制到新的内存地址上,并指向对象旧地址的指针都要调整到对象新的地址上。
    • 复制准备阶段:选取需要复制的活跃对象范围。【Region与Region之间RSet日志必须完整】
    • 初始转移阶段:分配新内存,将 GC Roots上的活跃对象复制转移到新内存地址。
    • 转移阶段:分配新内存,把剩余活跃对象复制到新的内存地址上。
    • 重映射阶段:因为转移导致对象的地址发生了变化,在重映射阶段,所有指向对象旧地址的指针都要调整到对象新的地址上。

并发转移

通过着色指针和读屏障技术,解决了转移过程中准确访问对象的问题,实现了并发转移。

原理:并发转移中“并发”意味着GC线程在转移对象的过程中,应用线程也在不停地访问对象。假设对象发生转移,但对象地址未及时更新,那么应用线程可能访问到旧地址,从而造成错误。而在ZGC中,应用线程访问对象将触发“读屏障”,如果发现对象被移动了(通过着色指针进行判断),那么“读屏障”会把读出来的指针更新到对象的新地址上,这样应用线程始终访问的都是对象的新地址。

着色指针

当应用程序创建对象时,首先在堆空间申请一个虚拟地址,但该虚拟地址并不会映射到真正的物理地址。ZGC同时会为该对象在M0、M1和Remapped地址空间分别申请一个虚拟地址,且这三个虚拟地址对应同一个物理地址,但这三个空间在同一时间有且只有一个空间有效(相当于是同一时间只可看到一个)。

https://gitee.com/hzm_pwj/FigureBed/raw/master/giteeImg/202108151640030.png

  • **Marked0/marked1:**标记对象用于辅助GC
  • **Remapped:**标记对象是否已指向新的地址
  • **Finalizable:**此位与并发引用处理有关,它表示这个对象只能通过finalizer才能访问

读屏障

读屏障是JVM向应用代码插入一小段代码的技术。当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。

当应用线程从堆中读取对象引用时,就会执行这段代码。需要注意的是,仅“从堆中读取对象引用”才会触发这段代码。

Object o = obj.FieldA   // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o  // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i =  obj.FieldB  //无需加入屏障,因为不是对象引用

作用:ZGC 在并发转移的过程中会转移对象,并变更对象指针为 Remapped 空间指针。但此时应用程序中的指针还是 M0 或者 M1 空间指针,该指针也称为坏指针(bad pointer)。

如果应用线程或者GC线程访问到该坏指针并尝试从堆中读取指针背后的对象,则会尝试在读取之后通过读屏障进行指针修复,类似伪代码如下:

Object o = obj.fieldA; // Loading an object reference from heap
<load barrier needed here>
Object p = o; // No barrier, not a load from heap
o.doSomething(); // No barrier, not a load from heap
int i = obj.fieldB; // No barrier, not an object reference

如上,假定 fieldA 对象已经被转移,需要被重定向。那么,第一行代码其后会被插入一段读屏障代码,代码里所做的事情就是修正当前指针为 M0 或者 M1 指针:

Object o = obj.fieldA; // Loading an object reference from heap
if (!(o & good_bit_mask)) {
    if (o != null) {
        slow_path(register_for(o), address_of(obj.fieldA));
    }
}

这个修改过程大概有4%的执行开销。

处理过程

  • 初始化:ZGC初始化之后,整个内存空间的地址视图被设置为Remapped。程序正常运行,在内存中分配对象,满足一定条件后垃圾回收启动,此时进入标记阶段。
  • 并发标记阶段:第一次进入标记阶段时视图为M0,如果对象被GC标记线程或者应用线程访问过,那么就将对象的地址视图从Remapped调整为M0。所以,在标记阶段结束之后,对象的地址要么是M0视图,要么是Remapped。如果对象的地址是M0视图,那么说明对象是活跃的;如果对象的地址是Remapped视图,说明对象是不活跃的。
  • 并发转移阶段:标记结束后就进入转移阶段,此时地址视图再次被设置为Remapped。如果对象被GC转移线程或者应用线程访问过,那么就将对象的地址视图从M0调整为Remapped。

然后对 Remapped 的对象进行回收。

https://gitee.com/hzm_pwj/FigureBed/raw/master/giteeImg/20210401175021.png

卡表(Card Table)

使用场景:老年代的对象可能引用新生代的对象,那标记存活对象的时候,需要扫描老年代中的所有对象。

使用过程:将整个堆划分为一个个大小为512字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。

杂识

分代垃圾回收器中新生代和老年代

分代回收器有两个分区:老生代和新生代,新生代默认的空间占比总空间的 1/3,老生代的默认占比是 2/3。
新生代使用的是复制算法,新生代里有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1,它的执行流程如下:
把 Eden + From Survivor 存活的对象放入 To Survivor 区;
清空 Eden 和 From Survivor 分区;
From Survivor 和 To Survivor 分区交换,From Survivor 变 To Survivor,To Survivor 变 From Survivor。
每次在 From Survivor 到 To Survivor 移动时都存活的对象,年龄就 +1,当年龄到达 15(默认配置是 15)时,升级为老生代。大对象也会直接进入老生代。
老生代当空间占用到达某个值之后就会触发全局垃圾收回,一般使用标记整理的执行算法。以上这些循环往复就构成了整个分代垃圾回收的整体执行流程。

为什么新生代有两个Survivor分区?

https://blog.csdn.net/baidu_20608025/article/details/87936965

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值