浅谈 Java 虚拟机是如何标识垃圾的

浅谈 Java 虚拟机是如何标识垃圾的

Java 作为一门 VM 语言,它的垃圾回收机制确实帮我们省了很多事情,我们不再需要去”手动管理内存的分配和释放”,只需要交给 VM 来做就好了。

然而,真的是这样吗?即使有神一般高性能的垃圾回收器,我们写代码时仍然需要注意它是如何标记垃圾对象的,因为垃圾回收器并不是万能的,仍然有一些工作需要程序员自己完成。

本文试图通俗易懂的讲解 JVM 上标记垃圾的方法,如有错误请在评论区指正。

两种标记垃圾的方式

或许你曾听闻过 引用计数法,也就是 一个对象被引用时计数器 + 1 ,解除引用时计数器 - 1,当计数器为 0 时将会被 GC,看起来非常可行。

但是这种方法没有被 Java 采用,因为他有两个显而易见的问题:

  • 循环引用问题 如果一个对象内部引用了另一个 引用这个对象的 对象,那么计数器将永远不会为 0
  • 计数器的维护问题 引用计数器的值会以极快的速度更新,更新任务变得繁重

或许因此,Java 采用了 可达性分析 的方法对垃圾进行标记。

可达性分析

可达性分析的思路很简单。

他从一组叫 GC Root 的引用出发,递归搜索出所有能被到达的节点作为存活的对象,而此外那些没有被搜索到的对象就会被标记 将被清理

途中,被蓝色尖头指向的对象将不会被清除,因为他们间接或者直接的被 GC Root 引用。而旁边没有被 GC Root 引用的两个对象将会被清除,无论他们之间有什么关系。

不久,因为 Garbage F 和他的朋友 Garbage E 没有来自 GC Root 的直接/间接引用,他们就会被 gc 回收掉了。

想想看,如果在这个图中 Object C 建立了到 Garbage F 的一个引用,会发生什么?

引用

在聊 GC Root 是什么之前,你可能需要知道引用是什么。

举个例子:

1
2
3
4
Object a = new Object();
var b = a;
var c = b;
assert a == c;

以上代码运行不会报错,因为他们是在内存中是同一个对象。这是如何做到的呢? JVM 并没有把这个对象拷贝很多次,因为他赋值并不是赋一个对象,而是引用。

这是因为对象是分配在堆里的,new Object() 返回的实际上是一个引用。引用就是指向对象的钥匙。

打个比方说, 网盘链接 可以指向一个资源,你把链接给了别人并不是直接把资源发送给了别人,只是给了一个指向资源的钥匙,它可以通过这个钥匙获取到资源。

再来看一个例子

1
2
3
List<Object> someObject = new ArrayList();
someObject.add(objectA);
someObject.add(objectB);

显然,以上的代码将两个对象塞到了一个容器里,看起来是这样的:

当然,不只是塞到容器才有引用

1
2
3
4
5
6
7
class A{
A anotherA;
}

A a = new A();
A b = new A();
a.anotherA = b;

另外,Java 还有多种引用类型来帮助你实现更加灵活的对象生命周期管理。本文主要讨论的是强引用的情况,并不考虑弱引用类型,有兴趣的读者可以自行了解( WeakReference , PhantomReference , SoftReference )。

如果你无法理解引用也没有问题,只需要理解成一个对象存了另一个对象之间建立的关系就好了。

GC Root

GC Root 是垃圾收集器进行分析的起点,不会被回收,而且类型有很多种但是基本上不用特地记,主要就注意这几个。

  1. 局部变量,参数之类的 就是指方法里面声明的那些变量,不过出了方法就没了
  2. 类静态字段或常量 比如 private static final XX xx = new XX()
  3. 虚拟机内部引用
  4. 被同步锁持有的对象

来个例子

1
2
3
4
5
private static final List<OOMObject> oomObjects = new ArrayList();

for (int i = 0; i <114514 ; i++) {
oomObjects.add(new OOMObject());
}

试图说明程序内存溢出的原因。

End

总之,写代码的时候要注意一些潜在的,未消除的引用,虽然一般碰不到。

浅谈 Java 虚拟机是如何标识垃圾的

https://ib67.io/2021/11/05/How-Does-Java-Tag-Garbages/

作者

iceBear67

发布于

2021-11-05

更新于

2022-12-20

许可协议

评论