前言:学习JVM,那么不可避免的要去了解JVM相关的垃圾回收算法,本文只是讲了讲了可达性分析算法,至于标记-清除、标记-复制,标记-整理,分代收集等等算法,会在近两天的文章中陆续更新出来。
很喜欢一句话:“八小时内谋生活,八小时外谋发展。”
共勉
地点:
湖南省永州市蓝山县舜河村
作者:
用心笑*
JVM 垃圾回收算法 -可达性分析算法
- 一、先谈谈不被Java所用的引用计数法
- 二、可达性分析算法
- 2.1概念:
- 2.2、思路:
- 2.3、GC Roots可以是哪些?
- 1、详细解释:
- 2、总结
- 3、关键小技巧
- 4、注意
- 三、 对象的finalization机制
- 3.1、概述:
- 3.2、生存还是死亡?
- 3.3、具体过程
- 3.4、代码演示
- 四、自言自语
一、先谈谈不被Java所用的引用计数法
在java中是通过引用来和对象进行关联的,也就是说如果要操作对象,必须通过引用来进行。
那么很显然一个简单的办法就是通过引用计数来判断一个对象是否可以被回收。不失一般性,如果一个对象没有任何引用与之关联,则说明该对象基本不太可能在其他地方被使用到,那么这个对象就成为可被回收的对象了。这种方式成为引用计数法。
上面这段话看起来似乎并没有什么问题,但是我想起Spring中那个循环依赖,然后一套在这个引用计数法身上就翻车了。如下图:
为了解决这个问题,所以在Java中采取的是可达性分析法。即本文第二章节😁
二、可达性分析算法
2.1概念:
- 可达性分析算法:也可以称为 根搜索算法、追踪性垃圾收集
- 通过可达性分析算法的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)
2.2、思路:
通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。以下图为例:
再给大家举个生活中实例: (我想这个举例应该没谁了吧)
Java 程序员 女朋友:new GirlFriend(); (🐕保命)🐱🏍
2.3、GC Roots可以是哪些?
- 虚拟机栈中引用的对象
- 比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 方法区中类静态属性引用的对象
- 比如:Java类的引用类型静态变量
- 方法区中常量引用的对象
- 比如:字符串常量池(string Table)里的引用
- 本地方法栈内JNI(通常说的本地方法)引用的对象
- (可以理解为:引用Native方法的所有对象)
- 所有被同步锁synchronized持有的对象
- Java虚拟机内部的引用。
- 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
- 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
1、详细解释:
(1)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。
(2)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。
(3)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。
(4)第四种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。
2、总结
总结一句话就是,除了堆空间外的一些结构,比如 虚拟机栈、本地方法栈、方法区、字符串常量池 等地方对堆空间进行引用的,都可以作为GC Roots进行可达性分析。
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(PartialGC)。😶
3、关键小技巧
Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
4、注意
如果需要使用可达性算法来判断内存是否可以回收,那么分析工作一定要在一个能保障一致性的快照中进行。否则分析结果的准确性就无法保证。这点也是导致GC进行时必须“stop The World”(即停止当前工作)的一个重要原因。
即使在可达性分析算法中不可达的对象,其实也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。也就牵扯到下文的finalization机制啦
三、 对象的finalization机制
3.1、概述:
- Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑
- 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
- finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
注意:
永远不要主动调用某个对象的finalize()方法应该交给垃圾回收机制调用。理由包括下面三点:
- 在finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由Gc线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
- 因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收
- 一个糟糕的finalize()会严重影响Gc的性能。(因为GC 期间会暂停用户线程)🤭
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
3.2、生存还是死亡?
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
3.3、具体过程
即使在可达性分析算法中不可达的对象,其实也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。😀
标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
判定一个对象objA是否可回收,至少要经历两次标记过程:
- 第一次标记🙄
- 如果对象objA到GC Roots没有引用链,则进行第一次标记。
- 进行筛选,判断此对象是否有必要执行finalize()方法
- 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
- 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
- finalize()方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
3.4、代码演示
package com.crush.sringtable;
/**
* 测试Object类中finalize()方法
* 对象复活场景
*
* @author: crush
* @create: 2021-08-04-09:06
*/
public class CanReliveObj {
/**
* 类变量,属于GC Roots的一部分
*/
public static CanReliveObj canReliveObj;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
canReliveObj = this;
}
public static void main(String[] args) throws InterruptedException {
canReliveObj = new CanReliveObj();
canReliveObj = null;
// 手动调用第一次GC
System.gc();
System.out.println("-----------------第一次gc操作------------");
// 因为Finalizer线程的优先级比较低,暂停2秒,以等待它
Thread.sleep(2000);
if (canReliveObj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("-----------------第二次gc操作------------");
canReliveObj = null;
System.gc();
// 下面代码和上面代码是一样的,但是 canReliveObj却自救失败了
Thread.sleep(2000);
if (canReliveObj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
}
}
// 输出:
-----------------第一次gc操作------------
调用当前类重写的finalize()方法
obj is still alive
-----------------第二次gc操作------------
obj is dead
在进行第一次清除的时候,我们会执行finalize方法,然后 对象 进行了一次自救操作,但是因为finalize()方法只会被调用一次,因此第二次该对象将会被垃圾清除。
四、自言自语
慢慢的将学习形成一种习惯,让自己更努力一些,多一些追求。
让我们一起加油吧,我相信未来会因努力而改变的!!!
共勉
🐱🏍