it编程 > 软件设计 > 算法

JVM GC垃圾回收算法使用及说明

21人参与 2025-10-15 算法

垃圾回收算法(gc algorithms)

jvm 根据对象生命周期特性(分代假设)采用不同的回收算法,核心算法包括:

标记-清除(mark-sweep)

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。

复制算法(copying)

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小, 同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

​​标记-整理(mark-compact)

此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

分代收集(generational collection)

可达性算法(reachability analysis)

可达性算法(​​reachability analysis​​)是 jvm 垃圾回收的核心机制,用于判断对象是否存活。

其核心思想是:​​从一组根对象(gc roots)出发,遍历所有引用链,未被引用的对象视为可回收垃圾​​。以下是其详细原理、流程和应用场景:

可达性算法的基本流程​

1.​​确定 gc roots​​

jvm 枚举所有根对象(如栈帧中的局部变量、静态变量等),作为遍历起点。

2.遍历引用链​​

从 gc roots 出发,递归遍历所有直接或间接引用的对象,形成​​对象图(object graph)​​。

3.​​标记存活对象​​

所有被遍历到的对象标记为存活(如使用三色标记法中的黑色或灰色状态)。

4.​​回收不可达对象​​

未被遍历到的对象(白色对象)视为垃圾,由具体 gc 算法回收(如标记-清除、复制等)。

gc roots 的具体类型​

以下对象被定义为 gc roots,不会被回收:

虚拟机栈中的引用​​

当前执行方法的局部变量、方法参数(如 public void foo(object param))。

当前线程的调用栈中所有方法的局部变量。

本地方法栈中的引用​​

jni(java native interface)方法中的对象引用(如通过 jnienv 调用的对象)。

方法区的静态变量和常量​​

类的静态变量(static 修饰)。

字符串常量池中的引用(如 string s = “abc”)。

同步锁持有的对象​​

通过 synchronized 关键字锁定的对象(如 synchronized(obj) 中的 obj)。

jvm 内部对象​​

系统类加载器(classloader)加载的类。

异常对象(如 outofmemoryerror)、线程对象等。

跨代引用记录对象​​

卡表(card table)中记录的老年代对新生代的引用(需特殊处理)。

引用类型对可达性的影响​

引用类型强引用(strong)软引用(soft)弱引用(weak)虚引用(phantom)
定义​默认引用(object obj = new object())内存不足时回收(softreference)下次 gc 必回收(weakreference)仅用于跟踪对象回收(phantomreference)
​​可达性影响​强可达软可达弱可达不可达
​​回收条件​不可达时回收内存不足时回收无论内存是否充足均回收对象回收后入队通知

示例:

object strongref = new object();          // 强引用
softreference<object> softref = new softreference<>(new object());
weakreference<object> weakref = new weakreference<>(new object());
phantomreference<object> phantomref = new phantomreference<>(new object(), new referencequeue<>());

三色标记(tri-color marking)

定义

用三种颜色抽象对象的状态:

​​白色(white)​​

​​灰色(gray)​​

​​黑色(black)​​

标记流程(基本步骤)​

​​初始阶段

标记阶段(并发)

从灰色队列中取出对象:

​​最终阶段

所有存活对象应为黑色,白色对象视为垃圾。

并发标记的两种问题​

因用户线程与 gc 线程并发运行,对象引用可能发生变化,导致两种风险:

漏标(对象丢失)

场景​​:

黑色对象(已标记完成)被用户线程写入了一个新的白色对象引用。

用户线程删除了灰色对象到某个白色对象的引用。

结果​​:

白色对象未被标记为存活,导致被错误回收。

例​​:

初始:黑(a) → 灰(b) → 白(c) → 白(d)
b 即将处理,但此时用户线程执行:
1. a.field = d   // 黑对象引用了白对象
2. b.field = null // 断开灰对象到c的引用
最终:c未标记(白色),d未标记(白色),会被误回收。

解决

1. 增量更新(incremental update)​​

​​2. 原始快照(satb, snapshot at the beginning)​​

错标(浮动垃圾)

场景​​:

对象实际已死亡,但在标记阶段被标记为存活。

解决:

​​可容忍​​:仅导致少量内存未及时释放,下次 gc 可清理。

oopmap(ordinary object pointer map)

oopmap 记录了栈上本地变量到堆上对象的引用关系。其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 reference 类型。如果发现某个位置确实存的是 reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈上的本地变量表里面只有一部分数据是 reference 类型的(它们是我们所需要的),那些非 reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。

一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 hotspot ,它使用一种叫做 oopmap 的数据结构来记录这类信息。

我们知道,一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 oopmap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 oopmap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( gc roots )。

oopmap(ordinary object pointer map)是 jvm 用于​​快速定位 gc roots​​ 的关键数据结构,通过记录栈帧和寄存器中的对象引用位置,显著减少垃圾回收时的停顿时间(stop-the-world, stw)。以下是其生成时机及作用机制的详细解析:

oopmap 的生成时机​

oopmap 的生成与 ​​jit 编译器​​ 和 ​​安全点(safe point)​​ 密切相关,主要发生在以下场景:

方法编译时(jit 阶段)​

​​即时编译(jit)​​:

当方法被 jit 编译器编译为本地机器码时,编译器会分析方法的栈帧布局,并生成对应的 oopmap。

​​记录内容​​:

栈帧中哪些位置(偏移量)存储了对象引用(oop,ordinary object pointer)。如:局部变量表、方法参数、this 指针等。

​​示例​​:

若方法的局部变量表第 3 个槽位是 object obj,则 oopmap 会记录该槽位的偏移量。

安全点(safe point)​

gc 仅是触发安全点的一种场景,其他操作(如偏向锁撤销)也会触发安全点。

​​并非所有 oopmap 都在 gc 前生成​​,但 gc 前必须依赖安全点更新 oopmap。

特定指令插入​

显式生成指令​​:jit 编译器会在生成的机器码中插入特殊指令(如 test 指令),用于检查是否需要进入安全点并生成 oopmap。

oopmap 如何协助 jvm 获取 gc roots​

gc roots 是垃圾回收的起点,包括​​栈帧中的局部变量、静态变量、jni 引用等​​。

oopmap 的作用是快速枚举这些根引用,避免全栈扫描。

快速定位引用位置​

结合安全点减少 stw 时间​

与卡表(card table)协作​

oopmap 与 gc 流程的协作​

以 ​​young gc​​ 为例,流程如下:

1.​​触发 gc​​:新生代空间不足,需回收。

​​2.进入安全点​​:所有用户线程暂停,生成 oopmap。

3.​​枚举 gc roots​​:

4.​​标记存活对象​​:从 gc roots 出发,标记所有可达对象。

5.​​恢复线程​​:完成 gc 后,线程继续执行。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

您想发表意见!!点此发布评论

推荐阅读

耳边的AI私人助理! 行业首发双低音算法荣耀Earbuds开放式耳机发布

07-03

全解析MeanShift传统目标跟踪算法

05-07

OpenCV图像形态学的实现

04-14

openCV中KNN算法的实现

04-14

nginx中调度算法的五种实现

03-18

京东有实体店吗?位置在哪里?

10-17

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论