Android Art 虛擬機 GC 機制之 java 部落的崛起

前言

  在正式研究 android art 虛擬機的GC機制之前,必須要先了解 linux 的內存管理,是的,只需要瞭解,不必深入,畢竟 android 系統是基於 linux 系統開發出來的移動操作系統,而GC機制當然也是基於 linux 系統的內存管理開發出來的用戶態內存回收進制。除此之後,還要有一定的 linux 系統進程管理的基礎,在深入探究之前可自行先複習一下 linux 系統的進程管理和內存管理,加固基礎知識,在後期的研究會有事半功倍的效果,否則會不知所云,導致越到後面越沒興趣,最後放棄 android art 虛擬機的GC機制的學習。
  現在行業內也有不少相關的文章是分析 android art 虛擬機的內存回收機制的,有些也寫得不錯,比如老羅的ART運行時一系列的文章,結合源碼分析其原理,不過從頭看一次下來,能記住並且搞懂的內容還是比較少的。本文是隻針對整個GC機制從淺到深系統地整理一次,包括GC機制所涉及到的虛擬機相關知識,對比 linux 系統的進行系統整理,使得讀者更好理解。
  本文只做知識分享,給廣大的想要深入理解 GC 機制的開發者提供前車之鑑,帶大夥入個門,少走彎路,最後也感謝老羅的分享,在細節上分析得很好。
  給讀者的個人建議:最好是在深入 art 虛擬機的源碼之前先閱讀完本篇文章,對他有個大概的理論瞭解,否則會非常痛苦。

介紹

  Android GC 的全稱是 Android Gabage Collection,顧名思義是安卓系統的垃圾收集,爲什麼會有垃圾收集?衆所周知,Android 應用程序大部分都是基於 java 語言開發的,在編寫應用程序時不需要對對象進行內存釋放的操作,通常在 new 一個對象後,當不需要使用時只需要把該對象的引用重新賦值爲 Null 就可以了。這時 GC 就有存在的價值了,該對象的真正內存釋放是在 GC 過程中釋放掉了,不需要應用程序開發者操心。這就是 java 語言的核心之一,讓開發者有更多的精力去關注應用邏輯,而並不需要與 C/C++ 那樣考慮因內存沒有釋放導致內存泄漏的問題。
  但是當把對象的引用重新賦值爲 Null 時,並不會立即觸發 GC 釋放該對象的內存,那麼,是什麼時候把對象釋放掉的呢?這個問題會等閱讀完整篇文章後,你自然就有答案了。請跟隨我的腳步耐心閱讀下去。

1、java 對象的創建

  要想清楚知道GC是如何工作的,則必須先知道對象是如何創建的?java 對象在 linux 系統中是以什麼形狀存在的?
  首先,請讀者先以瞭解到的 linux 系統知識想像,或者猜測一下 Android 系統是如何在 new 一個對象時,爲該對象分配內存的?如果你猜到最終是通過 malloc 函數分配一塊內存的話,說明你的 linux 系統知識很紮實; 如果你猜到最終是通過 mmap 函數分配內存的話,說明你是 linux 高手。我們先不討論哪種是正確的,我們先回顧一下 linux 應用程序的內存分佈,畢竟 Android 應用程序也是跑在 linux 系統之上的,Android 應用程序的內存分佈同樣有 linux 應用程序的特性。如下圖所示,是從教科書上截下來的。
  這裏寫圖片描述
  Android 應用程序在啓動運行之後,其4G的內存分佈空間與 linux 應用程序基本一樣,有代碼段、數據段、和堆棧區域。與 linux 應用程序的唯一區別是,Android 應用程序是運行在 java 虛擬機之上的,如果你把 Android 應用程序與 java 虛擬機看作整體,其本質就是一個 linux 應用程序。好了,在這裏開始多出了一個 java 虛擬機。下面先介紹一下 java 虛擬機。

1.1、java 虛擬機

  目前 Android 系統中的 java 虛擬機有兩個,一個是dalvik,另一個是 art,在 Android 4.4 之後的版本都是使用 art 作爲 java 虛擬機。從網絡上搜索 java 虛擬機,就會有大量的介紹,這裏爲了加深印象,我自己總結了下什麼是 java 虛擬機,它是一個能夠爲 java 語言提供運行環境的程序,注意,這裏說的是程序。主要的功能是把 java 語言解釋成計算機可執行的機器碼,管理 java 程序中的對象內存分配與釋放等。也就是說,Android 系統的 java 程序都是運行在這個虛擬機(“程序”)之上的,以 Android 7.0 爲例,當 linux 內核啓動之後,首先通過 init 進程啓動各種守護進程,這裏面其中有個叫 zygote 的進程,其啓動的可執行文件以及參數都在 init.rc 文件中指定,以下是 Android 7.0 的 zygote 進程啓動參數

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks /sys/fs/cgroup/stune/foreground/tasks

  從啓動參數可以看到, zygote 進程其實是一個名爲 app_process64 的可執行程序,也就是說,zygote 進程的啓動入口是在 app_process64 的 main 函數,至於整個啓動過程這裏不一一細說,有興趣的可以閱讀源碼 (frameworks/base/cmds/app_process/app_main.cpp),這裏先大概描述一下它在整個Android 系統中的作用以及設計思想,然後結合部分關鍵代碼來了解 Android 系統中的 art 虛擬機。
  前面有提到, Android 系統的 Java 程序都是跑在虛擬機上的,那麼 Android 的應用程序都需要一個 Java 虛擬機的環境,或者到這裏有些讀者會搞不懂了,沒關係,先放下這個虛擬機環境的概念,我們先回顧一下 linux 應用程序的運行,如果我要寫一個簡單的能在arm平臺上運行的 hello world 的 linux C 程序,那麼首先我們先創建一個 C 文件 test.c,並且按照 C 語言的語法寫一個打印 hello world 程序,如下

 #include <stdio.h>

 int main()  
 {  
     printf("Hello World\n");
     return 0;
 }

