2011年3月1日火曜日

Android System.exit(int): 驚愕の実装

Android の System.exit(int) を実行すると、次の順序で処理が進む。

System.exit(int) of Android leads to the code flow shown as below.

  (1) java.lang.System.exit(int code)
  (2) java.lang.Runtime.exit(int code)
  (3) java.lang.Runtime.nativeExit(int code, boolean isExit)
  (4) Dalvik_java_lang_Runtime_nativeExit(const u4* args, JValue* pResult)
  (5) exit(int status)

何に驚愕したかというと、System.exit(int) を実行すると、Dalvik VM の終了処理など全くおこなわれず、Dalvik VM インタプリタの文脈でC言語インターフェースの exit(int) 関数が実行され、そこで Dalvik VM プロセスが終了してしまう点だ。Dalvik VM 終了処理関数である dvmShutdown() は呼ばれないので、dvmShutdown() 経由で呼び出されるはずのあらゆる終了処理が実行されない。メモリやスレッドなどのシステムリソースの解放処理が走ることはなく、それらの解放は完全にOS任せである。valgrind 下で実行すると、メモリリークが大量に検出される。OpenJDK の System.exit(int) が慎重に終了処理をおこなっているのとは大違いだ。やはり Dalvik VM は品質が良くない(「Android で OSGi を使うべきでない理由」も参照のこと)。

What made me aghast was that if System.exit(int) is executed, the shutdown steps of Dalvik VM are not performed at all and the C function "exit(int)" is executed in the context of the Dalvik VM interpreter and the Dalvik VM process terminates there. Because the shutdown function of Dalvik VM, dvmShutdown(), is not called, none of shutdown steps that are supposed to be called via dvmShutdown() is executed. No steps to release memory, threads and other system resources are executed, so freeing such resources is totally left to the OS. Many memory leaks are reported by valgrind. Great difference from OpenJDK whose System.exit(int) performs shutdown processing very carefully. After all, quality of Dalvik VM is not good (See "The reason that OSGi should not be used on Android", too).

 下記のコードは、Android における、System.exit(int) から exit(int) までのソースコードの流れである。

The code below is the flow from System.exit(int) to exit(int) in Android.

java.lang.System.exit(int code)

    public static void exit(int code) {
        Runtime.getRuntime().exit(code);
    }

java.lang.Runtime.exit(int code)

    public void exit(int code) {
        // Security checks
        SecurityManager smgr = System.getSecurityManager();
        if (smgr != null) {
            smgr.checkExit(code);
        }

        // Make sure we don't try this several times
        synchronized(this) {
            if (!shuttingDown) {
                shuttingDown = true;

                Thread[] hooks;
                synchronized (shutdownHooks) {
                    // create a copy of the hooks
                    hooks = new Thread[shutdownHooks.size()];
                    shutdownHooks.toArray(hooks);
                }

                // Start all shutdown hooks concurrently
                for (int i = 0; i < hooks.length; i++) {
                    hooks[i].start();
                }

                // Wait for all shutdown hooks to finish
                for (Thread hook : hooks) {
                    try {
                        hook.join();
                    } catch (InterruptedException ex) {
                        // Ignore, since we are at VM shutdown.
                    }
                }

                // Ensure finalization on exit, if requested
                if (finalizeOnExit) {
                    runFinalization(true);
                }

                // Get out of here finally...
                nativeExit(code, true);
            }
        }
    }

java.lang.Runtime.nativeExit(int code, boolean isExit)

    static void Dalvik_java_lang_Runtime_nativeExit(const u4* args, JValue* pResult)
    {
        int status = args[0];
        bool isExit = (args[1] != 0);

        if (isExit && gDvm.exitHook != NULL) {
            dvmChangeStatus(NULL, THREAD_NATIVE);
            (*gDvm.exitHook)(status);     // not expected to return
            dvmChangeStatus(NULL, THREAD_RUNNING);
            LOGW("JNI exit hook returned\n");
        }
        LOGD("Calling exit(%d)\n", status);
    #if defined(WITH_JIT) && defined(WITH_JIT_TUNING)
        dvmCompilerDumpStats();
    #endif
        exit(status);
    }


Dalvik VM に手を加えて、dvmShutdown() の文脈で何か処理をおこなおうとしても、アプリが System.exit(int) を呼ぶと dvmShutdown() 自体がスキップされるので要注意。 なお、下記のコードは dvmShutdown() の実装である。System.exit(int) で、この内容が全部スキップされるということである。

When you modify Dalvik VM to do something in the context of dvmShutdown(), you have to keep in mind that the entire dvmShutdown() function is skipped if System.exit(int) is called by applications. The code below is the implementation of dvmShutdown(). System.exit(int) skips all of the content.

void dvmShutdown(void)
{
    LOGV("VM shutting down\n");

    if (CALC_CACHE_STATS)
        dvmDumpAtomicCacheStats(gDvm.instanceofCache);

    /*
     * Stop our internal threads.
     */
    dvmGcThreadShutdown();

    if (gDvm.jdwpState != NULL)
        dvmJdwpShutdown(gDvm.jdwpState);
    free(gDvm.jdwpHost);
    gDvm.jdwpHost = NULL;
    free(gDvm.jniTrace);
    gDvm.jniTrace = NULL;
    free(gDvm.stackTraceFile);
    gDvm.stackTraceFile = NULL;

    /* tell signal catcher to shut down if it was started */
    dvmSignalCatcherShutdown();

    /* shut down stdout/stderr conversion */
    dvmStdioConverterShutdown();

#ifdef WITH_JIT
    if (gDvm.executionMode == kExecutionModeJit) {
        /* shut down the compiler thread */
        dvmCompilerShutdown();
    }
#endif

    /*
     * Kill any daemon threads that still exist.  Actively-running threads
     * are likely to crash the process if they continue to execute while
     * the VM shuts down.
     */
    dvmSlayDaemons();

    if (gDvm.verboseShutdown)
        LOGD("VM cleaning up\n");

    dvmDebuggerShutdown();
    dvmReflectShutdown();
    dvmProfilingShutdown();
    dvmJniShutdown();
    dvmStringInternShutdown();
    dvmExceptionShutdown();
    dvmThreadShutdown();
    dvmClassShutdown();
    dvmVerificationShutdown();
    dvmRegisterMapShutdown();
    dvmInstanceofShutdown();
    dvmInlineNativeShutdown();
    dvmGcShutdown();
    dvmAllocTrackerShutdown();
    dvmPropertiesShutdown();

    /* these must happen AFTER dvmClassShutdown has walked through class data */
    dvmNativeShutdown();
    dvmInternalNativeShutdown();

    free(gDvm.bootClassPathStr);
    free(gDvm.classPathStr);

    freeAssertionCtrl();

    /*
     * We want valgrind to report anything we forget to free as "definitely
     * lost".  If there's a pointer in the global chunk, it would be reported
     * as "still reachable".  Erasing the memory fixes this.
     *
     * This must be erased to zero if we want to restart the VM within this
     * process.
     */
    memset(&gDvm, 0xcd, sizeof(gDvm));
}