垃圾回收相关算法
1.垃圾标记阶段算法
1.1标记阶段的目的
垃圾标记阶段:主要是为了判断对象是否存活
(1)在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段。
(2)那么在JVM中究竟是如何标记一个死亡对象呢?简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
(3)判断对象存活一般有两种方式:引用计数算法和可达性分析算法。
1.2引用计数算法
(1)引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
(2)对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1.只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
(3)优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
(4)缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法的操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
1.3可达性分析算法
可达性分析算法:也可以称为根搜索算法、追踪性垃圾收集
(1)相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
(2)相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。
可达性分析实现思路
所谓"GCRoots”根集合就是一组必须活跃的引用
其基本思路如下:
(1)可达性分析算法是以根对象(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
(2)使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
(3)如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
(4)在可达性分析算法中,只有能够被根对象集合直接或者 间接连接的对象才是存活对象。
那么GC Roots 可以是哪些元素呢?
(1)虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。
(2)本地方法栈内JNI(通常说的是本地方法)引用的对象
(3)方法区中类静态属性引用的对象,比如:Java类的引用类型静态变量
(4)方法区中常量引用的对象,比如:字符串常量池(StringTable)里的引用
(5)所有被同步锁synchronized持有的对象
(6)Java虚拟机内部的引用。
基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutofMemoryError),系统类加载器。
总结:
简单一句话就是,除了堆空间的周边,比如:虚拟机栈、本地方法栈、方法区、 字符串常量池等地方对堆空间进行引用的,都可以作为 GC Roots 进行可达性分析。
1.4 对象的finalization 机制
(垃圾只是被标记不是回收,finalize() 方法机制使得我们的对象可能起死回生)
finalize() 方法机制
对象销毁前的回调函数:finalize();
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法,一个对象的finalize()方法只能被调用一次。
finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理工作,比如关闭文件、套接字和数据库连接等。
Object 类中 finalize() 源码
protected void finalize() throws Throwable { }
永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:
- 在finalize()时可能会导致对象复活。
- finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法江梅你有执行机会。
- 一个糟糕的finalize()会严重影响GC的性能。比如finalize是个死循环。
1.5生存还是死亡?
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候他们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对他立即进行回收就是不合理的。为此,定义虚拟机中的对象可能有以下三种状态:
(1)可触及的: 从根节点开始,可以到达这个对象。
(2)可复活的: 对象的所有引用都被释放,但是对象有可能在finalize()中复活。
(3)不可触及的: 对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
以上三种状态中,是由于finalize()方法的存在进行的区分。只有在对象不可触及时才可以被回收。
具体过程
判定一个对象objA是否可以回收,至少要经历两次标记过程:
(1)如果对象objA到GCRoots没有引用链,则进行第一次标记。
(2)进行筛选,判断此对象是否有必要执行finalize()方法
- 如果对象 objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
- 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个细腻及自动创建的低优先级的finalizer线程触发器finalize()方法执行。
- finalize()方法是对象逃脱死亡的最后机会,稍后 GC 会对 F-Queue 队列 中的对象进行第二次标记。如果 objA 在 finalize()方法中与引用链上的任何一个 对象建立了联系,那么在第二次标记时,objA 会被移出“即将回收”集合。之 后,对象会再次出现没有引用存在的情况。在这个情况下,finalize()方法不会被 再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的 finalize() 方法只会被调用一次。
代码演示finalize()方法可复活对象
public class CanReliveObj {
public static CanReliveObj obj;//类变量,属于 GC Root
//此方法只能被调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
obj = this;//当前待回收的对象在finalize()方法中与引用链上的一个对象obj建立了联系
}
public static void main(String[] args) {
try {
obj = new CanReliveObj();
// 对象第一次成功拯救自己
obj = null;
System.gc();//调用垃圾回收器
System.out.println("第1次 gc");
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("第2次 gc");
// 下面这段代码与上面的完全相同,但是这次自救却失败了,因为finalize()方法只能被调用一次
obj = null;
System.gc();
// 因为Finalizer线程优先级很低,暂停2秒,以等待它
Thread.sleep(2000);
if (obj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
第一次自救成功,但由于finalize()方法只会执行一次,所以第二次自救失败。
小结:
垃圾收集算法分为两大类:
1.垃圾标记阶段算法
主要是来判定哪些对象已经不再被使用,标记为垃圾对象.
判定对象为垃圾的标准: 不被任何引用所指向的对象. Object obj = new Object();
垃圾回收阶段算法:
引用计数算法(在jvm中不被使用)
如果有一个引用指向此对象,那么计数器加1. 如果没有引用指向,计数器为0, 此时就判定为垃圾.
优点:方便使用,设计简洁.
缺点: 增加了计数器的存储空间,计数需要消耗时间.导致一个循环引用问题. 好几个对象之间相互引用,但是没有其他引用指向他们,此时垃圾回收不能回收他们,但是也没有引用指向. 这就造成了内存泄漏
Object obj = new Object();
obj=null;
可达性分析算法/根搜素算法(这是java目前所使用的垃圾标记算法)
解决 循环引用问题, 设计简单 ,运行高效,防止内存泄漏
思路:
从一些活跃引用(GCRoots 根)开始, 如果对象被根直接或间接引用,那么此对象不是垃圾, 否则标记为垃圾对象.
哪些引用被用来当做根:
(1)虚拟机栈中引用的对象 (方法中引用的对象)
(2)本地方法栈中引用的对象
(3)静态变量所引用的对象
(4)常量引用指向的对象
(5)被synchronized当做锁的对象
(6)Java 虚拟机内部的引用
总结: 栈中引用的(正在使用的) 方法区,常量池中(生命周期较长的),被synchronized当做锁的对象
final(关键字) finally(代码块) finalize()(方法) 是Object类中的一个方法,在对象被最终回收之前调用,只调用一次.
finalize() 方法机制
java允许对象在销毁前去调用finalize(),去处理一些逻辑. 一般不用(不建议用)
不要自己显示的去调用finalize()方法,在里面写代码一定要慎重
在 finalize()时可能会导致对象复活。
finalize()由垃圾回收器调用,没有固定的时间.
一个糟糕的 finalize()会严重影响 GC 的性能。比如 finalize 是个死循环。
对象状态:
可触及的: 从根节点开始,可以到达这个对象 。 (没有被标记为垃圾)
可复活的: 对象的所有引用都被释放,但是对象有可能在 finalize()中复活。 确定为垃圾了,但没有调用finalize()方法.
不可触及的: 对象的 finalize()被调用,并且没有复活,那么就会进入不可触及 状态。不可触及的对象不可能被复活,因为 finalize()只会被调用一次.