接着,使用交叉編譯工具鏈把 test.c 編譯成可在 arm 平臺上可執行的機器碼,也就是可執行程序。

arm-linux-gcc -o hello hello.c

這樣直接放到 arm 平臺設備上直接執行了,這裏有一個交叉編譯工具鏈 arm-linux-gcc,這個工具的作用就是把我們熟悉的 C 代碼翻譯成arm 平臺可執行的機器碼,當然,我們知道使用不同的交叉編譯工具鏈最終會翻譯成不同平臺可執行的機器碼。這是我們在學校裏學習到的最熟悉的做法,那麼 java 語言又是怎麼被翻譯成機器碼的呢?這就需要 java 虛擬機了,它扮演的角色跟交叉編譯工具鏈相似,區別在於,交叉編譯工具鏈需要在 PC 機上預先把 C 語言翻譯成機器碼,然後再把可執行文件 hello 放在目標機器上,而 java 虛擬機則不需要,它是直接執行在目標機器上的,同時一邊執行 java 虛擬機的相關邏輯,一邊把待執行的 java 語言翻譯成機器碼,翻譯成機器碼後直接在目標機器上執行,所以在文章前面說 java 虛擬機是一個程序,是一個把可以明白 java 程序想要幹嘛,並且告訴機器 java 代碼的執行邏輯的程序,爲了加深理解,下面講個故事。
  現在有三個部落,爲了對號入座,把這三個部落命名爲 Java 部落,C 部落,和 Arm 部落,這三個部落講的話不一樣,誰都聽不懂誰的,在這三個部落中,Arm 部落製造鐵劍的環境很完善並且有充足的人力,不過缺少設計鐵劍的人才,Java 部落和 C 部落都擅長劍的設計,但製造鐵劍的環境很惡劣並且缺少人力,有一天,C 部落的人在討論爲什麼他們製造出來的劍沒有達到設計時的效果,其中有一個人提出了一個大膽的設想,乾脆把設計方案給 Arm 部落的人制造,他們那有更好的製造環境,於是他們便跑到 Arm 部落裏想讓他們幫忙按照 C 部落的要求製造一把鐵劍,由於他們語言不通,結果雞和鴨講,最後 C 部落的人回去後努力學習了 Arm 部落的語言,並且把製造步驟和設計圖紙按照 Arm 部落的語言寫下來,派了一個信使把這個設計方案送到了 Arm 部落,Arm 部落最後把 C 部落的鐵劍製造了出來。
同樣,Java 部落的國情與 C 部落是一樣的,但與 C 部落的做法不一樣,他們先派了個人去 Arm 部落學習語言和口語,後來這個人被兩個部落的人稱爲“Java 虛擬機”,很受人尊敬,於是每當 java 部落的鐵劍設計師需要找 Arm 部落的工匠製造鐵劍時,都會找 java 虛擬機做翻譯工作,就這樣,越來越多的,各種各樣的鐵劍一把一把地製造出來了。故事先講到這。
  Java 虛擬機到低是什麼時候運行在 Arm 平臺上的呢?前面有提到在 linux 內核啓動之後,由 init 進程創建了一個名叫 zygote 的進程,就是這個 zygote 進程啓動了 Java 虛擬機,以下是啓動過程的函數調用,紅色代表 java 代碼。Java 虛擬機在 AndroidRuntime.cpp 裏的 startVm 函數啓動,如果此時 java 虛擬機指定爲 art 虛擬機,則調用<android-root-dir>/art/runtime/runtime.cc 裏的 Create 函數正式啓動虛擬機,並且初始化所有的 java 運行時環境,這個過程非常複雜,想深入瞭解可參考《老羅的 Android 之旅》,本文只分析 art 虛擬機中的內存管理部分。
  zygote 進程啓動過程
  虛擬機啓動過程中,會初始化堆,GC,以及內存分配器等內存管理相關的內容,這裏先跳過虛擬機的啓動過程,先對整個 Android 運行狀態有個大概的瞭解,在虛擬機啓動之後,此時一切 java 語言的執行環境都準備就緒,接着 zygote 進程就開始運行 java 代碼了。ZygoteInit.java 文件中的 main 方法是 zygote 進程的 java 代碼入口,這裏會先創建本地 socket 用來 ActivitySerivce 請求應用進程創建,接着加載 /etc/compiled-classes 文件所列的所有 java 類,然後啓動 SystemServer 進程,最後進入等待循環,等待應用進程創建的請求。
  再對比一下 linux 進程,其實 zygote 進程與它沒什麼區別,其實質都是 linux 進程,只不過 zygote 進程在進入等待循環(“死循環”)之前運行了一段 java 虛擬機的相關代碼(下文把這種運行一段 java 虛擬機的相關代碼稱之爲“啓動 java 虛擬機”),用來創建 java 語言的執行環境。到這裏,有讀者可能會想到,zygote 進程之所以能運行 java 代碼是因爲 zygote 進程在開始時啓動了 java 虛擬機,那麼如果一個應用程序的進程是不是又要像 zygote 進程一樣,在啓動之初都需要啓動 java 虛擬機呢?
  如果你有這個問題拋出來,證明你已經對虛擬機有大致的瞭解了。至於這個問題我們先回顧一下 linux 進程創建子進程的做法。以下是一段C 語言創建子進程的代碼

