finalize() 原理,了解一下?

前言

在之前深入浅出 JVM GC(1)我们知道,finalize 方法的作用是:

如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。
注意:当对象没有覆盖 finalize 方法,或者 finalize 方法已经被虚拟机调用过,虚拟机将这两种情况都视为 “没有必要执行”。也就是说,finalize 方法只会被执行一次。
=========================================================
如果这个对象被判定为有必要执行 finalize 方法,那么这个对象将会放置在一个叫做 F-Queue 的队列之中,并在稍后由一个虚拟机自动建立的,优先级为 8 的 Finalizer 线程去执行它。
注意:如果一个对象在 finalize 方法中运行缓慢,将会导致队列后的其他对象永远等待,严重时将会导致系统崩溃。
=========================================================
finalize 方法是对象逃脱死亡命运的最后一道关卡。稍后 GC 将对队列中的对象进行第二次规模的标记,如果对象要在 finalize 中 “拯救” 自己,只需要将自己关联到引用上即可,通常是 this。
如果这个对象关联上了引用,那么在第二次标记的时候他将被移除出 “即将回收” 的集合;如果对象这时候还没有逃脱,那基本上就是真的被回收了。

那么,就看看这个方法的具体原理。

测试 demo

public class FinalizeTest {

  public static void main(String[] args) {
    FinalizeTest f = new FinalizeTest();
    f = null;
    System.gc();
  }

  @Override
  protected void finalize() throws Throwable {
    System.out.println("finalize");
  }
}

这是我们的测试 demo,然后,我们在 finalize 方法中,打上断点。启动 JVM,得到以下堆栈。

image.png

可以看到,一个 FinalizerThread 的线程执行了我们的 finalize 方法。那么过程是如何的呢?

堆栈分析

这个 FinalizerThread 的初始化和启动在 Finalizer 的 static 块中,由 JVM 主动访问其外部类 Finalizer 初始化这个静态块。具体访问方法是 Finalizer 的 register 方法。

静态块会启动这个线程,这个线程的优先级是 8 ,比普通的线程要高一点,但是是 demon 线程。

这个线程的任务则是死循环从 Finalizer 的队列中,取出 Finalizer 对象,然后调用这些对象的 runFinalizer 方法。

而这个队列是一个 ReferenceQueue 队列 。里面存放的就是 Finalizer 对象,当一个对象需要执行 finalize 方法(未执行过且重写了该方法)的时候, JVM 会将这个对象包装成 Finalizer 实例,然后,链接到 Finalizer 链表中,并放入这个队列(详细的等会再讲)。

而这个 runFinalizer 方法的具体逻辑则是获取 Finalizer 对象包装的引用,即实际对象(是枚举则跳过),执行这个对象的 finalize 方法。执行完毕后,清空 Finalizer。

到这里,一个对象的 finalize 方法就执行结束了。

如何放入队列?

Finalizer 继承了 Reference 类,该类和 GC 密切相关。

而该类有一个高优先级的线程—— ReferenceHandler。他的任务则是死循环执行 tryHandlePending 方法。处理 Reference 的 pending 属性,而这个属性其实就是 Reference 自己。GC 的时候,会设置这个地址 pending 地址。这段代码在 Hotspot 中。有兴趣的可以看看。

当这个线程发现 pending 地址不是空,就会尝试将自身放到自己的 queue 属性队列中。

代码如下:

ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);

因此,当我们构造了一个 Finalizer 对象,这个对象会被 GC 设置到自该对象的 pending 属性中,然后 ReferenceHandler 线程会处理这个 pending 属性,具体处理则是将自己添加到构造函数设置的队列中。

这个时候,Finalizer 中的线程就可以从队列中取出这个 Finalizer 对象了。

而这一切都是虚拟机做的。

总结

finalize 方法高度依赖 JVM 和 GC,当一个对象被标记后,便会被 JVM 包装成 Finalizer 对象,然后,被 JVM 设置到 Reference 的静态属性 pending 中,Reference 的内部线程则会将这个 pending 放入到构造函数的队列中。

Finalizer 的内部线程则会从队列中取出 Finalizer 对象,并调用其包装的实际对象的 finalize 方法。

所以,finalize 方法需要两个线程来处理他,一个是 ReferenceHandler ,一个是 FinalizerThread。

前者负责将 Finalizer 对象放入到 Reference 队列中,后者负责从队列中取出 Finalizer 对象并调用实际对象的 finalize 方法。

同时,GC 大概也要做 2 件事情,一个是创建 Finalizer 对象,一个是将该对象设置到自己的 pending 属性中。

拾遗

在 Reference 的 tryHandlePending 方法中,有一个需要注意的地方,就是 Cleaner,相关代码如下:

static boolean tryHandlePending(boolean waitForNotify) {
    Reference<Object> r;
    Cleaner c;
    try {
        synchronized (lock) {
            if (pending != null) {
                r = pending;
                c = r instanceof Cleaner ? (Cleaner) r : null;
                pending = r.discovered;
                r.discovered = null;
            } else {
                if (waitForNotify) {
                    lock.wait();
                }
                // retry if waited
                return waitForNotify;
            }
        }
    } catch (OutOfMemoryError x) {
        Thread.yield();
        return true;
    } catch (InterruptedException x) {
        return true;
    }
    if (c != null) {
        c.clean();
        return true;
    }

    ReferenceQueue<? super Object> q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    return true;
}

判断如果,这个引用时 Cleaner 类型,执行该类的 clean 方法就可以了,就不放入队列了。而这个 Cleaner 和 NIO 的直接内存相关,这点其实在楼主分析 Netty 的 noCleaner 策略时提过。

DirectByteBuffer 类中有个 Deallocator 线程,该线程的 run 方法就是调用 unsafe.freeMemory(address) 方法释放直接内存。

当构造 DirectByteBuffer 对象的时候,会创建一个相应的 Deallocator。

而这个 Cleaner 对象则包装了这个 Deallocator,当调用 Cleaner 的 clean 方法的时候,实际上,调用的是用 Deallocator 的 run 方法。这样,当 Cleaner 对象回收的时候,就可以顺手清理直接内存。

由于 DirectByteBuffer 对象中的 Cleaner 目前除了自己使用外,无他人使用,那么当 DirectByteBuffer 被回收时,Cleaner 也会被回收,自然,也就会执行 Finalizer 的逻辑了。

注意:这个 Deallocator 线程只有一个构造方法会创建它 —— DirectByteBuffer(int cap). 对应的 ByteBuffer 构造方法应该是 static ByteBuffer allocateDirect(int capacity)

使用的时候需要注意。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269