Dalvik虚拟机 Finalize 方法执行分析

来源:互联网 发布:蒙特祖玛的宝藏5 mac 编辑:程序博客网 时间:2024/04/29 20:05

    毕业以后一直都在公司做虚拟机相关方面的开发,很多知识比较零碎,最近终于不用996了,可以空下来整理一下思路,写写博客做些总结。

    Finalize 方法是java object在被heap完全回收之前,一定会被调用的方法。但是,虚拟机规范不保证该方法会在什么时候执行。前段时间,有个在native out of memory的bug,就是因为android的cts中有个leakmemory测试,会大量分分配native内存,然后在finalize中释放。但是由于lemur并没有及时的保证执行finalize方法导致虚拟地址用完程序crash。 所以建议,劲量不要依赖finalize方法去释放紧缺的资源。

    一.  Finalize方法

        在虚拟机内部,对于有Finalize方法的object,其实是通过FinalizeReference来实现的。FinalizeReference类似与softReference,weakReference等,都派生自Reference类。

        不同的是,softReference这些是在java代码中显式的new 出来的,比如:

    private static final WeakReference<String> weakTest =  new WeakReference<String>(new String("weak")); 
        但是,FinalizeReference不同,我们是不会显示的去new 一个Finalize reference的,换句话将对java层的程序员是透明的。

        那么虚拟机是怎么处理的呢,是什么时候去new出Finalize reference的呢,并且怎样去执行finalize方法的呢?

        这一切可以从虚拟机的class loader机制说起。虚拟机对class的load往细了说比较复杂,需要以后开专题讨论,但是概括的讲,就是几个过程:

        FindClass->DefineClass->LoadClassFromDex->LoadMethod

        在LoadMethod的阶段,虚拟机会去判断,该class的函数表中是否含有Finalize方法,如果存在Finalize方法就会给该class置一个标记位,对应与dalvik的代码 dalvik/vm/oo/class.cpp  LoadMethodFromDex函数:

      

