1. 引用计数
这种思想方法,并没有在 JVM
中使用,但是广泛应用于其他主流语言的垃圾回收机制中(Python
、PHP
)。
《深入理解 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
- 当
a
和b
都被赋值为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;
- 就会导致
c
与f
之间断开了,此时f
这个对象就被"孤立"了 - 按照上述从
root
出发进行遍历的操作就也无法访问到f
了,f
这个节点对象就称为"不可达 "
- 如果
a
和c
之间断开了,此时c
就不可达了。由于访问f
必须通过c
,c
不可达就导致f
不可达。所以此时c
和f
都是垃圾了- 如果
root=null
,此时整棵树都是垃圾了
JVM
自身知道一共有哪些对象,通过可达性分析的遍历,把可达的对象都标记出来了,剩下的自然就是不可达的了