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 可达性分析法

从根节点对象出发,使用DFSBFS算法,沿着引用递归遍历,而无法被遍历到的对象,就是无法再被使用的对象,可以被垃圾回收器回收。所谓的根节点,就是我们能够直接使用的引用类型变量,如:

  • 方法中的参数或局部变量;

  • 类的静态成员或非静态成员;

  • 代码中的常量;

2. 如何释放垃圾

2.0 标记—清除算法(Mark-Sweep)

根据可达性分析标记后,开始清除,直接释放垃圾对象所占内存空间。

问题:

  1. 清除的效率低。因为需要扫描整个内存空间,逐个释放对象所占内存;

  2. 使用这个算法清除垃圾后,将会造成很多内存碎片,所以可能出现剩余内存较多,但是没有较大的连续空间,导致大对象无法被分配空间,而再次触发垃圾回收; image.png

2.1 复制算法(Copying)

将内存分为两个相等大小的区域,一块存放对象,一块保留。当存放对象的那块区域无法再分配空间时,将所有仍然存活的对象复制到保留的那块区域中,然后直接释放当前正在使用区域的全部内存。

image.png

但是,这里存在一个问题,复制算法将内存区域划分为相等的两部分,这也意味着每次都有一半的空间无法被使用,这未免也太浪费了。所以,对于空间的划分,需要做出一些改进。IBM 公司的研究表明,98% 的对象存活时间都非常的短暂,所以,完全没有必要保留一半的空间供复制使用。在实际实现中,会将空间划分为三块区域,一块较大的 Eden(伊甸园,很文艺的名字) 空间,以及两块较小的 Survivor (幸存者区,有点末日生存的味道)空间。

交替地使用两块 Survivor 空间,来存放垃圾回收中任然存活的对象。而在具体实现中,这三个空间的比例一搬是8:1:1,即是说只有 10% 的空间无法被使用。

可以看出,这个算法在大部分对象的生命周期都短时,效率会非常高,但是若大部分对象的生命周期都很长,将不再适用。如果在某次垃圾回收过后,仍然有大量的对象存活,此时一个 Survivor 空间不够存放这些对象怎么办?这时候就需要有另一个空间来做担保了。就像我们去银行贷款,需要有一个担保人,当贷款人不能偿还时,由担保人代为偿还。以上算法是用在新生代中,而所谓的担保空间,实际上就是老年代。

参考资料 Java中的垃圾回收算法详解 - 特务依昂 - 博客园 (cnblogs.com)

2.2 标记—整理(Mark-Compact)

由于老年代中的对象一般存活时间都比较长,所以并不适合在老年代使用上面的复制算法进行垃圾回收。

所谓的整理,就是将内存中还存活的对象向一边移动,直至这些对象相互靠拢。

image.png

0.3 分代收集算法

  • 对于新生代而言,每一次垃圾回收都能回收大部分内存,所以适合使用复制算法,同时以老年代作为这个算法的担保空间;

  • 对于老年代而言,每次垃圾回收只能释放小部分空间,若使用复制算法,每次将需要做大量复制,而且此时Survivor 需要较大的空间,所以不适合使用复制算法,因此在老年代中,一般使用标记—清除或者标记—整理算法;

3. JVM Heap 的划分

对于大多数应用,Java 堆 (Heap)是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。

为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(==分代的唯一理由就是优化 GC 性能==):

  • 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代

  • 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大

  • ( 元空间(JDK1.8 之前叫永久代):像一些方法中的操作临时对象等,JDK1.8 之前是占用 JVM 内存,JDK1.8 之后直接使用物理内存。)

image.png

Last updated