static void loadMethodFromDex(ClassObject* clazz, const DexMethod* pDexMethod,    Method* meth){    DexFile* pDexFile = clazz->pDvmDex->pDexFile;    const DexMethodId* pMethodId;    const DexCode* pDexCode;    pMethodId = dexGetMethodId(pDexFile, pDexMethod->methodIdx);    meth->name = dexStringById(pDexFile, pMethodId->nameIdx);    dexProtoSetFromMethodId(&meth->prototype, pDexFile, pMethodId);    meth->shorty = dexProtoGetShorty(&meth->prototype);    meth->accessFlags = pDexMethod->accessFlags;    meth->clazz = clazz;    meth->jniArgInfo = 0;<span style="color:#FF0000;">    if (dvmCompareNameDescriptorAndMethod("finalize", "()V", meth) == 0) {        /*         * The Enum class declares a "final" finalize() method to         * prevent subclasses from introducing a finalizer.  We don't         * want to set the finalizable flag for Enum or its subclasses,         * so we check for it here.         *         * We also want to avoid setting it on Object, but it's easier         * to just strip that out later.         */        if (clazz->classLoader != NULL ||            strcmp(clazz->descriptor, "Ljava/lang/Enum;") != 0)        {            SET_CLASS_FLAG(clazz, CLASS_ISFINALIZABLE);        }    }</span>
       这里有个问题,因为java/lang/object是含有finalize方法的,而其他的object包括class object都是继承自java/lang/object的,按理所,
<span style="color:#FF0000;">          dvmCompareNameDescriptorAndMethod("finalize", "()V", meth)</span>

      这个判断会永久成立,但是请注意,在dalvik中,loadMethodFromdex是在LinkClass之前做的,即这个时候,父类的method还没有被拷贝到当前load的class的函数表中。所以不存在以上问题。

       完成标记以后,会需要在object从heap分配以后为该object执行add finalize reference操作,具体的时间点,可以根据虚拟机的实现选择

       1. dalvik在执行java/lang/obect的init方法之前,代码在汇编解释器中,  代码在vm/mterp/out/InterpAsm-armv7-a.S 

.L_OP_INVOKE_OBJECT_INIT_RANGE: /* 0xf0 *//* File: armv5te/OP_INVOKE_OBJECT_INIT_RANGE.S */    /*     * Invoke Object.<init> on an object.  In practice we know that     * Object's nullary constructor doesn't do anything, so we just     * skip it unless a debugger is active.     */    FETCH(r1, 2)                  @ r1<- CCCC    GET_VREG(r0, r1)                    @ r0<- "this" ptr    cmp     r0, #0                      @ check for NULL    beq     common_errNullObject        @ export PC and throw NPE    ldr     r1, [r0, #offObject_clazz]  @ r1<- obj->clazz    ldr     r2, [r1, #offClassObject_accessFlags] @ r2<- clazz->accessFlags    tst     r2, #CLASS_ISFINALIZABLE    @ is this class finalizable?    bne     .LOP_INVOKE_OBJECT_INIT_RANGE_setFinal        @ yes, go
.LOP_INVOKE_OBJECT_INIT_RANGE_setFinal:    EXPORT_PC()                         @ can throw    bl      dvmSetFinalizable           @ call dvmSetFinalizable(obj)    ldr     r0, [rSELF, #offThread_exception] @ r0<- self->exception    cmp     r0, #0                      @ exception pending?    bne     common_exceptionThrown      @ yes, handle it    b       .LOP_INVOKE_OBJECT_INIT_RANGE_finish
   
    在dvmSetFinalizeable中,虚拟机会回调java层的libcore中FinalizeRerence中的add函数
void dvmSetFinalizable(Object *obj){    assert(obj != NULL);    Thread *self = dvmThreadSelf();    assert(self != NULL);    Method *meth = gDvm.methJavaLangRefFinalizerReferenceAdd;    assert(meth != NULL);    JValue unusedResult;    dvmCallMethod(self, meth, NULL, &unusedResult, obj);}

FinalizeReference中add函数的实现在libcore/luni/src/main/java/java/lang/ref/Finalizereference.java中:

   public static void add(Object referent) {        <span style="color:#FF0000;">FinalizerReference<?> reference = new FinalizerReference<Object>(referent, queue);</span>        synchronized (LIST_LOCK) {            reference.prev = null;            reference.next = head;            if (head != null) {                head.prev = reference;            }            head = reference;        }    }

在这里,终于看到在libcore中,我们new 出来了一个FinalizeReference,并且把含有Finalize的方法放到了新new出来的FinalizeReferce的referent域(具体实现可以去参看FinalizeReference的构造函数)。然后,以链表的形式串起来,head是FinalizeReferenc中的一个static域,

 private static FinalizerReference<?> head = null;

用heap头引用住,可以确保Finalize reference可以在heap gc的mark阶段被mark到。


   二  Finalize的 执行

    上面我们详细的分析了一个含有finalize方法的object转化为一个Finalize reference的过程,下面我们分析,在Heap gc掉该object以后,finalize进行了那些操作,finalize方法又是怎么执行的。

     Dalvik的garbage gc的算法使用的是CMS(Concurrent  mark-sweep )的算法,即先通过root集mark所以live的object,然后sweep掉所有非mark过的object。Gc也是虚拟机里面很重要的一个模块,我们以后再讨论,在这里只是粗略的总结一下,以可以继续我们后面的讨论。典型的concurrent mark-sweep算法分为5个阶段:

   1. initliaze phase

   2. mark root

   3. scan object

   4. remark

   5. reclaim

  处理reference的主要在scan object, remark和reclaim阶段。

  在scan object阶段,我们需要区分出所有的refence并且用链表把所有的相应的reference串起来,请注意这里串起来的是FinalizeReference,而不是含有finalize方法的object,但是object在该FinalizeReference的referent域中。具体的代码在vm/alloc/marksweep.cpp中:

static void scanDataObject(const Object *obj, GcMarkContext *ctx){    assert(obj != NULL);    assert(obj->clazz != NULL);    assert(ctx != NULL);    markObject((const Object *)obj->clazz, ctx);    scanFields(obj, ctx);    if (IS_CLASS_FLAG_SET(obj->clazz, CLASS_ISREFERENCE)) {        delayReferenceReferent((Object *)obj, ctx);    }}
所有的reference都会进入delayReferencereferent中处理:

/* * Process the "referent" field in a java.lang.ref.Reference.  If the * referent has not yet been marked, put it on the appropriate list in * the gcHeap for later processing. */static void delayReferenceReferent(Object *obj, GcMarkContext *ctx){    assert(obj != NULL);    assert(obj->clazz != NULL);    assert(IS_CLASS_FLAG_SET(obj->clazz, CLASS_ISREFERENCE));    assert(ctx != NULL);    GcHeap *gcHeap = gDvm.gcHeap;    size_t pendingNextOffset = gDvm.offJavaLangRefReference_pendingNext;    size_t referentOffset = gDvm.offJavaLangRefReference_referent;    Object *pending = dvmGetFieldObject(obj, pendingNextOffset);    Object *referent = dvmGetFieldObject(obj, referentOffset);    if (pending == NULL && referent != NULL && !isMarked(referent, ctx)) {        Object **list = NULL;        if (isSoftReference(obj)) {            list = &gcHeap->softReferences;        } else if (isWeakReference(obj)) {            list = &gcHeap->weakReferences;<span style="color:#FF0000;">        } else if (isFinalizerReference(obj)) {            list = &gcHeap->finalizerReferences;</span>        } else if (isPhantomReference(obj)) {            list = &gcHeap->phantomReferences;        }        assert(list != NULL);       <span style="color:#FF6666;"> enqueuePendingReference(obj, list);</span>    }}

至于isfinalizerReferenc(obj)的实现就很简单,只需要判断obj的clazz是否是java/lang/FinalizeReference即可。

enqueuPendingReference就是一个串链表的过程,这里就不贴代码了。

完成mark阶段以后,所有的reference都已经被被串在了一起,在remark完以后会真正的处理FinalizeReference中的object,把object取出来,mark 它然后把它从FinalizeReference中的引用中剥离出来,代码在: vm/alloc/marksweep.cpp中:

static void enqueueFinalizerReferences(Object **list){    assert(list != NULL);    GcMarkContext *ctx = &gDvm.gcHeap->markContext;    size_t referentOffset = gDvm.offJavaLangRefReference_referent;    size_t zombieOffset = gDvm.offJavaLangRefFinalizerReference_zombie;    bool hasEnqueued = false;    while (*list != NULL) {<span style="color:#FF0000;">        Object *ref = dequeuePendingReference(list);        Object *referent = dvmGetFieldObject(ref, referentOffset);        if (referent != NULL && !isMarked(referent, ctx)) {            markObject(referent, ctx);            /* If the referent is non-null the reference must queuable. */            assert(isEnqueuable(ref));            dvmSetFieldObject(ref, zombieOffset, referent);            clearReference(ref);            enqueueReference(ref);            hasEnqueued = true;        }</span>    }    if (hasEnqueued) {        processMarkStack(ctx);    }    assert(*list == NULL);}

有没有看到,红色的部分,正是从该FinalizeReference中取出了object,然后把referent从Referenct的referent域移到zombie域中,这两个域对对应于libcore FinalizeReference中的两个域。完了以后,还需要把该reference连到cleardReferences链表中:

/* * Schedules a reference to be appended to its reference queue. */static void enqueueReference(Object *ref){    assert(ref != NULL);    assert(dvmGetFieldObject(ref, gDvm.offJavaLangRefReference_queue) != NULL);    assert(dvmGetFieldObject(ref, gDvm.offJavaLangRefReference_queueNext) == NULL);<span style="color:#FF0000;">    enqueuePendingReference(ref, &gDvm.gcHeap->clearedReferences);</span>}

  这个cleardReference是干嘛用的呢? 答案看下面的函数:

void dvmEnqueueClearedReferences(Object **cleared){    assert(cleared != NULL);    if (*cleared != NULL) {        Thread *self = dvmThreadSelf();        assert(self != NULL);       <span style="color:#FF0000;"> Method *meth = gDvm.methJavaLangRefReferenceQueueAdd;</span>        assert(meth != NULL);        JValue unused;        Object *reference = *cleared;        <span style="color:#FF0000;">dvmCallMethod(self, meth, NULL, &unused, reference);</span>        *cleared = NULL;    }}  
    是的,它正是用来将所有需要释放的Refernce返回到java 层用的。

    至此,虚拟机内部对Finalize的处理已经结束,你是不是会很奇怪,因为搞了半天,貌似并没有地方真正地执行了object的finalize方法。

   是的,真正的Finalize方法不是由虚拟机执行的,虚拟机只是把reference链接到需要处理的表中,真正的执行由Finalize Daemon进程来处理。 Finalize Daemon是一个守护进程,在那里监控Finalize Reference的queue中是否有新的reference进来,如果是,就去执行该object的finalize方法,代码在 libcore/libdvm/src/main/java/java/lang/Daemons.jav中:

        @Override public void run() {            while (isRunning()) {                // Take a reference, blocking until one is ready or the thread should stop                try {                    <span style="color:#FF0000;">doFinalize((FinalizerReference<?>) queue.remove());</span>                } catch (InterruptedException ignored) {                }            }        }        @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")        private void doFinalize(FinalizerReference<?> reference) {   <span style="color:#FF0000;">         FinalizerReference.remove(reference);            Object object = reference.get();</span>            reference.clear();            try {                finalizingStartedNanos = System.nanoTime();                finalizingObject = object;                synchronized (FinalizerWatchdogDaemon.INSTANCE) {                    FinalizerWatchdogDaemon.INSTANCE.notify();                }         <span style="color:#FF0000;">       object.finalize();</span>            } catch (Throwable ex) {                // The RI silently swallows these, but Android has always logged.                System.logE("Uncaught exception thrown by finalizer", ex);            } finally {                finalizingObject = null;            }        }    }
   但是又是谁来把reference放到Finalize Reference的queue中的呢,我从libcore中看到了两条路径,一条是从上面的cleard refernce queue中,它会被另一个daemon,  ReferenceQueueDaemon处理,我们来看看它的处理过程:

    @Override public void run() {            while (isRunning()) {                Reference<?> list;                try {                    synchronized (ReferenceQueue.class) {                        while (ReferenceQueue.unenqueued == null) {                            ReferenceQueue.class.wait();                        }                        list = ReferenceQueue.unenqueued;                        ReferenceQueue.unenqueued = null;                    }                } catch (InterruptedException e) {                    continue;                }                enqueue(list);            }        }        private void enqueue(Reference<?> list) {            while (list != null) {                Reference<?> reference;                // pendingNext is owned by the GC so no synchronization is required                if (list == list.pendingNext) {                    reference = list;                    reference.pendingNext = null;                    list = null;                } else {                    reference = list.pendingNext;                    list.pendingNext = reference.pendingNext;                    reference.pendingNext = null;                }                <span style="color:#FF0000;">reference.enqueueInternal();</span>            }        }    }

看最后的enqueuIntenal函数,该函数的在Reference类里面:

    public final synchronized boolean enqueueInternal() {        if (queue != null && queueNext == null) {           <span style="color:#FF0000;"> queue.enqueue(this);</span>            queue = null;            return true;        }        return false;    }

对了,正是在该点,cleared queue中的Finalize Reference被转移到了Finalize 类中的queue中,然后被Finalize Daemon守护线程执行。


三  总结

  我们从虚拟机和libcore的角度分析了Finalize方法的执行流程,到现在我们可以得出以下两点结论:

    1. Finalize 方法被执行的时间不确定,所以不能依赖与它来释放紧缺的资源。时间不确定的原因是:

         a.  虚拟机调用GC的时间不确定

         b.  Finalize daemon线程被调度到的时间不确定

  2. Finalize方法只会被执行一次,即使对象被复活,如果已经执行过了Finalize方法,再次被gc时也不会再执行了,原因是:

       含有Finalize方法的object是在new的时候由虚拟机生成了一个Finalize reference在来引用到该Object的,而在Finalize方法执行的时候,该object所对应的Finalize Reference会被释放掉,即使在这个时候把该object复活(即用强引用引用住该object),再第二次被gc的时候由于没有了Finalize reference与之对应,所以Finalize方法不会再执行

  3. 含有Finalize方法的object需要至少经过两轮GC才有可能被释放(所以在对内存回收速度有要求的情况下,)

       

       

        

  


  

0 0