59、可达性分析
在 C++ 语言中,当我们使用完一个对象之后,需要显式地调用 delete 语句进行内存释放,否则就会导致无用对象一直占用内存,而产生内存泄露
而在 Java 语言中,无用对象是由 JVM 负责自动回收的,无须程序员去显式释放,这样既提高了开发效率,又避免了在编程中因忘记释放内存而产生的内存泄露问题
// C++ 代码
void func() {
Student* stu = new Student();
// ...
delete stu; // 主动释放内存
}
// Java 代码
public void func() {
Student stu = new Student();
// ...
}
在 JVM 中,我们把无用对象的内存释放过程叫做垃圾回收,垃圾回收是 JVM 中非常重要的一项功能,涉及到的知识点很多,是面试中常考的内容
除此之外,在平时的开发中,当线上出现 OOM 或频繁 GC 等问题时,深刻理解 JVM 垃圾回收机制,也有利于快速解决问题
因此在接下来的几节中,我们详细讲解一下 JVM 垃圾回收机制
虚拟机在进行垃圾回收时,首先要判断哪些对象可以被回收,常用的判断算法为可达性分析
本节我们就来讲一讲可达性分析,以及其中涉及到的 STW、安全点、安全区等知识点
1、引用计数
实际上除了可达性分析之外,还有另外一种用来判定对象是否可以被回收的算法,叫做引用计数,只不过引用计数存在严重问题,大部分虚拟机都不会采用
但是作为对比,我们还是有必要讲一下的
在 Java 中,我们使用引用来表示变量和对象之间的关系,将对象赋值给变量就表示变量引用对象
- 如果有 N 个变量引用某个对象,那么这个对象的引用计数就是 N
- 如果引用这个对象的某个变量取消与这个对象之间的引用关系,比如:改为引用其他对象、赋值为 null、或生命周期结束,那么这个对象的引用计数便会减一
- 当这个对象的引用计数减少为 0 时,说明这个对象不再被任何变量引用,没有变量引用的对象是无法使用的,因此这个对象就可以被虚拟机当做垃圾回收了
// f() 函数结束, 变量 a 的生命周期结束, 对象的引用计数变为 0, 可以被垃圾回收
public void f() {
Object a = new Object(); // 对象的引用计数为 1
g(a);
// g(a) 退出之后, 变量 b 的生命周期结束, 对象的引用计数变为 1
}
public void g(Object a) {
Object b = a; // 对象的引用计数为 2
Object c = a; // 对象的引用计数为 3
Object d = a; // 对象的引用计数为 4
d = new Object(); // 变量 d 重新引用其他对象, 对象的引用计数减一, 变为 3
c = null; // 变量 c 赋值为 null, 对象的引用计数减一, 变为 2
}
不过引用计数存在一个严重问题,那就是无法检测循环依赖,如下所示
两个对象互相引用,尽管都已经被设置为 null,不再有变量使用它们,但是引用计数并不为 0,仍然无法被回收
这就是大部分虚拟机不使用引用计数作为判定对象是否应该被回收的原因
Wife hanmeimei = new Wife("HanMeiMei"); // Wife 对象的引用计数为 1
Husband lilei = new Hasband("LiLei"); // Husband 对象的引用计数为 1
hanmeimei.husband = lilei; // Husband 对象的引用计数为 2
lilei.wife = hanmeimei; // Wife 对象的引用计数为 2
hanmeimei = null; // Wife 对象的引用计数为 1
lilei = null; // Husband 对象的引用计数为 1
2、可达性分析
我们正式来看下可达性分析
- 我们把对象之间的引用关系用数据结构中的有向图来表示,图中的顶点表示对象
- 如果对象 A 中的变量引用了对象 B,那么我们便在对象 A 对应的顶点和对象 B 对应的顶点之间画一条有向边
- 在有向图中有一组特殊的顶点,叫做 GC Roots(Garbage Collection 垃圾回收)
GC Roots 为堆外变量所直接引用的堆内对象,包括:虚拟机栈、本地方法栈中的局部变量所直接引用的对象、方法区中静态变量所直接引用的对象等 - 虚拟机以 GC Roots 为起点,遍历(深度优先遍历或广度优先遍历)整个图
可以遍历到的对象为可达对象,也叫做存活对象
遍历不到的对象为不可达对象,也叫做死亡对象,死亡对象会被虚拟机当做垃圾回收
从上图我们可以发现,即便两个对象互相引用,但只要从 GC Roots 出发无法遍历到这两个对象,换句话说,从 GC Roots 到这两个对象不可达
那么这两个对象就会被判定为死亡对象,因此可达性分析可以解决引用计数存在的循环引用问题
3、STW
我们把运行在虚拟机上的应用程序启动的线程叫做用户线程,执行垃圾回收的线程叫做垃圾回收线程
如果虚拟机在执行垃圾回收的同时,用户线程仍然在执行,那么对象之间的引用关系有可能被用户线程中途更改,这就会导致可达性分析的结果存在误报或者漏报的情况
- 注意:这里的误报和漏报针对的都是 "存活对象",毕竟可达性分析过程遍历直接得到的是存活对象
关于误报和漏报产生的详细原因,我们在后面讲到三色标记算法时再详细讲解 - 误报:将非存活对象误报为存活对象
误报问题不大,只会导致本该被垃圾回收的对象没有被回收,等待再次垃圾回收即可 - 漏报:将存活对象漏报,漏报的存活对象会被判定为死亡对象
漏报会产生严重的问题,导致本不该被回收的对象被回收,这显然是无法接受的,会导致程序执行出错
为了解决漏报的问题,保证垃圾回收过程不被用户线程所打扰,最简单粗暴的解决方法是:在进行垃圾回收时,停止所有用户线程的执行,直到垃圾回收结束
我们把这个过程形象化地称为 Stop The World,简称为 STW
STW 的时间过长,会严重影响应用程序的性能,特别是对响应时间比较敏感的应用程序来说,因此优化垃圾回收过程,尽量减少 STW 的时间,是各个垃圾回收器努力的重点
关于如何减少 STW 的时间,我们在后续讲解 CMS、G1 等垃圾回收器时再详细讲解
4、安全点
前面提到:可达性分析遍历的起点是 GC Roots,那么 GC Roots 又是如何获得的呢?
比较简单的方法是:遍历栈中的局部变量和方法区中的静态变量,找出引用类型变量,然后再将引用类型变量所引用的对象放入 GC Roots 中
每次进行垃圾回收都要遍历栈和方法区来查找 GC Roots,效率非常低,因此虚拟机使用 OopMap 来存储当前的 GC Roots 并动态更新,具体的做法是
- 虚拟机先遍历查找一次 GC Roots 并初始化 OopMap
- 然后在代码的执行过程中,如果有变量更新所引用的对象,那么虚拟机就同步更新 OopMap
- 当虚拟机需要进行垃圾回收时,OopMap 中存储的便是当前的 GC Roots
对于解释执行来说,上述 OopMap 的更新过程没有任何问题,虚拟机每执行一行字节码,就同步更新一下 OopMap
但对于 JIT 编译执行来说,上述 OopMap 的更新过程却无法实现,这是因为
- JIT 编译之后的机器码直接交由 CPU 执行,并不经虚拟机之手,虚拟机无法边执行指令、边分析指令,然后再动态更新 OopMap
- 虚拟机需要在将字节码编译为机器码时,静态地分析指令,为每一条指令存储此指令执行结束后对应的 OopMap,但是这样做显然非常浪费内存空间
为了解决内存的浪费问题,虚拟机采用 "时间换空间" 的策略
- 为每个指令存储一个 OopMap,改为只选取部分指令存储 OopMap,这些被选取的指令被称为 "安全点"
- 当虚拟机启动垃圾回收并需要 STW 时,会向用户线程发送暂停的中断请求
此时用户线程并不能立刻停止,而是需要运行到安全点之后才能停止,这是因为只有安全点处才记录了 OopMap - 所有的线程都运行到安全点之后,虚拟机才能得到完整的 GC Roots
5、安全区
大部分情况下,用户线程在接收到暂停的中断请求之后,都可以在较短的时间内到达最近的安全点
但是在少数情况下,如果用户线程处于阻塞状态(比如等待 I / O 读写就绪),那么就无法在较短的时间内到达最近的安全点
为了解决这个问题,虚拟机引入了一个新的概念:安全区,不会改变对象引用关系的一段连续的代码区间
比如:线程阻塞等待 I / O 读写就绪,不会改变对象的引用关系,因此就属于安全区
当虚拟机执行垃圾回收并发起 STW 请求时
- 如果某个线程处于安全区,那么这个线程并不需要停止执行,而是可以跟垃圾回收线程并行执行
- 但是当用户线程离开安全区时,它需要检查虚拟机是否处于 STW 状态
如果是,那么用户线程需要阻塞等待 STW 结束,才能继续往下执行,以免用户线程跳出安全区之后,执行非安全代码导致对象引用关系的改变
6、课后思考题
局部变量、静态变量所引用的对象都是 GC Roots,那么类的非静态成员变量所引用的对象是不是 GC Roots 呢?为什么?
类的非静态成员变量所引用的对象,实际上是由类的实例对象所引用的,而实例对象是存储在堆内存中的
因此类的非静态成员变量所引用的对象并不属于 GC Roots,而是属于堆内存中的普通对象
可达性分析中的误报和漏报都是针对存活对象来讲的,误报和漏报为什么不针对死亡对象来讲呢?
在可达性分析过程中,遍历直接得到的对象是存活对象,因此误报和漏报也是针对存活对象来讲的
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17498611.html