# 对象存活算法

尽量以简短精炼的语言讲解,唔,所见即所得。

# 引用计数算法

给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。

两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。

正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

public class ReferenceCountingGC {
    public Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGC objectA = new ReferenceCountingGC();
        ReferenceCountingGC objectB = new ReferenceCountingGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
    }
}

# 可达性分析算法

通过 GC Roots 作为起始点进行搜索,能够达到的对象都是存活的,不可达的对象可被回收。

GC Roots 一般包含:

  • 虚拟机栈中引用的对象(每个线程都有一个虚拟机栈)。
  • 本地方法栈中引用的对象(调用 native 方法)。
  • 方法区中类静态属性,常量引用的对象(方法区其实就是 heap 老年代后面那个,又叫元数据区,永久代)。
  • 被添加了锁的对象
  • 虚拟机内部需要用到的对象。

一旦已经存在的根节点不满足存在的条件时,那么根节点与对象之间的连接将被断开。此时虽然对象 1 仍存在对其他对象的引用,但是由于其没有任何根节点引用,所以此对象即可被判定为不再使用。

# 方法区回收

方法区主要存放永久代对象,其回收率很低,因此在方法区主要是对常量池的回收和对类的卸载

大量使用反射,动态代理,CGLib 等框架,这类频繁定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。

类卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:

  • 该类所有实例都被回收

  • 加载该类的 ClassLoader 已经被回收

  • 该类对象的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。

# finalize()

finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用

当一个对象可被回收时,如果需要执行该对象的 finalize () 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize () 方法自救,后面回收时不会调用 finalize() 方法。

// 该方法在 Object 类中
protected void finalize() throws Throwable { }

# 引用类型

这个在 WeakHashMap 中讲过,相关链接

# 垃圾回收

# 标记 - 清除

单纯清理掉未被标记的对象:标记和清除过程效率不高,但是会产生大量不连续的内存碎片。

# 标记 - 整理

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

# 复制

有没有想起堆区中的两个幸存者区,尽管这不是,这种复制的缺点是只是用了内存的一半,现在的 JVM 都采用这种收集算法回收新生代,而不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。

HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象(这就是空间分配担保机制)。

先看一下垃圾收集类别:

  • Minor GC - 次要垃圾回收,主要进行新生代区域的垃圾收集。

    • 触发条件:新生代的 Eden 区容量已满时。
  • Major GC - 主要垃圾回收,主要进行老年代的垃圾收集。

  • Full GC - 完全垃圾回收,对整个 Java 堆内存和方法区进行垃圾回收。

    • 触发条件 1:每次晋升到老年代的对象平均大小大于老年代剩余空间
    • 触发条件 2:Minor GC 后存活的对象超过了老年代剩余空间
    • 触发条件 3:永久代内存不足(JDK8 之前)
    • 触发条件 4:手动调用 System.gc() 方法

详细的空间分配担保

当新生代无法容纳更多的的对象时,可以把新生代中的对象移动到老年代中,这样新生代就腾出了空间来容纳更多的对象。

如果老年代也装不下,首先会判断一下之前的每次垃圾回收进入老年代的平均大小是否小于当前老年代的剩余空间,如果小于,那么说明也许可以放得下(不过也仅仅是也许,依然有可能放不下,因为判断的实际上只是平均值,万一这一次突然非常大呢),否则,会先来一次 Full GC,进行一次大规模垃圾回收,来尝试腾出空间,再次判断老年代是否有空间存放,要是还是装不下,直接抛出 OOM 错误,摆烂。

# 分代收集

现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。

一般将堆分为新生代和老年代。

  • 新生代使用:复制算法
  • 老年代使用:标记 - 清除 或者 标记 - 整理 算法

# 参考

https://pdai.tech/md/java/jvm/java-jvm-gc.html

https://www.yuque.com/qingkongxiaguang/javase/hla7hr#ccb10fe4