zl程序教程

您现在的位置是:首页 >  后端

当前栏目

JVM 学习笔记(2):垃圾回收GC

JVM笔记学习 垃圾 回收 GC
2023-06-13 09:12:58 时间

1、对象存活判断算法

1)引用计数算法

在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。

这个算法原理简单,判断效率也高,但如果出现对象之间的相互引用(循环引用),那么相应对象的引用计数器将永不为0,也就无法被回收掉,即使他们已经没有存活的意义。

2)可达性分析算法

GC Root 也称为根对象,代表的是当前时刻仍然存活着的对象,可作为根对象的主要有以下几类:

① 在虚拟机栈中引用的对象,例如参数、局部变量、临时变量等;

② 在方法区中类静态属性(static 属性)引用的对象,例如 Java 类的引用类型静态变量;

③ 在方法区中常量引用的对象,例如字符串常量池(StringTable)里的引用("ab","a"等等);

④ 在本地方法栈中Native方法引用的对象;

⑤ JVM 内部引用对象,例如一些基本数据类型对应的对象,一些异常类对象等;

⑥ 所有被同步锁( synchronized ) 持有的对象;

⑦ 反映Java虚拟机内部情况的 JMXBean 、 JVMTI 中注册的回调、本地代码缓存等

除了这些还可以有其他对象临时性地加入其中,构成一个 GC Roots 集合。

从 GC Root(根对象)出发,走过的路径叫做引用链,在引用链上的对象将不会被回收(有关联关系),而其他从根对象出发到达不了的对象将被回收掉。这也是 JVM 所采用的算法。

2、引用分类

在 JDK1.2 之前对象只有被引用以及未被引用这两种状态,而诸如缓存数据这些想当内存足够时存放,内存不足时释放就没办法了,因此 JDK1.2 对引用的概念进行扩充,细分为四种引用类型。

1)强引用

强引用即最朴素的引用赋值, 诸如 Object obj = new Object() 这种,只要对象的引用关系存在,就永远不会被垃圾回收掉;

2)软引用

软引用指的是那些还有用,但是非必须的对象,在内存充裕时不会回收,在内存抛出溢出异常前会对这些对象进行回收,如果回收后仍不足再抛异常。可以使用 SoftReference 类来实现软引用;

3)弱引用

弱引用指的也是非必须的对象,被弱引用关联的对象只能存活到下一次垃圾回收,当垃圾回收器工作时,无论内存是否充裕都会把这些对象回收掉。可以使用 WeakReference 类来实现弱引用;

4)虚引用

一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。当虚引用关联的对象被垃圾回收时,会收到一个系统通知,像之前的 Cleaner 对象关联着 ByteBuffer 对象,当 ByteBuffer 对象被回收掉时会收到系统通知触发执行直接内存的回收;

这四种引用类型的强度依次递减,虚引用是最弱的一种引用关系。

3、finalize

在进行可达性分析时,如果发现对象没有在引用链上将会做第一次标记,随后筛选该对象是否有必要执行 finalize() 方法,如果对象没有重写该方法或者该方法已被执行过一次,那么将视作“没有必要执行”。

当对象被判定为确有必要执行 finalize() 方法时,将会被放置在 F-Queue 队列之中,并由一个低优先级的 Finalizer 线程去执行该方法,JVM 并不保证执行成功率,这样可以避免超时导致的内存回收子系统崩溃。

在这之后收集器将对 F-Queue 中的对象进行第二次小规模的标记,如果对象通过 finalize() 方法的执行重新与引用链上的任何一个对象建立关联,那在第二次标记时将被移出“即将回收”的集合,否则将会被真正回收。

上面讲到的 finalize() 方法实际上是对 C/CPP 中析构函数的妥协,并不完全等同,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐使用

4、分代收集

三大假说:

弱分代假说:绝大多数对象都是朝生夕灭的;

强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡;

跨代引用假说:跨代引用相对于同代引用来说仅占极少数;

基于这几个理论假说,诞生了分代收集的理论/经验法则。比起在一大块空间中穿插着回收内存进而造成内存碎片,把对象按照存活时间进行划分有助于一次性回收较大内存空间,兼顾了垃圾回收的时间以及内存的有效利用。

HotSpot 中把堆区域划分为新生代和老年代,每次垃圾收集时新生代中的对象大都死亡被回收,只有少部分存活的会晋升到老年代中

为了解决跨代引用的问题,在新生代上会建立一个全局的数据结构(记忆集),把老年代分成若干小块,并标识出哪一块存在跨代引用,那么在垃圾收集时我们只需要把这一部分的对象加入 GC Root 去进行扫描即可,无需扫描整个老年代

