【JVM】垃圾回收机制(GC)之引用计数和可达性分析

1. 引用计数

这种思想方法,并没有在 JVM 中使用,但是广泛应用于其他主流语言的垃圾回收机制中(PythonPHP)。

《深入理解 Java 虚拟机》中谈到了引用计数,就导致有些面试官还是会问

给每个对象安排一个额外的空间,空间里要保存当前这个对象有几个引用

java 复制代码
Test a = new Test();
Test b = a;
a = null;
b = null;
  • new 出对象的时候,就在堆上开辟了一块空间,并且在前面额外有一块空间用来存储引用计数
  • 当把对象的地址给到栈上的局部变量的时候,这个引用就指向了这个对象,引用计数就变成了 1
  • 当引用 b 同样指向这个对象的时候,引用计数就变成了 2
  • 当引用 a 的值由对象的地址变为 null 的时候,a 引用就销毁了,引用计数变为 1
  • 当引用 b 的值由对象的地址变为 null 的时候,b 引用也销毁了,引用计数变为 0

此时垃圾回收机制发现对象的引用计数为 0,说明这个对象就可以释放掉了

  • 引用计数为 0,就说明这个对象是垃圾了
  • 有专门的线程,去获取到当前每个对象的引用计数的情况

存在问题

引用计数机制,是一个简单有效的机制,存在两个关键问题

1. 消耗额外的内存空间

要给每个对象都安排一个计数器,就算计数器按照两个字节算,整个程序中对象数目很多,总的消耗空间也会非常多;尤其是如果每个对象体积比较小,假设每个对象四个字节,计数器消耗的空间,就达到了对象空间的一半

类似于花钱买 100 平的房子,实际上你房子的使用面积也就 70 多平(非常难受)

2. "循环引用"问题

引用计数可能会产生"循环引用的问题"。此时,引用计数就无法正确工作了

java 复制代码
class Test {
	Test t;
}

Test a = new Test();
Test b = new Test();

a.t = b;
b.t = a;

a = null;
b = null;
  • Test 对象里面有一个成员变量 t,他的类型也是 Test,也就是说它也可以引用一个对象
  • a.t = b 的意思是:将 a 引用对象中的 t 成员变量的值赋为 b 的引用
    • 所以此时第二个引用对象就会有两个引用指向,一个是 a,一个是 a.t
    • 所以第二个引用对象的引用计数就会变成 2
  • 同理,b.t=a 的结果就是第一个引用计数也会变成 2

  • ab 都被赋值为 0 之后,两个对象的引用计数都变成了 1,但此时这两个对象都没法使用了(双方的引用指向都在对方那里,类似于"死锁"的情况)。由于引用计数不为 0,也没法被回收

2. 可达性分析(JVM 用的)

本质上是用"时间换空间",相比于引用计数,需要小号更多的额外的时间。但是总体来说还是可控的,不会产生类似于"循环引用"这样的问题

在写代码的过程中,会定义很多的变量。比如,栈上的局部变量/方法区中的静态类型的变量/常量池引用的对象...

  • 就可以从这些变量作为起点出发,尝去进行"遍历"。
  • 所谓遍历就是会沿着这些变量中持有的引用类型的成员,再进一步的往下进行访问
  • 所有能被访问到的对象,自然就不是垃圾,剩下的遍历一圈也访问不到的对象,自然就是垃圾了

比如有如下代码:

java 复制代码
class Node {
	char val;
	Node left;
	Node right;
}

Node buildTree() {
	Node a = new Node();
	Node b = new Node();
	Node c = new Node();
	Node d = new Node();
	Node e = new Node();
	Node f = new Node();
	Node g = new Node();
	
	a.right = b;
	a.left = c;
	b.left = d;
	b.right = e;
	e.left = g;
	c.right = f;
	
	return a;
)

Node root = buildTree();

虽然这个代码中,只有一个 root 这样的引用,但是实际上上述 7 个节点对象都是"可达"的

  • b == root. left;
  • c == root. right;
  • d == root. left. left;
  • 依此类推,上述的对象都能通过 . 的方式访问到
    JVM 中存在扫描线程,会不停地尝试对代码中已有的这些变量进行遍历,尽可能多的访问到对象

上述代码中,如果执行这个代码:root.right.right = null;

  • 就会导致 cf 之间断开了,此时 f 这个对象就被"孤立"了
  • 按照上述从 root 出发进行遍历的操作就也无法访问到 f 了,f 这个节点对象就称为"不可达 "
  • 如果 ac 之间断开了,此时 c 就不可达了。由于访问 f 必须通过 cc 不可达就导致 f 不可达。所以此时 cf 都是垃圾了
  • 如果 root=null,此时整棵树都是垃圾了

JVM 自身知道一共有哪些对象,通过可达性分析的遍历,把可达的对象都标记出来了,剩下的自然就是不可达的了

相关推荐
kill bert5 小时前
Java八股文背诵 第四天JVM
java·开发语言·jvm
你是理想9 小时前
wait 和notify ,notifyAll,sleep
java·开发语言·jvm
returnShitBoy11 小时前
Go语言中的垃圾回收是如何工作的?
java·jvm·golang
liwulin050617 小时前
【JAVA】JVM 堆内存“缓冲空间”的压缩机制及调整方法
java·开发语言·jvm
八股文领域大手子18 小时前
从接口400ms到20ms,记录一次JVM、MySQL、Redis的混合双打
jvm·数据库·redis·mysql·jar
佩奇的技术笔记18 小时前
Java学习手册:JVM、JRE和JDK的关系
java·开发语言·jvm
qian_qh1 天前
如何判断JVM中类和其他类是不是同一个类
jvm
魔道不误砍柴功1 天前
Java性能调优2025:从JVM到Kubernetes的全链路优化策略
java·jvm·kubernetes
bing_1581 天前
JVM 如何分析 GC 日志,定位 GC 性能问题?
jvm·定位gc性能·分析gc日志
斗锋在干嘛2 天前
Android 回答视频边播放边下载的问题
android·jvm·音视频