一道常见的java面试题:描述final、finally、finalize的区别
final、finally是常用的java关键字,不赘述。
finalize是Object类的方法名,如果重写了finalize方法,jvm在这个对象被gc之前会执行对象的finalize方法。java的引用常见的有强引用、软引用(SoftReference)、弱引用(WeakReference)、虚引用(PhantomReference),而FinalReference同样继承了Reference类,但在编程时从未用到过。
一、FinalReference
class FinalReference<T> extends Reference<T> {
public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
FinalReference继承了Reference,类访问权限是package,我们无法继承扩展,jdk对此类进行了扩展实现java.lang.ref.Finalizer
二、Finalizer
final class Finalizer extends FinalReference<Object> { /* Package-private; must be in
same package as the Reference
class */
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
private static Finalizer unfinalized = null;
private static final Object lock = new Object();
private Finalizer
next = null,
prev = null;
private Finalizer(Object finalizee) {
super(finalizee, queue);
add();
}
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
...
Finalizer的类访问权限也是package,而且是final不能继承。
- Finalizer的属性
- static ReferenceQueue<Object> queue,Finalizer引用的对象被gc之前,jvm会把相应的Finalizer对象放入队列。
- static Finalizer unfinalized,静态的Finalizer对象链。
- Finalizer next = null, prev = null,对象链上一个、下一个的引用
- Finalizer构造函数
- private私有构造函数,我们无法自己创建Finalizer类的对象。
- 参数finalizee,FinalReference引用的对象。
- 构造方法会调用add(),把当前对象加入Finalizer对象链。
三、注册Finalizer对象
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
由于构造函数是私有的,外部无法调用,只有jvm调用Finalizer.register(finalizee)时才会创建Finalizer对象并加入对象链。
- f类:如果一个类重写了void finalize()方法,并且方法体不为空,类加载时jvm会给这个类加上标记,表示这是一个finalizer类(为和Finalizer类区分,以下都叫f类)。
- 创建一个对象,会先为分配对象空间,然后调用构造方法。
- 如果创建的是f类对象,默认会在调用构造方法返回之前调用register方法,参数就是当前对象。如果设置了-XX:-RegisterFinalizersAtInit,则会在调用构造方法之前调用register方法。
- clone一个f类对象,会在clone完成时调用register方法。
四、加入ReferenceQueue等待gc回收
public abstract class Reference<T> {
static private class Lock { }
private static Lock lock = new Lock();
private static Reference<Object> pending = null;
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
while (true) {
tryHandlePending(true);
}
}
...
}
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
...
if (pending != null) {
...
} else {
if (waitForNotify) { lock.wait(); }
return waitForNotify;
}
...
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
static {
...
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
...
}
}
- gc发生时,gc算法会判断对象是否只被Finalizer类引用了(f类对象被Finalizer对象引用,然后放到Finalizer对象链里),
如果是,jvm会把Finalizer对象赋给Reference的pending属性,并调用lock.notify()。 - Reference的静态块会创建一个守护线程ReferenceHandler,循环执行tryHandlePending方法
- tryHandlePending方法执行时,如果pending为空,会调用lock.wait(),释放锁对象并让线程进入阻塞状态。
- 一旦jvm给pending赋值并调用了lock.notify(),ReferenceHandler线程将被唤醒,将Finalizer对象加入ReferenceQueue。
五、f类对象的GC回收
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
if (running)
return;
running = true;
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer();
} catch (InterruptedException x) {
continue;
}
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
FinalizerThread是Finalizer的内部类,继承了Thread。
Finalizer的静态块中,会创建一个守护线程FinalizerThread,run方法会循环从ReferenceQueue中取出Finalizer对象,执行runFinalizer方法
private void runFinalizer() {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
invokeFinalizeMethod(finalizee);
/* Clear stack slot containing this variable, to decrease
the chances of false retention with a conservative GC */
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
static native void invokeFinalizeMethod(Object o) throws Throwable;
runFinalizer方法会将对象传递给本地方法invokeFinalizeMethod(),最终调用f类对象自身的finalize()。
以上就是对象被回收之前jvm执行finalize方法的全过程,其中对多线程的使用值得借鉴。
下图显示了此过程中参与的类,红色带+号的线表示内部类
六、Finalizer导致的内存泄露
假如某个类想通过finalize方法,来防止类被使用后忘记释放资源,那么对象至少会在第二次gc时才能被回收,
所以不应在运行期创建大量f类对象,容易导致内存泄漏。
- Finalizer其实是实现了析构函数的概念,我们在对象被回收前可以执行一些『收拾性』的逻辑,但也给对象生命周期和gc带来了影响。
- f类对象因为Finalizer的引用而变成了一个临时的强引用,无法被立即回收。
- f类对象只有在FinalizerThread执行完finalize()后的下一次gc才能被回收,这期间可能经历多次gc了。
- cpu资源比较稀缺的情况下,FinalizerThread线程有可能因为优先级较低而延迟执行f类对象的finalize()。
- 因为f类对象的finalize()迟迟没有执行,有可能会导致大部分f对象进入到老年代,引发老年代gc甚至fullgc,gc暂停时间明显变长。
参考资料
https://mp.weixin.qq.com/s/OVtGfivZxBt8Ht2yZ8rccg
https://www.jianshu.com/p/65369496d0b6