垃圾收集 (GC)
垃圾收集(Garbage Collection,GC),它的任务是解决以下 3 件问题:
- 哪些内存需要回收?
- 什么时候回收?
- 如何回收?
其中第一个问题很好回答,在 Java 中,GC 主要发生在 Java 堆和方法区中,对于后两个问题,我们将在之后的内容中进行讨论,并介绍 HotSpot 的 7 个垃圾收集器。
判断对象的生死
什么时候回收对象?当然是这个对象再也不会被用到的时候回收。所以要想解决 “什么时候回收?” 这个问题,我们要先能判断一个对象什么时候什么时候真正的 “死” 掉了,判断对象是否可用主要有以下两种方法。
判断对象是否可用的算法
引用计数算法
- 算法描述:
- 给对象添加一个引用计数器;
- 每有一个地方引用它,计数器加 1;
- 引用失效时,计数器减 1;
- 计数器值为 0 的对象不再可用。
- 缺点:
- 很难解决循环引用的问题。即
objA.instance = objB; objB.instance = objA;
,objA 和 objB 都不会再被访问后,它们仍然相互引用着对方,所以它们的引用计数器不为 0,将永远不能被判为不可用。
- 很难解决循环引用的问题。即
package com.jvm;
/**
* @author :jhys
* @date :Created in 2021/2/13 19:34
* @Description :
*/
public class TestGC {
public Object obj = null;
private static final int _1MB = 1024 * 1024;
// 每个对象中包含2M的成员,方便观察
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
TestGC objA = new TestGC();
TestGC objB = new TestGC();
objA.obj = objB.obj;
objB.obj = objA.obj;
//取消对对象的引用
objA = null;
objB = null;
// 是否进行垃圾回收
System.gc();
}
}
首先,我们将 System.gc() 注释掉,也就是我们在默认情况下,不去触发垃圾回收。并在运行的时候,添加VM参数 -XX:+PrintGCDetails。我们观察输出结果;
可以看到,这个时候,占用的空间为10M左右。
如果我们取消注释,也就是主动去调用垃圾回收器,那么运行结果为:
占用空间为2M左右。
可以看出来,Java 的垃圾回收,并非采用我们上面介绍的引用计数方式。
可达性分析算法(主流)
- 算法描述:
- 从 "GC Root" 对象作为起点开始向下搜索,走过的路径称为引用链(Reference Chain);
- 从 "GC Root" 开始,不可达的对象被判为不可用。
- Java 中可作为 “GC Root” 的对象:
- 栈中(本地变量表中的reference)
- 虚拟机栈中,栈帧中的本地变量表引用的对象;
- 本地方法栈中,JNI 引用的对象(native方法);
- 方法区中
- 类的静态属性引用的对象;
- 常量引用的对象;
- 栈中(本地变量表中的reference)
即便如此,一个对象也不是一旦被判为不可达,就立即死去的,宣告一个的死亡需要经过两次标记过程。
可达性算法,还有一系列的别名:根搜索算法,追踪性垃圾收集,GC Root。
之后,看到原理,其实这些别名都是描述原理的。
首先,我们选取一些对象,这些对象是存活的,也被称为 GC Roots,然后根据这些对象的引用关系,凡是直接或者间接跟 GC Roots 相关联的对象,都是存活的。就像图中的连通性判断一样。
这个算法的想法不难。难的是,如何确定 GC Roots。
我们考虑,我们什么时候需要用到对象?(我们需要对象的时候,肯定需要这个对象是存活的)
- 栈中保存着,我们当前或者之后需要运行的方法及相关参数,所以,栈上所引用的堆中对象肯定是存活的。
- 类中的一些属性,比如,静态属性,因为它不依赖于具体的类
- 一些常用的对象,以免清理后,又要重复加载,比如常用的异常对象,基本数据类型对应的 Class 对象。
除此之外,还有很多零零碎碎的。
在堆结构周围的一些结构,其中引用的对象可以作为GC Roots
具体 GC Roots 可以概括为:
-
虚拟机栈上(确切的说,是栈帧上的本地变量表)所引用的对象
-
本地方法栈引用的对象
-
方法区中的静态属性,常量引用
-
Java 虚拟机的内部引用,常用数据类型的 Class 对象,常驻的异常对象,系统类加载器
-
所有被同步锁持有的对象
除此之外,还有一些临时的 GC Roots 可以加入进来。这里涉及到新生代老年代。
比如老年代中的对象一般都存活时间比较久,也就是大概率是活着的对象,也可临时作为 GC Roots。
可达性算法的一些细节
前面说了可达性算法,我们根据 GC Roots 来进行标记对象的死活。
但是,被判定为不可达的对象,并不立刻死亡。它仍然有次机会进行自救。
这个自救的机会,是需要重写 finalize()进行自救。
也就是可达性算法的逻辑大致是这样的:
- 第一次进行标记,凡是不可达 GC Roots 的对象,都暂时判定为死亡,只是暂时
- 检查暂时被判定为死亡对象,检查是否有重写 finalize()方法,如果有,则触发,对象可以在里面完成自救。
如果没有自救成功 或者 没有重写 finalize()方法,则宣告这个对象的死亡。
除此之外,这个对象中的 finalize()方法,只能被调用一次,一生只有一次自救机会。
四种引用类型
JDK 1.2 后,Java 中才有了后 3 种引用的实现。
- 强引用: 像
Object obj = new Object()
这种,只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用: 用来引用还存在但非必须的对象。对于软引用对象,在 OOM 前,虚拟机会把这些对象列入回收范围中进行第二次回收,如果这次回收后,内存还是不够用,就 OOM。实现类:
SoftReference
。 - 弱引用: 被弱引用引用的对象只能生存到下一次垃圾收集前,一旦发生垃圾收集,被弱引用所引用的对象就会被清掉。实现类:
WeakReference
。 - 虚引用: 幽灵引用,对对象没有半毛钱影响,甚至不能用来取得一个对象的实例。它唯一的用途就是:当被一个虚引用引用的对象被回收时,系统会收到这个对象被回收了的通知。实现类:
PhantomReference
。
宣告对象死亡的两次标记过程
- 当发现对象不可达后,该对象被第一次标记,并进行是否有必要执行
finalize()
方法的判断;- 不需要执行:对象没有覆盖
finalize()
方法,或者finalize()
方法已被执行过(finalize()
只被执行一次); - 需要执行:将该对象放置在一个队列中,稍后由一个虚拟机自动创建的低优先级线程执行。
- 不需要执行:对象没有覆盖
finalize()
方法是对象逃脱死亡的最后一次机会,不过虚拟机不保证等待finalize()
方法执行结束,也就是说,虚拟机只触发finalize()
方法的执行,如果这个方法要执行超久,那么虚拟机并不等待它执行结束,所以最好不要用这个方法。finalize()
方法能做的,try-finally 都能做,所以忘了这个方法吧!
方法区的回收
永久代的 GC 主要回收:废弃常量 和 无用的类。
- 废弃常量:例如一个字符串 "abc",当没有任何引用指向 "abc" 时,它就是废弃常量了。
- 无用的类:同时满足以下 3 个条件的类。
- 该类的所有实例已被回收,Java 堆中不存在该类的任何实例;
- 加载该类的 Classloader 已被回收;
- 该类的 Class 对象没有被任何地方引用,即无法在任何地方通过反射访问该类的方法。
垃圾收集算法
基础:标记 - 清除算法
- 算法描述:
- 先标记出所有需要回收的对象(图中深色区域);
- 标记完后,统一回收所有被标记对象(留下狗啃似的可用内存区域……)。
- 不足:
- 效率问题:标记和清理两个过程的效率都不高。
- 空间碎片问题:标记清除后会产生大量不连续的内存碎片,导致以后为较大的对象分配内存时找不到足够的连续内存,会提前触发另一次 GC。
解决效率问题:复制算法
-
算法描述:
- 将可用内存分为大小相等的两块,每次只使用其中一块;
- 当一块内存用完时,将这块内存上还存活的对象复制到另一块内存上去,将这一块内存全部清理掉。
-
不足: 可用内存缩小为原来的一半,适合GC过后只有少量对象存活的新生代。
-
节省内存的方法:
- 新生代中的对象 98% 都是朝生夕死的,所以不需要按照 1:1 的比例对内存进行划分;
- 把内存划分为:
- 1 块比较大的 Eden 区;
- 2 块较小的 Survivor 区;
- 每次使用 Eden 区和 1 块 Survivor 区;
- 回收时,将以上 2 部分区域中的存活对象复制到另一块 Survivor 区中,然后将以上两部分区域清空;
- JVM 参数设置:
-XX:SurvivorRatio=8
表示Eden 区大小 / 1 块 Survivor 区大小 = 8
。
解决空间碎片问题:标记 - 整理算法
- 算法描述:
- 标记方法与 “标记 - 清除算法” 一样;
- 标记完后,将所有存活对象向一端移动,然后直接清理掉边界以外的内存。
- 不足: 存在效率问题,适合老年代。
进化:分代收集算法
- 新生代: GC 过后只有少量对象存活 —— 复制算法
- 老年代: GC 过后对象存活率高 —— 标记 - 整理算法
参考资料: