深度揭秘垃圾回收底层,这次让你彻底弄懂她

Java 与 C++ 之间有一堵由内存动态分配和垃圾收集技术所围成的高墙 ---《深入理解Java虚拟机》

我们知道手动管理内存意味着自由、精细化地掌控,但是却极度依赖于开发人员的水平和细心程度。

如果使用完了忘记释放内存空间就会发生内存泄露,再如释放错了内存空间或者使用了悬垂指针则会发生无法预知的问题。

这时候 Java 带着 GC 来了(GC,Garbage Collection 垃圾收集,早于 Java 提出),将内存的管理交给 GC 来做,减轻了程序员编程的负担,提升了开发效率。

所以并不是用 Java 就不需要内存管理了,只是因为 GC 在替我们负重前行。

但是 GC 并不是那么万能的,不同场景适用不同的 GC 算法,需要设置不同的参数,所以我们不能就这样撒手不管了,只有深入地理解它才能用好它。

关于 GC 内容相信很多人都有所了解。我最早得知有关 GC 的知识是来自《深入理解Java虚拟机》,但是有关 GC 的内容单看这本书是不够的。

当时我以为我懂很多了,后来经过了一番教育之后才知道啥叫无知者无畏。

深度揭秘垃圾回收底层,这次让你彻底弄懂她

而且过了一段时间很多有关 GC 的内容都说不上来了,其实也有很多同学反映有些知识学了就忘,有些内容当时是理解的,过一段时间啥都不记得了。

大部分情况是因为这块内容在脑海中没有形成体系,没有搞懂前因后果,没有把一些知识串起来

近期我整理了下 GC 相关的知识点,想由点及面展开有关 GC 的内容,顺带理一理自己的思路,所以输出了这篇文章,希望对你有所帮助。

有关 GC 的内容其实有很多,但是对于我们这种一般开发而言是不需要太深入的,所以我就挑选了一些我认为重要的整理出来,本来还有一些源码的我也删了,感觉没必要,重要的是在概念上理清。

本来还打算分析有关 JVM 的各垃圾回收器,但是文章太长了,所以分两篇写,下篇再发。

本篇整理的 GC 内容不限于 JVM 但大体上还是偏 JVM,如果讲具体的实现默认指的是 HotSpot。

正文

首先我们知道根据 「Java虚拟机规范」,Java 虚拟机运行时数据区分为程序计数器、虚拟机栈、本地方法栈、堆、方法区。

深度揭秘垃圾回收底层,这次让你彻底弄懂她

而程序计数器、虚拟机栈、本地方法栈这 3 个区域是线程私有的,会随线程消亡而自动回收,所以不需要管理。

因此垃圾收集只需要关注堆和方法区。

而方法区的回收,往往性价比较低,因为判断可以回收的条件比较苛刻。

比如类的卸载需要此类的所有实例都已经被回收,包括子类。然后需要加载的类加载器也被回收,对应的类对象没有被引用这才允许被回收。

就类加载器这一条来说,除非像特意设计过的 OSGI 等可以替换类加载器的场景,不然基本上回收不了。

而垃圾收集回报率高的是堆中内存的回收,因此我们重点关注堆的垃圾收集

如何判断对象已成垃圾?

既然是垃圾收集,我们得先判断哪些对象是垃圾,然后再看看何时清理,如何清理。

常见的垃圾回收策略分为两种:一种是直接回收,即引用计数;另一种是间接回收,即追踪式回收(可达性分析)。

大家也都知道引用计数有个致命的缺陷-循环引用,所以 Java 用了可达性分析。

那为什么有明显缺陷的计数引用还是有很多语言采用了呢?

比如 CPython ,由此看来引用计数还是有点用的,所以咱们就先来盘一下引用计数。

引用计数

引用计数其实就是为每一个内存单元设置一个计数器,当被引用的时候计数器加一,当计数器减少为 0 的时候就意味着这个单元再也无法被引用了,所以可以立即释放内存。

深度揭秘垃圾回收底层,这次让你彻底弄懂她

如上图所示,云朵代表引用,此时对象 A 有 1 个引用,因此计数器的值为 1。

对象 B 有两个外部引用,所以计数器的值为 2,而对象 C 没有被引用,所以说明这个对象是垃圾,因此可以立即释放内存。

由此可以知晓引用计数需要占据额外的存储空间,如果本身的内存单元较小则计数器占用的空间就会变得明显。

其次引用计数的内存释放等于把这个开销平摊到应用的日常运行中,因为在计数为 0 的那一刻,就是释放的内存的时刻,这其实对于内存敏感的场景很适用。

如果是可达性分析的回收,那些成为垃圾的对象不会立马清除,需要等待下一次 GC 才会被清除。

引用计数相对而言概念比较简单,不过缺陷就是上面提到的循环引用。

那像 CPython 是如何解决循环引用的问题呢?

首先我们知道像整型、字符串内部是不会引用其他对象的,所以不存在循环引用的问题,因此使用引用计数并没有问题。

那像 List、dictionaries、instances 这类容器对象就有可能产生循环依赖的问题,因此 Python 在引用计数的基础之上又引入了标记-清除来做备份处理。

但是具体的做法又和传统的标记-清除不一样,它采取的是找不可达的对象,而不是可达的对象。

Python 使用双向链表来链接容器对象,当一个容器对象被创建时,它被插入到这个链表中,当它被删除时则移除。

然后在容器对象上还会添加一个字段 gc_refs,现在咱们再来看看是如何处理循环引用的:

对每个容器对象,将 gc_refs 设置为该对象的引用计数。

对每个容器对象,查找它所引用的容器对象,并减少找到的被引用的容器对象的 gc_refs 字段。

将此时 gc_refs 大于 0 的容器对象移动到不同的集合中,因为 gc_refs 大于 0 说明有对象外部引用它,因此不能释放这些对象。

然后找出 gc_refs 大于 0 的容器对象所引用的对象,它们也不能被清除。

最后剩下的对象说明仅由该链表中的对象引用,没有外部引用,所以是垃圾可以清除。

具体如下图示例,A 和 B 对象循环引用, C 对象引用了 D 对象。

深度揭秘垃圾回收底层,这次让你彻底弄懂她

为了让图片更加清晰,我把步骤分开截图了,上图是 1-2 步骤,下图是 3-4 步骤。

深度揭秘垃圾回收底层,这次让你彻底弄懂她

最终循环引用的 A 和 B 都能被清理,但是天下没有免费的午餐,最大的开销之一是每个容器对象需要额外字段。

还有维护容器链表的开销。根据 pybench,这个开销占了大约 4% 的减速

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsxwjp.html