GC
0. Introduce
垃圾主要还是指对象,所以以下算法将介绍对象的回收。所以垃圾回收的含义就是:将内存中已经不会被使用的对象(或类和常量)清除,释放内存空间。
JVM
的内存模型分为五个部分,其中堆内存的唯一目的就是存放对象,对象也基本上都是存放在堆内存中。
1. 如何识别垃圾
对象在什么情况下无法被使用?很简单,没有引用指向这个对象,我们自然无法使用它。
public static void main(String[] args) throws InterruptedException {
Object a = new Object(); a = null;
}
上面的代码中,我创建了一个对象,并使用变量a
指向这个对象,但是在这之后,我又将null
赋给了a
,这会出现什么情况?不难发现,我们已经无法使用这个对象了,它已经丢失了,因为我们已经无法通过任何变量去调用这个对象,但是它依然在内存中。此时,这个对象占用着内存就是白白浪费资源。
1.0 引用计数法
若有新的变量引用一个对象时,这个对象的引用个数加 1;若一个引用失效时,引用的个数减 1,而引用个数为 0的对象,即可作为垃圾被回收。这个算法实现简单,效率也高,但是,它并没有被用在主流的 Java
虚拟机中,因为它有一个很大的缺陷——很难解决循环引用的问题。
1.1 可达性分析法
从根节点对象出发,使用DFS
或BFS
算法,沿着引用递归遍历,而无法被遍历到的对象,就是无法再被使用的对象,可以被垃圾回收器回收。所谓的根节点,就是我们能够直接使用的引用类型变量,如:
方法中的参数或局部变量;
类的静态成员或非静态成员;
代码中的常量;
2. 如何释放垃圾
2.0 标记—清除算法(Mark-Sweep)
根据可达性分析标记后,开始清除,直接释放垃圾对象所占内存空间。
问题:
清除的效率低。因为需要扫描整个内存空间,逐个释放对象所占内存;
使用这个算法清除垃圾后,将会造成很多内存碎片,所以可能出现剩余内存较多,但是没有较大的连续空间,导致大对象无法被分配空间,而再次触发垃圾回收;
2.1 复制算法(Copying)
将内存分为两个相等大小的区域,一块存放对象,一块保留。当存放对象的那块区域无法再分配空间时,将所有仍然存活的对象复制到保留的那块区域中,然后直接释放当前正在使用区域的全部内存。

但是,这里存在一个问题,复制算法将内存区域划分为相等的两部分,这也意味着每次都有一半的空间无法被使用,这未免也太浪费了。所以,对于空间的划分,需要做出一些改进。IBM
公司的研究表明,98%
的对象存活时间都非常的短暂,所以,完全没有必要保留一半的空间供复制使用。在实际实现中,会将空间划分为三块区域,一块较大的 Eden
(伊甸园,很文艺的名字) 空间,以及两块较小的 Survivor
(幸存者区,有点末日生存的味道)空间。
交替地使用两块 Survivor
空间,来存放垃圾回收中任然存活的对象。而在具体实现中,这三个空间的比例一搬是8:1:1
,即是说只有 10%
的空间无法被使用。
可以看出,这个算法在大部分对象的生命周期都短时,效率会非常高,但是若大部分对象的生命周期都很长,将不再适用。如果在某次垃圾回收过后,仍然有大量的对象存活,此时一个 Survivor
空间不够存放这些对象怎么办?这时候就需要有另一个空间来做担保了。就像我们去银行贷款,需要有一个担保人,当贷款人不能偿还时,由担保人代为偿还。以上算法是用在新生代中,而所谓的担保空间,实际上就是老年代。
参考资料 Java中的垃圾回收算法详解 - 特务依昂 - 博客园 (cnblogs.com)
2.2 标记—整理(Mark-Compact)
由于老年代中的对象一般存活时间都比较长,所以并不适合在老年代使用上面的复制算法进行垃圾回收。
所谓的整理,就是将内存中还存活的对象向一边移动,直至这些对象相互靠拢。

0.3 分代收集算法
对于新生代而言,每一次垃圾回收都能回收大部分内存,所以适合使用复制算法,同时以老年代作为这个算法的担保空间;
对于老年代而言,每次垃圾回收只能释放小部分空间,若使用复制算法,每次将需要做大量复制,而且此时
Survivor
需要较大的空间,所以不适合使用复制算法,因此在老年代中,一般使用标记—清除或者标记—整理算法;
3. JVM Heap 的划分
对于大多数应用,Java 堆 (Heap)是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。
为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(==分代的唯一理由就是优化 GC 性能==):
新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大
( 元空间(JDK1.8 之前叫永久代):像一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存。)

Last updated