Finalize进程以及Object.finalize的作用
一. finalize()有什么作用
有过Java虚拟机基础的读者们知道,Java程序中的每个对象都有属于自己的生命周期,一旦某个对象进入死亡状态,那么这个对象就会被GC
回收掉。那么如何判断一个对象是死亡的呢?
1.1 对象是否死亡
- 引用计数法。
- 可达性分析法。
- 再谈引用(强、软、弱。虚引用)。
finalize()
方法。
我们知道Object
类是所有类的父类,而 finalize()
方法就是Object类下的方法,并且没有具体的实现。那么在这里直接说 finalize()
方法的作用:
- 若某个类实现了此方法并重写(且没有被执行过),并能够成功的让此对象重新与引用链上的任何一个对象关联(即可达)。
- 那么这个对象就能够逃脱
GC
,逃脱被回收的命运。
备注:
1.2 finalize()实战
Demo如下(引自我的另一篇文章深入理解Java虚拟机系列(二)–垃圾收集器与内存分配策略):
public class FinalizeTest {
public static FinalizeTest INSTANCE = null;
public void isAlive() {
System.out.println("--------我存活了---------");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("----------finalize方法执行成功!-------");
FinalizeTest.INSTANCE = this;
}
public static void main(String[] args) throws InterruptedException {
INSTANCE = new FinalizeTest();
INSTANCE = null;
System.gc();
// 因为finalize方法的优先级很低,所以暂停0.5s等待他。
// 对象第一次去尝试拯救自己
Thread.sleep(500);
if (INSTANCE != null) {
INSTANCE.isAlive();
} else {
System.out.println("----------我已经死亡(第一次)----------");
}
INSTANCE = null;
System.gc();
// 因为finalize方法的优先级很低,所以暂停0.5s等待他。
// 对象第二次去尝试拯救自己
Thread.sleep(500);
if (INSTANCE != null) {
INSTANCE.isAlive();
} else {
System.out.println("----------我已经死亡(第二次)----------");
}
}
}
程序运行结果如下:
可以佐证:
finalize()
方法确实在GC
的时候被触发了一次,并成功的让对象可达,逃脱被GC
。finalize()
方法只能够执行一次,所以第二次无法逃脱被GC
。
那如果不重写 finalize()
方法,看看结果是什么?(自己把代码注释掉)
可以见到,必须要重写父类Object
类的 finalize()
方法,才能够在GC的时候,有机会去逃脱回收。
二. Finalizer线程
2.1 Demo1
Finalizer
线程相关介绍:
- 若对象重写了
finalize()
方法,那么该方法会由Finalizer
线程来执行。 Finalizer
线程的优先级较低。Finalizer
线程虽然会触发finalize()
方法,但是并不会保证会等待它运行结束。
给大家看个案例:
相关的VM参数设置:
-verbose:ge -XX:+PrintGCDetails -Xms64m -Xmx64m -Xmn64m
Demo
public class Test {
static AtomicInteger aliveCount = new AtomicInteger(0);
Test() {
aliveCount.incrementAndGet();
}
@Override
protected void finalize() throws Throwable {
Test.aliveCount.decrementAndGet();
}
public static void main(String[] args) {
int i = 0;
while (true) {
Test test = new Test();
if ((i++ % 100_000) == 0) {
System.out.format("After creating %d objects, %d are still alive.%n", new Object[]{i, Test.aliveCount.get()});
}
}
}
}
运行结果如下:
若加上GC分析日志,部分GC结果如下:
代码分析如下:
- 首先程序的本质是不断创建
Test
类,并且创建出来的Test
对象,我们并没有做什么事情,我们理想的状态是这个Test
类会被立马GC
回收掉。 - 我们创建一个对象,计时器+1,对象经过一次
GC
(就是会调用一次finalize()
),计时器-1。 - 从结果可以发现,我们可以发现创建出来的对象并不是我们理想状态中的被立马回收,并且最终抛了
OOM
异常。
2.2 Demo2
同时,可以尝试将finalize()
方法删除,并将VM的堆大小限制给去除,那么程序放一段时间后,又是另一番景象:
从日志输出可以发现,后期一直在进行Full GC
,对老年代和持久代进行垃圾回收。实际上,当我们重写了finalize()
方法后,会为每一个Finalizable
对象创建一个实例,并为Finalizable
类所引用,那么由于该引用链,导致整个Test
对象都是存活的。
那么在不断创建Test
对象的过程当中,Eden
区的内存也会被不断的占用,因此对象从Eden区
最终转移到老年代里面,老年代堆满了之后,就会进行Full GC
。
在代码执行过程中,在终端使用jps
命令查看线程
随后使用jstack
命令来查看相关线程信息:
jstack 8100
可得:代码执行过程中有个Finalizer
守护线程正在运行。
在程序启动过程中,可以使用Arthas来观察相关的参数:
代码分析:
此时此刻有两个线程在不停的循环:
- Main主线程:负责创建
Test
对象,由于Test
类重写了finalize()
方法,因此每个Test
的子类都会有个Finalizer
对象创建出来,会放到F-Q
队列里面。(java.lang.ref.Finalizer.ReferenceQueue
) - Finalizer守护线程:负责处理
F-Q
队列,将对象弹出,并调用其finalize()
方法。
但是。由于 Finalizer守护线程的优先级比较低,因此其处理速度无法跟的上主线程的创建对象的速度。导致堆中可用的空间被耗尽。