5、垃圾收集行为

1)Partitional GC:部分收集

分为以下三种:

Minor GC:针对新生代的收集;

Major GC:针对老年代的收集,目前只有 CMS 收集器会有单独收集老年代的行为;

Mixed GC:新生代老年代混合收集,目前只有 G1 收集器有这种行为;

2) Full GC:整堆收集

针对整个 Java 堆和方法区的收集。收集效率较低,因此不建议手动调用 System.gc() 进行 Full GC。

6、垃圾收集算法

1)标记-清除算法

该算法分为两个阶段:标记清除。标记阶段则是垃圾的判断阶段,而标记完成之后会统一回收掉所有被标记的对象。这是最基础的算法,后续的算法都是基于它的改进。需要注意的是,该算法执行效率不稳定,如果需要回收的对象有很多则需要多次标记清除;如果清除的不是连续的内存空间,则会造成严重的内存碎片化问题

2)标记-复制算法

在提及该算法之前需要先介绍一下半区复制算法,其指的是将内存空间分为两块大小相等的区域,如下图,每次只使用其中一块(假设只用A),那么将 A 中还存活的对象转移至 B 中,之后清除掉 A 的全部空间。这样可以避免空间的碎片化,但是内存的可用空间会直接折半。

半区复制

现代商用 JVM 大都采用该算法进行新生代的垃圾回收,但 HotSpot 的新生代垃圾收集器并不是采用半区复制的算法,而是采用 Appel式回收 :将新生代划分为 Eden区 和两个 Survivor区 ,比例为 8:1:1,每次分配内存时只使用 Eden 和其中一块 Survivor ,当垃圾收集发生时则将这两块区域中存活的对象复制进另一块 Survivor 区 ,接着清空这两块区域。相当于存放存活对象的空间只有 10%,当存活对象超过 10%时,会依赖其他内存区域(大多是老年代)进行分配担保

3)标记-整理算法

从上面我们也可以发现,复制算法需要有一块内存区域进行分配担保,因此对于老年代来说不会采用这种垃圾收集算法,而是先将可回收对象标记,接着让存活对象往一侧移动,直接清理掉边界以外的内存,这就是标记-整理算法。

这种算法的优点是可以避免空间的碎片化,但老年代中每次回收可能都有大量存活对象区域,那么移动他们并更新对象引用是很繁重的工作,同时移动对象时必须暂停用户进程,这也称为 “Stop The World”。这里需要注意,标记-清除算法也会触发 STW ,只是停顿时间相对较短。

7、GC 相关参数

GC相关参数

8、垃圾收集器

1)分类

垃圾收集器分类

2)Serial

这是一款最基础的垃圾收集器,它是单线程的,并且在进行垃圾收集工作时会停掉其他的工作线程,直到收集完毕。虽然它会导致工作线程暂时停止运行,但至今依然是 HotSpot 运行在客户端模式下的默认新生代收集器,因为它是单线程收集器中最简单且高效的,适用于核心少、内存少的环境。

3)ParNew

这实质上是 Serial 收集器的多线程并行版本,并且除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作,但是随着 G1 的出现,从 JDK9 之后这个组合不再是官方推荐的服务端模式下的收集器解决方案了。在多核心多线程的环境下,它可以比 Serial 发挥更强的作用,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。

4)Parallel Scavenge

这是一款新生代垃圾收集器,基于标记-复制算法,可以并行地垃圾收集。它的目标是达到一个可控制的吞吐量(处理器用于运行用户代码的时间与处理器总消耗时间的比值),高吞吐量可以最高效率地利用处理器资源,适合在后台运算的分析任务。

设置参数 -XX:+UseAdaptiveSizePolicy 之后可以让虚拟机根据系统运行情况动态调整新生代大小等参数,也称为 **垃圾收集的自适应的调节策略(GC Ergonomics)**。

5)Serial Old

这是 Serial 收集器的老年代版本,是一个单线程收集器,使用标记-整理算法,主要是供客户端模式下使用。在服务端模式下,主要是作为 CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure 时使用。

6)Parallel Old

这是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现,在注重吞吐量或者处理器资源较为稀缺的场合,可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器这个组合。

7)CMS(Concurrent Mark Sweep)

这是一种响应时间优先的垃圾收集器,其基于标记-清除算法实现,整个过程分为四个步骤:

初始标记:标记 GC Root 直接关联的对象,用时较短,会造成 STW ;

并发标记:从 GC Root 的直接关联对象出发,遍历整个对象图做标记,耗时较长,但可以与用户线程同时运行;

