In computer science, garbage collection (GC) is a form of automatic memory management. The garbage collector, or just collector, attempts to reclaim garbage, or memory occupied by objects that are no longer in use by the program. Garbage collection was invented by John McCarthy around 1959 to simplify manual memory management in Lisp.
在计算机科学中,垃圾收集(GC)是一种自动的存储器管理机制。当一个计算机上的动态存储器不再需要时,就应该予以释放,以让出存储器,这种存储器资源管理,称为垃圾回收。垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会。垃圾回收最早起源于LISP语言。
上面是来自维基百科对GC的介绍。
写在前面
本文知识点:
Java 是怎么实现自动垃圾收集的?
有哪些垃圾收集算法,分别怎么实现?
JVM 垃圾收集器有哪些?以及优劣势比较?
自动垃圾收集
免费是世界上最昂贵的东西!
这是马云在节目《赢在中国》说过颇有哲理的一句话。简单概括就是:免费是暂时的,之后你会为这个免费买单,而且这个代价是高昂的。
Java 虚拟机的自动内存管理,是将原本应该由开发人员手动回收的内存,交给 JVM (确切来说是垃圾收集器)来自动完成回收。可想而知,自动进行的内存回收必定没有手动来的精确,还可能会出现与内存回收相关的现实问题,如内存泄露。这甚至还催生了一波垃圾回收调优的业务。
下面先来看一下垃圾收集的基础知识。
可达性分析
垃圾回收,顾名思义,就是将已经分配出去的且以后都不会再使用的内存回收,以便可以继续进行内存分配,其中,以后都不会再使用的内存就称之为垃圾。在 Java 虚拟机的语境下,垃圾指的是死亡的对象所占据的堆空间。
这里便涉及了一个关键的问题:如何辨别一个对象是存是亡?
引用计数法
引用计数法,是一种比较古老的判断对象是否存活的方法。具体做法是为每一个对象额外添加一个引用计数器,用来统计指向该对象的引用个数。在进行垃圾回收的时候,如果某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。
它的具体实现是这样子的:如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器 +1。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器 -1。也就是说,我们需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。
这种做法简单明了,很容易判断一个对象是否存活。缺点也比较明显,需要额外的空间来存储引用计数器和比较频繁的更新操作。另外它还有一个更大的缺陷,就是当两个对象相互引用时,引用计数器是无法判断对象是否存活的,这也是现行的 java 虚拟机不采用引用计数器判断对象是否存活的主要原因。
根搜索算法
根搜索算法是目前 Java 虚拟机的主流垃圾回收器采取的可达性分析算法。具体做法是选取一些对象作为垃圾收集根对象集合 (GC Roots),从跟对象开始遍历整个 Java 堆空间,能够遍历到的对象标记为存活的对象,否则对象是死亡的,也就是垃圾,可以被回收。
那么什么是 GC Roots 呢?我们可以暂时理解为由堆外指向堆内的引用,一般而言,GC Roots 包括(但不限于)如下几种:
Java 方法栈桢中的局部变量
已加载类的静态变量
JNI handles
已启动且未停止的 Java 线程
可达性分析可以解决引用计数法所不能解决的循环引用问题。举例来说,即便对象 a 和 b 相互引用,只要从 GC Roots 出发无法到达 a 或者 b,那么可达性分析便不会将它们加入存活对象合集之中。
虽然可达性分析的算法本身很简明,但是在实践中还是有不少其他问题需要解决的。
比如说,在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)。误报并没有什么伤害,Java 虚拟机至多损失了部分垃圾回收的机会。漏报则比较麻烦,因为垃圾回收器可能回收事实上仍被引用的对象内存。一旦从原引用访问已经被回收了的对象,则很有可能会直接导致 Java 虚拟机崩溃。
Stop-the-world 以及安全点
垃圾收集算法
当标记完所有的存活对象时,我们便可以进行死亡对象的回收工作了。主流的基础回收方式可分为三种。
标记-清除
即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。
插图
清除这种回收方式的原理及其简单,但是有两个缺点。一是会造成内存碎片。由于 Java 虚拟机的堆中对象必须是连续分布的,因此可能出现总空闲内存足够,但是无法分配的极端情况。
另一个则是分配效率较低。如果是一块连续的内存空间,那么我们可以通过指针加法(pointer bumping)来做分配。而对于空闲列表,Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存。