【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 自身知道一共有哪些对象,通过可达性分析的遍历,把可达的对象都标记出来了,剩下的自然就是不可达的了

相关推荐
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS墙绘产品展示交易平台(JAVA毕业设计)
java·jvm·vue.js·spring boot·后端·spring cloud·开源
尘世壹俗人3 小时前
设置Hadoop守护进程的JVM参数
大数据·jvm·hadoop
水w4 小时前
什么是AQS
java·开发语言·jvm·什么是aqs
Qu1NnTZ5 小时前
JVM 类加载机制
jvm
easyboot6 小时前
C#和Python共享内存技术
java·jvm·算法
来一杯龙舌兰11 小时前
【JVM】一篇文章彻底理解JVM的组成,各组件的底层实现逻辑
java·开发语言·jvm·面试题·虚拟机·底层原理
big-huan12 小时前
JAVA JVM常见面试题
java·jvm
扎克begod13 小时前
JAVA并发编程系列(11)线程池底层原理架构剖析
java·jvm·架构
傻啦嘿哟14 小时前
SQLite3模块使用详解
jvm·数据库·oracle