重新标记:在上一阶段并发标记时,可能有用户线程运行导致对象标记变动,因此这里需要重新标记,耗时较长,但仍比 ② 短;

并发清除:对上面几个阶段标记好的可回收对象进行并发地回收,可以与用户线程同时运行。

虽然这可以看作是一款并发收集、低停顿的收集器,但它有以下三个明显缺点:

吞吐量低:由于在并发阶段会占据一定的线程,因此会导致程序变慢,CPU 利用率不高;

无法处理浮动垃圾:在并发阶段,用户线程产生的垃圾成为浮动垃圾,在 CMS 中只能留待下一次垃圾收集再进行清理,由于浮动垃圾的存在,需要预留出一部分内存,意味着 CMS 不能等待老年代快满的时候再回收,而是应该设置一个百分比阈值,如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS;

空间碎片化程度高:由于使用标记-清除算法,不可避免的会有空间的碎片化产生。

8)G1(Garbage First)

G1 是一款面向服务端应用的支持停顿时间模型兼顾高吞吐量的垃圾收集器。G1 开创了基于 Region 的堆内存布局,把 Java 堆内存分为多个大小相等的 Region ,这些 Region 可以根据需要被当做 Eden、Survivor、老年代空间 其中一种,因此垃圾收集不再是严格依赖分代的收集,而是哪一块区域垃圾回收效益高就回收哪一块,这也是前面提到的 Mixed GC 模式。

在 G1 中保留着新生代和老年代的概念,但是他们不再是物理隔离的,而是一系列区域的动态集合,并且垃圾收集的最小单元为 Region ,这可以避免对不必要的区域进行垃圾收集。同时 G1 会追踪每一块 Region 区域的回收内存大小以及收集时间作为其衡量标准,根据这个标准去把 Region 存入优先级列表中,每次从里面取出优先级最高的区域进行垃圾收集。用户可以使用 -XX:MaxGCPauseMillis 参数来设置期望的垃圾收集停顿时间,G1 会根据这个期望值来调整垃圾收集中 Region 的顺序。

这里我们需要关注前面提到的跨代引用的问题,G1 会存在跨 Region 引用的情况,因此 G1 使用记忆集避免全堆扫描。G1 中每个 Region 都有一个 Remembered Set(记忆集),用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。与此同时 G1 也会比其他垃圾回收器具有更大的内存占用

如果不计算用写屏障维护记忆集的操作,G1 可以分成四个过程:

初始标记:主要是标记 GC Root 关联的对象,会造成 STW 但耗时较短;

并发标记:通过上面标记出的对象遍历整个对象图去找出所有可达对象,这个过程耗时较长,但可以与用户线程并发执行;

最终标记:处理上面并发过程后遗留下的 SATB 记录,此过程会暂停用户线程,造成 STW ;

筛选回收:按 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,之后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 。此过程必须暂停用户线程,由多条收集器线程并行完成的。

上面提到的 SATB 可以理解成在 GC 开始之前对堆内存里的对象做一次快照,此时活的对象在当次垃圾收集中不会被划入其中。同时在 Region 中会设置两个 TAMS 指针,这两个指针以上的位置的对象默认是存活的,并且并发过程中新分配的对象只能存入这一块区域,后续最终标记时会对这块区域做最后的处理。

G1 垃圾收集器的特点:

① 整体看基本采用 标记-整理 算法,但涉及两个 Region 区域之间的收集又是 标记-复制 算法,这两种算法的使用不会导致空间碎片化的发生,并且收集后可以提供规整的可用内存,有利于程序的长时间运行。

② G1 在内存占用以及额外执行负载方面都要比 CMS 要高,内存占用问题是因为 G1 在每个 Region 中都要维护一个记忆集,这会占用掉一部分的空间;额外执行负载问题是因为 G1 除了使用写后屏障来维护卡表之外,还需要使用写前屏障来跟踪并发时的指针变化情况。

参考资料:

•《深入理解Java虚拟机(第三版)》——周志明[1]•黑马 JVM[2]•[Java 虚拟机 | CS-Notes](http://www.cyc2018.xyz/Java/Java 虚拟机.html)•JavaGuide--jvm (gitee.io)[3]

相关链接

[1] 《深入理解Java虚拟机(第三版)》——周志明: https://book.douban.com/subject/34907497/ [2] 黑马 JVM: https://www.bilibili.com/video/BV1yE411Z7AP [3] JavaGuide--jvm (gitee.io): https://snailclimb.gitee.io/javaguide-interview/#/./docs/b-4jvm