堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)

1.引用计数法

给对象中添加一个引用计数器

  • 每当有一个地方引用它,计数器就加 1;
  • 当引用失效,计数器就减 1;
  • 任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题

image.png|400

2.可达性分析算法

目前主流的商用JVM都是通过可达性分析来判断对象是否可以被回收的。
image.png

这个算法的基本思路是:

通过一系列被称为「GC Roots」的根对象作为起始节点集,从这些节点开始,通过引用关系向下搜寻,搜寻走过的路径称为「引用链」,如果某个对象到GC Roots没有任何引用链相连,就说明该对象不可达,即可以被回收

要想理解可达性算法,首先要想明白几个问题:
image.png|450

1、什么是对象可达?

对象可达指的就是:双方存在直接或间接的引用关系
根可达或GC Roots可达就是指:对象到GC Roots存在直接或间接的引用关系。

2、GC Roots是什么?

垃圾回收时,JVM首先要找到所有的GC Roots,这个过程称作 「枚举根节点」 ,这个过程是需要暂停用户线程的,即触发STW。然后再从GC Roots这些根节点向下搜寻,可达的对象就保留,不可达的对象就回收。

GC Roots就是对象,而且是JVM确定当前绝对不能被回收的对象(如方法区中类静态属性引用的对象 )。只有找到这种对象,后面的搜寻过程才有意义,不能被回收的对象所依赖的其他对象肯定也不能回收嘛

当JVM触发GC时,首先会让所有的用户线程到达安全点SafePoint时阻塞,也就是STW,然后枚举根节点,即找到所有的GC Roots,然后就可以从这些GC Roots向下搜寻,可达的对象就保留,不可达的对象就回收。

即使是号称几乎不停顿的CMS、G1等收集器,在枚举根节点时,也是要暂停用户线程的。

GC Roots是一种特殊的对象,是Java程序在运行过程中所必须的对象,而且是根对象。

3、哪些对象可以作为GC Roots?

可以作为GC Roots的对象可以分为两大类:全局对象和执行上下文
image.png

(1)方法区静态属性引用的对象

全局对象的一种,Class对象本身很难被回收,回收的条件非常苛刻,只要Class对象不被回收,静态成员就不能被回收。

(2)方法区常量池引用的对象

也属于全局对象,例如字符串常量池,常量本身初始化后不会再改变,因此作为GC Roots也是合理的。

(3)方法栈中栈帧本地变量表引用的对象

属于执行上下文中的对象,线程在执行方法时,会将方法打包成一个栈帧入栈执行,方法里用到的局部变量会存放到栈帧的本地变量表中。只要方法还在运行,还没出栈,就意味这本地变量表的对象还会被访问,GC就不应该回收,所以这一类对象也可作为GC Roots。

(4)JNI本地方法栈中引用的对象

和上一条本质相同,无非是一个是Java方法栈中的变量引用,一个是native方法(C、C++)方法栈中的变量引用。

(5)被同步锁持有的对象

被synchronized锁住的对象也是绝对不能回收的,当前有线程持有对象锁呢,GC如果回收了对象,锁不就失效了嘛。

3.对象可以被回收,就代表一定会被回收吗?

即使在可达性分析法中不可达的对象,也并不是立刻回收。宣告一个对象死亡,至少要经历两次标记

第一次标记

如果对象进行可达性分析算法之后发现未与 GC Roots 引用链相连,那它将会第一次标记并且进行一次筛选。当对象没有覆盖 finalize () 方法、或者 finalize () 方法已经被 JVM 执行过,则判定为可回收对象。如果对象有必要执行 finalize () 方法,则被放入 F-Queue 队列中。稍后在 JVM 自动建立、低优先级的 Finalizer 线程(可能多个线程)中触发这个方法.

第二次标记

GC 对 F-Queue 队列中的对象进行二次标记。如果对象在 finalize () 方法中重新与引用链上的任何一个对象建立了关联,那么二次标记时则会将它移出 “即将回收” 集合。如果此时对象还没成功逃脱,那么只能被回收了

即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。

被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收

什么是finalize方法?

finalize方法

在 Java 中,finalize()方法是一个特殊的方法,它具有以下特点和作用:

一、方法定义

finalize()方法是Object类中的一个 protected 方法,其声明如下:
protected void finalize() throws Throwable {
// 方法体
}
这意味着子类可以重写这个方法来在对象被垃圾回收之前执行一些特定的清理操作

二、作用

  1. 资源清理
  • 主要用于在对象被垃圾回收器回收之前进行一些资源清理工作,比如关闭文件、释放数据库连接、释放网络连接等。因为垃圾回收器不能确定何时会回收一个对象,所以不能保证finalize()方法会在什么时候被调用。
  1. 对象复活
  • finalize()方法中,如果对象重新与引用链建立关联,那么这个对象就可能在这次垃圾回收中 “逃脱死亡”。但是,这种方式不建议使用,因为它不能保证对象一定会被复活,而且会使垃圾回收的过程变得更加复杂和不可预测。

三、注意事项

  1. 不确定性
  • finalize()方法的调用具有不确定性。垃圾回收器何时会回收一个对象以及是否会调用该对象的finalize()方法都是不确定的,不能依赖于finalize()方法来保证资源的及时释放。
  1. 性能问题
  • 调用finalize()方法会带来一定的性能开销。垃圾回收器在回收对象时,如果发现对象有重写的finalize()方法,会将该对象放入一个特殊的队列中,由一个专门的线程在稍后的时间调用finalize()方法。这个过程会增加垃圾回收的时间和复杂性。
  1. 替代方法
  • 在实际开发中,应该尽量使用try-with-resources语句、finally块等方式来确保资源的及时释放,而不是依赖于finalize()方法。例如,对于文件操作,可以使用try-with-resources语句来自动关闭文件流,这样更加可靠和高效。

总之,虽然finalize()方法在某些情况下可以用于资源清理,但是由于其不确定性和性能问题,应该谨慎使用,并优先考虑其他更可靠的资源管理方式。