#include<stdio.h>
static int value = 2;
static int showValue(int value)
{
        int ret = value * 2;
        return ret;
}
int main(void)
{
    printf("fork.\n");
    int pid = 0;
    value = 3;
    int child_value = 4;
    pid = fork();
    if (pid == 0) {
        printf("child=%d. child_value = %d, showValue = %d\n", getpid(), child_value, showValue(child_value));
        printf("child=%d. value = %d, showValue = %d\n", getpid(), value, showValue(value));
        return -1;
    }
    printf("father=%d. child_value = %d, showValue = %d\n", getpid(), child_value, showValue(child_value));
    printf("father=%d. value = %d, showValue = %d\n", getpid(), value, showValue(value));
    return 0;
}

   main 函數中創建了一個 child_value 的整形值,並初始化爲 4,修改了全局變量 value 的值爲 3,接着調用 fork 函數創建子進程,該函數返回的 pid 爲 0 時,說明正在運行的進程爲子進程,並把 fork 前創建的 child_value 的值和修改過的 value 的值打印出來,同時調用 showValue 對它乘以 2輸出打印。如果 pid 返回不爲 0 ,說明正在運行的進程爲父進程,同樣對 child_value 和 value 的值進行同樣的處理並打印,如下爲輸出結果

fork.
father=6081. child_value = 4, showValue = 8
father=6081. value = 3, showValue = 6
child=6082. child_value = 4, showValue = 8
child=6082. value = 3, showValue = 6

  從結果可以看出,無論是子進程還是父進程,child_value 和 value 的值都是 4 和 3,這就是著名的 fork 函數,爲什麼會這樣?是因爲子進程在創建時會拷貝一份父進程地址空間給自己作爲初始化地址空間,再深入的解釋就不解釋了,如果有 linux 基礎的人早就知道上段代碼的運行結果了,這裏我想說明的是,android 應用程序的進程正是利用這一特點繼承了 java 虛擬機的運行環境,這是如何實現的呢?上面提過到 zygote 進程在最後會進入等待循環,此時所有的 java 運行環境都創建好了,並保存在 zygote 進程的地址空間中,當有應用程序啓動時,會向 zygote 進程發出進程創建的申請,zygote 進程收到後最終調用 fork 函數創建進程,並把子進程的入口函數指向 java 代碼的 ActivityThread.java 中的main 函數,以下是僞代碼。

ZygoteInit.Main(){
    startVM();
    while(1){
        waitForCreateApp();  // 等待應用程序創建請求
        pid = fork();
        if(pid == 0) {    // 子進程創建成功
            ActivityThread.main(); // Android 應用程序的入口函數
            ...
        }
    }
}

  這樣 Android 應用進程就不需要重新啓動一次 java 虛擬機就啓動了一個 java 虛擬機,從此就可以執行 java 代碼了,由於所有 android 應用程序都是通過這種方式進行創建的,這樣就可以節省很多物理內存,因爲 java 虛擬機中只讀的那部分其實在物理內存中只保存一份,各應用程序對其共同享用。
  到這裏總結一下,Android 系統中,每一個 Android java 進程都對應一個 java 虛擬機實例,即每一個 Android java 進程都有一個單獨的 java 虛擬機,每一個 java 虛擬機都是 zygote 進程的 java 虛擬機的”副本”。爲了加深印象,接着上面的故事繼續類比。
  後來,java 部落派去 Arm 部落學習語言和口語的第一個人,即 java 虛擬機,一直生活在 Arm 部落裏,並組建了自己的家庭,他的後代也繼承了他的事業,並被外界稱爲 java 虛擬機實例,爲每一個 java 部落的鐵劍設計師做翻譯的工作,而最開始那們 java 虛擬機被封爲 zygote,從此 Java 部落的鐵劍越來越好,越來越多樣。
  讀完 java 部落的崛起一文後,相信大家對 java 虛擬機有了一定的瞭解,下篇開始介紹 java 虛擬機中資源(內存)管理的部分,敬請期待!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章