浅谈 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 | Object a = new Object(); |
以上代码运行不会报错,因为他们是在内存中是同一个对象。这是如何做到的呢? JVM 并没有把这个对象拷贝很多次,因为他赋值并不是赋一个对象,而是引用。
这是因为对象是分配在堆里的,new Object()
返回的实际上是一个引用
。引用就是指向对象的钥匙。
打个比方说, 网盘链接
可以指向一个资源,你把链接给了别人并不是直接把资源发送给了别人,只是给了一个指向资源的钥匙,它可以通过这个钥匙获取到资源。
再来看一个例子
1 | List<Object> someObject = new ArrayList(); |
显然,以上的代码将两个对象塞到了一个容器里,看起来是这样的:
当然,不只是塞到容器才有引用
1 | class A{ |
另外,Java 还有多种引用类型来帮助你实现更加灵活的对象生命周期管理。本文主要讨论的是强引用的情况,并不考虑弱引用类型,有兴趣的读者可以自行了解( WeakReference , PhantomReference , SoftReference )。
如果你无法理解引用也没有问题,只需要理解成一个对象存了另一个对象之间建立的关系就好了。
GC Root
GC Root 是垃圾收集器进行分析的起点,不会被回收,而且类型有很多种但是基本上不用特地记,主要就注意这几个。
- 局部变量,参数之类的 就是指方法里面声明的那些变量,不过出了方法就没了
- 类静态字段或常量 比如
private static final XX xx = new XX()
- 虚拟机内部引用
- 被同步锁持有的对象
来个例子
1 | private static final List<OOMObject> oomObjects = new ArrayList(); |
试图说明程序内存溢出的原因。
End
总之,写代码的时候要注意一些潜在的,未消除的引用,虽然一般碰不到。
浅谈 Java 虚拟机是如何标识垃圾的