Android內存管理原理

一位大神的文章,詳細講解android內存管理的,在此轉載一下。

http://www.cnblogs.com/killmyday/archive/2013/06/12/3132518.html

一般來說,程序使用內存的方式遵循先向操作系統申請一塊內存,使用內存,使用完畢之後釋放內存歸還給操作系統。然而在傳統的C/C++等要求顯式釋放內存的編程語言中,記得在合適的時候釋放內存是一個很有難度的工作,因此Java等編程語言都提供了基於垃圾回收算法的內存管理機制:

  1. 垃圾內存回收算法

常見的垃圾回收算法有引用計數法(Reference Counting)、標註並清理(Mark and Sweep GC)、拷貝(Copying GC)和逐代回收(Generational GC)等算法,其中Android系統採用的是標註並刪除和拷貝GC,並不是大多數JVM實現裏採用的逐代回收算法。由於幾個算法各有優缺點,所以在很多垃圾回收實現中,常常可以看到將幾種算法合併使用的場景,本節將一一講解這幾個算法。

  1. 引用計數回收法(Reference Counting GC)

引用計數法的原理很簡單,即記錄每個對象被引用的次數。每當創建一個新的對象,或者將其它指針指向該對象時,引用計數都會累加一次;而每當將指向對象的指針移除時,引用計數都會遞減一次,當引用次數降爲0時,刪除對象並回收內存。採用這種算法的較出名的框架有微軟的COM框架,如代碼清單14 - 1演示了一個對象引用計數的增減方式。

代碼清單14 - 1 引用計數增減方式演示僞碼

Object *obj1 = new Object(); // obj1的引用計數爲1

Object *obj2 = obj1; // obj1的引用技術爲2

Object *obj3 = new Object();

 

obj2 = NULL; // obj1的引用計數遞減1次爲1

obj1 = obj3; // obj1的引用計數遞減1次爲0,可以回收其內存。

 

通常對象的引用計數都會跟對象放在一起,系統在分配完對象的內存後,返回的對象指針會跳過引用計數部分,如代碼清單14 - 1所示:

 14 - 1 採用引用計數對象的內存佈局示例

然而引用計數回收算法有一個很大的弱點,就是無法有效處理循環引用的問題,由於Android系統沒有使用該算法,所以這裏不做過多的描述,請有興趣的讀者自行查閱相關文檔。

  1. 標註並清理回收法(Mark and Sweep GC)

在這個算法中,程序在運行的過程中不停的創建新的對象並消耗內存,直到內存用光,這時再要創建新對象時,系統暫停其它組件的運行,觸發GC線程啓動垃圾回收過程。內存回收的原理很簡單,就是從所謂的"GC Roots"集合開始,將內存整個遍歷一次,保留所有可以被GC Roots直接或間接引用到的對象,而剩下的對象都當作垃圾對待並回收,如代碼清單14 - 3:

代碼清單14 - 2 標註並清理算法僞碼

void GC()

{

SuspendAllThreads();

 

List<Object> roots = GetRoots();

foreach ( Object root : roots ) {

Mark(root);

}

 

Sweep();

 

ResumeAllThreads();

}

算法通常分爲兩個主要的步驟:

  • 標註(Mark)階段:這個過程的僞碼如代碼清單14 - 2所示,針對GC Roots中的每一個對象,採用遞歸調用的方式(第8行)處理其直接和間接引用到的所有對象:

代碼清單14 - 3 標註並清理的標註階段僞碼

  1. void Mark(Object* pObj) {
  2. if ( !pObj->IsMarked() ) {
  3.      // 修改對象頭的Marked標誌

4.     pObj->Mark();

5.     // 深度優先遍歷對象引用到的所有對象

6.     List<Object *> fields = pObj->GetFields();

7.     foreach ( Object* field : fields ) {

8.     Make(field); // 遞歸處理引用到的對象

9.     }

10. }

11. }

如果對象引用的層次過深,遞歸調用消耗完虛擬機內GC線程的棧空間,從而導致棧空間溢出(StackOverflow)異常,爲了避免這種情況的發生,在具體實現時,通常是用一個叫做標註棧(Mark Stack)的數據結構來分解遞歸調用。一開始,標註棧(Mark Stack)的大小是固定的,但在一些極端情況下,如果標註棧的空間也不夠的話,則會分配一個新的標註棧(Mark Stack),並將新老棧用鏈表連接起來。

與引用計數法中對象的內存佈局類似,對象是否被標註的標誌也是保存在對象頭裏的,如圖 14 - 2所示。

 14 - 2 標註和清理算法中的對象佈局

如圖 14 - 2是垃圾回收前的對象之間的引用關係;GC線程遍歷完整個內存堆之後,標識出所以可以被"GC Roots"引用到的對象-即代碼清單14 - 2中的第4行,結果如圖 14 - 3中高亮的部分,對於所有未被引用到(即未被標註)的對象,都將其作爲垃圾收集。

 14 - 3 回收內存垃圾之前的對象引用關係

 

 14 - 4 GC線程標識出所有不能被回收的對象實例

  • 清理(SWEEP)階段:即執行垃圾回收過程,留下有用的對象,如圖 14 - 4所示,代碼清單14 - 3是這個過程的僞碼,在這個階段,GC線程遍歷整個內存,將所有沒有標註的對象(即垃圾)全部回收,並將保留下來的對象的標誌清除掉,以便下次GC過程中使用。

代碼清單14 - 4 標註和清理法中的清理過程僞碼

  1. void Sweep() {
  2. Object *pIter = GetHeapBegin();
  3.      while ( pIter < GetHeapEnd() ) {
  4.      if ( !pIter->IsMarked() )
  5.      Free(pIter);
  6.      else
  7.      pIter->UnMark();
  8.  

9.     pIter = MoveNext(pIter);

10.      }

11. }

 14 - 5 GC線程執行完垃圾回收過程後的對象圖

這個方法的優點是很好地處理了引用計數中的循環引用問題,而且在內存足夠的前提下,對程序幾乎沒有任何額外的性能開支(如不需要維護引用計數的代碼等),然而它的一個很大的缺點就是在執行垃圾回收過程中,需要中斷進程內其它組件的執行。

  1. 標註並整理回收法(Mark and COMPACT GC)

這個是前面標註並清理法的一個變種,系統在長時間運行的過程中,反覆分配和釋放內存很有可能會導致內存堆裏的碎片過多,從而影響分配效率,因此有些採用此算法的實現(Android系統中並沒有採用這個做法),在清理(SWEEP)過程中,還會執行內存中移動存活的對象,使其排列的更緊湊。在這種算法中,,虛擬機在內存中依次排列和保存對象,可以想象GC組件在內部保存了一個虛擬的指針 – 下個對象分配的起始位置 ,如圖 14 - 6中演示的示例應用,其GC內存堆中已經分配有3個對象,因此"下個對象分配的起始位置"指向已分配對象的末尾,新的對象"object 4"(虛線部分)的起始位置將從這裏開始。

這個內存分配機制和C/C++的malloc分配機制有很大的區別,在C/C++中分配一塊內存時,通常malloc函數需要遍歷一個"可用內存空間"鏈表,採取"first-first"(即返回第一塊大於內存分配請求大小的內存塊)或"best-fit"( 即返回大於內存分配請求大小的最小內存塊),無論是哪種機制,這個遍歷過程相對來說都是一個較爲耗時的時間。然而在Java語言中,理論上,爲一個對象分配內存的速度甚至可能比C/C++更快一些,這是因爲其只需要調整指針"下個對象分配的起始位置"的位置即可,據Sun的工程師估計,這個過程大概只需要執行10個左右的機器指令。

 14 - 6 GC中爲對象分配內存

由於虛擬機在給對象分配內存時,一直不停地向後遞增指針"下個對象分配的起始位置",潛臺詞就是將GC堆當做一個無限大的內存對待的,爲了滿足這個要求,GC線程在收集完垃圾內存之後,還需要壓縮內存 – 即移動存活的對象,將它們緊湊的排列在GC內存堆中,如圖 14 - 7是Java進程內GC前的內存佈局,執行回收過程時,GC線程從進程中所有的Java線程對象、各線程堆棧裏的局部變量、所有的靜態變量和JNI引用等GC Root開始遍歷。

圖 14 - 7中,可以被GC Root訪問到的對象有A、C、D、E、F、H六個對象,爲了避免內存碎片問題,和滿足快速分配對象的要求,GC線程移動這六個對象,使內存使用更爲緊湊,如圖 14 - 7所示。由於GC線程移動了存活下來對象的內存位置,其必須更新其他線程中對這些對象的引用,如圖 14 - 7中,由於A引用了E,移動之後,就必須更新這個引用,在更新過程中,必須中斷正在使用A的線程,防止其訪問到錯誤的內存位置而導致無法預料的錯誤。

 14 - 7 垃圾回收前的GC堆上的對象佈局及引用關係

 14 - 8 GC線程移動存活的對象使內存佈局更爲緊湊

注意現代操作系統中,針對C/C++的內存分配算法已經做了大量的改進,例如在Windows中,堆管理器提供了一個叫做"Look Aside List"的緩存針對大部分程序都是頻繁分配小塊內存的情形做的優化,具體技術細節請可以參閱筆者的在線付費技術視頻:

  1. 拷貝回收法(Copying GC)

這也是標註法的一個變種, GC內存堆實際上分成乒(ping)和乓(pong)兩部分。一開始,所有的內存分配請求都有乒(ping)部分滿足,其維護"下個對象分配的起始位置"指針,分配內存僅僅就是操作下這個指針而已,當乒(ping)的內存快用完時,採用標註(Mark)算法識別出存活的對象,如圖 14 - 9所示,並將它們拷貝到乓(pong)部分,後續的內存分配請求都在乓(pong)部分完成,如圖 14 - 10。而乓(pong)裏的內存用完後,再切換回乒(ping)部分,使用內存就跟打乒乓球一樣。

 14 - 9 拷貝回收法中的乒乓內存塊

 14 - 10 拷貝回收法中的切換乒乓內存塊以滿足內存分配請求

回收算法的優點在於內存分配速度快,而且還有可能實現低中斷,因爲在垃圾回收過程中,從一塊內存拷貝存活對象到另一塊內存的同時,還可以滿足新的內存分配請求,但其缺點是需要有額外的一個內存空間。不過對於回收算法的缺點,也可以通過操作系統地虛擬內存提供的地址空間申請和提交分佈操作的方式實現優化,因此在一些JVM實現中,其Eden區域內的垃圾回收採用此算法。

  1. 逐代回收法(Generational GC)

也是標註法的一個變種,標註法最大的問題就是中斷的時間過長,此算法是對標註法的優化基於下面幾個發現:

  • 大部分對象創建完很快就沒用了 – 即變成垃圾;
  • 每次GC收集的90%的對象都是上次GC後創建的;
  • 如果對象可以活過一個GC週期,那麼它在後續幾次GC中變成垃圾的機率很小,因此每次在GC過程中反覆標註和處理它是浪費時間。

可以將逐代回收法看成拷貝GC算法的一個擴展,一開始所有的對象都是分配在"年輕一代對象池" 中 – 在JVM中其被稱爲Young,如圖 14 - 11:

 14 - 11 逐代(generational GC中開始對象都是分配在年輕一代對象池(Young generation)中

第一次垃圾回收過後,垃圾回收算法一般採用標註並清理算法,存活的對象會移動到"老一代對象池"中– 在JVM中其被稱爲Tenured,如圖 14 - 12,而後面新創建的對象仍然在"年輕一代對象池"中創建,這樣進程不停地重複前面兩個步驟。等到"老一代對象池"也快要被填滿時,虛擬機此時再在"老一代對象池"中執行垃圾回收過程釋放內存。在逐代GC算法中,由於"年輕一代對象池"中的回收過程很快 – 只有很少的對象會存活,而執行時間較長的"老一代對象池"中的垃圾回收過程執行不頻繁,實現了很好的平衡,因此大部分虛擬機,如JVM、.NET的CLR都採用這種算法。

 14 - 12 逐代GC中將存活的對象挪到老一代對象池

在逐代GC中,有一個較棘手的問題需要處理 – 即如何處理老一代對象引用新一代對象的問題,如圖 14 - 13中。由於每次GC都是在單獨的對象池中執行的,當GC Root之一R3被釋放後,在"年輕一代對象池"中執行GC過程時,R3所引用的對象f、g、h、i和j都會被當做垃圾回收掉,這樣就導致"老一代對象池"中的對象c有一個無效引用。

 14 - 13 逐代GC中老一代對象引用新對象的問題

爲了避免這種情況,在"年輕一代對象池"中執行GC過程時,也需要將對象C當做GC Root之一。一個名爲"Card Table"的數據結構就是專門設計用來處理這種情況的,"Card Table"是一個位數組,每一個位都表示"老一代對象池"內存中一塊4KB的區域 – 之所以取4KB,是因爲大部分計算機系統中,內存頁大小就是4KB。當用戶代碼執行一個引用賦值(reference assignment)時,虛擬機(通常是JIT組件)不會直接修改內存,而是先將被賦值的內存地址與"老一代對象池"的地址空間做一次比較,如果要修改的內存地址是"老一代對象池"中的地址,虛擬機會修改"Card Table"對應的位爲 1,表示其對應的內存頁已經修改過 - 不乾淨(dirty)了,如圖 14 - 14。

 14 - 14 逐代GCCard Table數據結構示意圖

當需要在 "年輕一代對象池"中執行GC時, GC線程先查看"Card Table"中的位,找到不乾淨的內存頁,將該內存頁中的所有對象都加入GC Root。雖然初看起來,有點浪費, 但是據統計,通常從老一代的對象引用新一代對象的機率不超過1%,因此"Card Table"的算法是一小部分的時間損失換取空間。

  1. Android內存管理源碼分析

在Android中 ,實現了標註與清理(Mark and Sweep)和拷貝GC,但是具體使用什麼算法是在編譯期決定的,無法在運行的時候動態更換 – 至少在目前的版本上(4.2)還是這樣。在Android的dalvik虛擬機源碼的Android.mk文件(路徑是/dalvik/vm/Dvm.mk)裏,有類似代碼清單14 - 5的代碼,即如果在編譯dalvik虛擬機的命令中指明瞭"WITH_COPYING_GC"選項,則編譯"/dalvik/vm/alloc/Copying.cpp"源碼 – 此是Android中拷貝GC算法的實現,否則編譯"/dalvik/vm/alloc/HeapSource.cpp" – 其實現了標註與清理GC算法,也就是本節分析的重點。

代碼清單14 - 5 編譯器指定使用拷貝GC還是標註與清理GC算法

WITH_COPYING_GC := $(strip $(WITH_COPYING_GC))

 

ifeq ($(WITH_COPYING_GC),true)

LOCAL_CFLAGS += -DWITH_COPYING_GC

LOCAL_SRC_FILES += \

    alloc/Copying.cpp.arm

else

LOCAL_SRC_FILES += \

    alloc/DlMalloc.cpp \

    alloc/HeapSource.cpp \

    alloc/MarkSweep.cpp.arm

endif

注意本節中分析的Android源碼,可以在網址:http://androidxref.com/source/xref/ 中在線瀏覽。

 

  1. 在Java中,對象是分配在Java內存堆之上的,當Java程序啓動後,JVM會向操作系統申請保留一大塊連續的內存。

    在Android源碼中,這個過程分爲下面幾步:

    1. dvmStartup函數(/dalvik/vm/Init.cpp:1376)解析完傳入虛擬機的命令行參數,調用dvmGcStartup函數初始化GC組件。
    2. dvmGcStartup函數(/dalvik/vm/alloc/Alloc.cpp:30)負責初始化幾個GC線程同步原語,再調用dvmHeapStartup函數初始化GC內存堆(即Java內存堆)。
    3. dvmHeapStartup函數(/dalvik/vm/alloc/Heap.cpp:75)則根據GC參數設置調用dvmHeapSourceStartup函數向操作系統申請一大塊連續的內存空間,這個內存空間會自動增長,在默認設置中(/dalvik/vm/Init.cpp:1237),該內存堆的初始大小是2MB – 由gDvm.heapStartingSize指定,內存堆最大不超過16MB(Java程序用完這16MB內存就會導致OOM異常) – 由gDvm.heapGrowthLimit指定,如果gDvm.heapGrowthLimit的值爲0的話(即表示可以無限增長),則將最大值限定爲gDvm.heapMaximumSize的值。申請完內存空間之後,初始化一個名爲clearedReferences的隊列(/dalvik/vm/alloc/Heap.cpp:98),這個隊列將用在保存finalizable對象,以在另一個線程中執行它們的finalize函數。最後,dvmHeapStartup函數還要初始化數據結構Card Table(/dalvik/vm/alloc/Heap.cpp:100),如代碼清單14 - 6。

代碼清單14 - 6 dvmHeapStartup初始化GC內存堆

75 bool dvmHeapStartup()

76 {

77 GcHeap *gcHeap;

78

79 if (gDvm.heapGrowthLimit == 0) {

80 gDvm.heapGrowthLimit = gDvm.heapMaximumSize;

81 }

82

83 gcHeap = dvmHeapSourceStartup(gDvm.heapStartingSize,

84 gDvm.heapMaximumSize,

85 gDvm.heapGrowthLimit);

86 if (gcHeap == NULL) {

87 return false;

88 }

89 gcHeap->ddmHpifWhen = 0;

90 gcHeap->ddmHpsgWhen = 0;

91 gcHeap->ddmHpsgWhat = 0;

92 gcHeap->ddmNhsgWhen = 0;

93 gcHeap->ddmNhsgWhat = 0;

94 gDvm.gcHeap = gcHeap;

95

96 /* Set up the lists we'll use for cleared reference objects.

97 */

98 gcHeap->clearedReferences = NULL;

99

100 if (!dvmCardTableStartup(gDvm.heapMaximumSize, gDvm.heapGrowthLimit)) {

101 LOGE_HEAP("card table startup failed.");

102 return false;

103 }

104

105 return true;

106 }

 

  1. dvmHeapSourceStartup函數(/dalvik/vm/alloc/HeapSource.cpp:541)通過dvmAllocRegion函數向操作系統申請保留一大塊連續的內存地址空間,其大小是內存堆最大可能的大小(/dalvik/vm/alloc/HeapSource.cpp:563),成功後,再根據內存堆的初始大小申請內存。如默認情況下,Java內存堆的初始大小是2MB,而最大能增長到16MB,那麼一開始dvmHeapSourceStartup會申請16MB大小的地址空間,但一開始只分配2MB的內存備用。在底層內存實現上,Android系統使用的是dlmalloc實現-又叫msspace,這是一個輕量級的malloc實現。

    除了創建和初始化用於存儲普通Java對象的內存堆,Android還創建三個額外的內存堆:用來存放堆上內存被佔用情況的位圖索引"livebits"、在GC時用於標註存活對象的位圖索引"markbits",和用來在GC中遍歷存活對象引用的標註棧(Mark Stack)。

    dvmHeapSourceStartup函數運行完成後,HeapSource、Heap、livebits、markbits以及mark stack等數據結構的關係如圖 14 - 15所示。

 14 - 15 GC堆上HeapSourceHeap等數據結構的關係

其中虛擬機通過一個名爲gHs的全局HeapSource變量來操控GC內存堆,而HeapSource裏通過heaps數組可以管理多個堆(Heap),以滿足動態調整GC內存堆大小的要求。另外HeapSource裏還維護一個名爲"livebits"的位圖索引,以跟蹤各個堆(Heap)的內存使用情況。剩下兩個數據結構"markstack"和"markbits"都是用在垃圾回收階段,後面會講解。

  1. 而dvmAllocRegion函數(/dalvik/vm/Misc.cpp:612)則通過ashmem和mmap兩個系統調用分配內存地址空間,其中ashmem是Android系統對Linux的一個擴展,而mmap則是Linux系統提供的系統調用,請讀者自行搜索參閱相關文檔瞭解其用法。
  2. 這些步驟做完之後,一個Android應用的內存情況如圖 14 - 16所示,虛線是應用實際申請的地址空間範圍,而實線部分則是已經分配的內存:

 14 - 16 GC向操作系統申請地址空間和內存

  1. 當需要應用需要分配內存,即通過"new"關鍵字創建一個實例時,在Android源碼的過程大致如下:
    1. 首先虛擬機在執行Java class文件時,遇到"new "或" newarray"指令(所有的Java字節指令碼請參考維基百科:http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings),表示要創建一個對象或者數組的實例,這裏爲了簡單起見,我們只看新建一個對象實例的情形。
    2. 虛擬機的JIT編譯器執行"new"指令,針對不同的CPU架構,"new"指令都有相應的機器碼與其對應,如ARM架構,JIT執行/dalvik/vm/mterp/armv5te/OP_NEW_INSTANCE.S中的機器碼;而x86架構,則是/dalvik/vm/mterp/x86/OP_NEW_INSTANCE.S中的機器碼。"OP_NEW_INSTANCE"函數的工作就是加載"new"指令的對象類型參數,獲取對象需要佔用的內存大小信息,然後調用"dvmAllocObject"分配必要的內存(/dalvik/vm/mterp/armv5te/OP_NEW_INSTANCE.S:29),當然還會處理必要的異常。
    3. dvmAllocObject函數(/dalvik/vm/alloc/Alloc.cpp:181)調用dvmMalloc根據對象大小分配內存空間,成功後,調用對象的構造函數初始化實例(/dalvik/vm/alloc/Alloc.cpp:191)。
  2. 程序在運行的過程中不停的創建新的對象並消耗內存,直到GC內存用光,這時再要創建新對象時,就會觸發GC線程啓動垃圾回收過程,在Android源碼中:
    1. dvmMalloc函數(/dalvik/vm/alloc/Heap.cpp:333)直接將分配內存的工作委託給函數tryMalloc。
    2. tryMalloc函數(/dalvik/vm/alloc/Heap.cpp:178)首先嚐試用dvmHeapSourceAlloc函數分配內存,如果失敗的話,喚醒或創建GC線程執行垃圾回收過程,並等待其完成後重試dvmHeapSourceAlloc(/dalvik/vm/alloc/Heap.cpp:201);如果dvmHeapSourceAlloc再次失敗,說明當前GC堆中大部分對象都是存活的,那麼調用dvmHeapSourceAllocAndGrow(/dalvik/vm/alloc/Heap.cpp:222)嘗試擴大GC內存堆 – 前面說過,一開始GC堆會根據初始大小向操作系統申請保留一塊內存,如果這塊內存用完了,GC堆就會再次向操作系統申請一塊內存,直到用完限額。
    3. dvmMalloc函數根據內存分配是否成功來執行相應的操作,如內存分配失敗時,拋出OOM(Out Of Memory)異常(/dalvik/vm/alloc/Heap.cpp:383)。
  3. Android源碼中垃圾回收過程大致如下:
    1. dvmCollectGarbageInternal函數(/dalvik/vm/alloc/Heap.cpp:440)開始垃圾回收過程,其首先調用dvmSuspendAllThreads(/dalvik/vm/Thread.cpp:2539)暫停系統中除與調試器溝通的其他所有線程(/dalvik/vm/alloc/Heap.cpp:462);
    2. 如果沒有啓用並行GC的話,虛擬機會提高GC線程的優先級,以防止GC線程被其它線程佔用CPU。
    3. 接下來調用dvmHeapMarkRootSet函數(/dalvik/vm/alloc/Heap.cpp:488)來遍歷所有可從GC Root訪問到的對象列表,dvmHeapMarkRootSet函數(/dalvik/vm/alloc/MarkSweep.cpp:181)的註釋中也列出了GC Root列表。其調用dvmVisitRoot遍歷GC Roots,代碼清單14 - 1是dvmVisitRoot的源碼(/dalvik/vm/alloc/Visit.cpp:212),筆者在其中以註釋的方式批註關鍵代碼。完整的GC Root列表有興趣的讀者可以參閱鏈接:http://help.eclipse.org/indigo/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fgcroots.html

代碼清單14 - 7 在虛擬機中通過dvmVisitRoot遍歷GC Roots

//

// visitor是一個回調函數,dvmHeapMarkRootSet傳進來的是rootMarkObjectVisitors

// (位於/dalvik/vm/alloc/MarkSweep.cpp:145),這個回調函數的作用就是標註(Mark

// 所有的GC root,並將它們的指針壓入標註棧(Mark Stack)中。

//

// 第二個參數arg實際上是GcMarkContext對象,用於找到GC Roots後,回傳給回調函數visitor

// 的參數。

//

void dvmVisitRoots(RootVisitor *visitor, void *arg)

{

assert(visitor != NULL);

// 所有已加載的類型都是GC Roots,這也意味着類型中所有的靜態變量都是GC Roots

visitHashTable(visitor, gDvm.loadedClasses, ROOT_STICKY_CLASS, arg);

 

// 基本類型也是GC Roots,包括

// void, boolean, byte, short, char, int, long, float, double

visitPrimitiveTypes(visitor, arg);

 

// 調試器對象註冊表中的對象(debugger object registry),這些對象

// 基本上是調試器創建的,因此不能把它們當作垃圾回收了,否則調試器

// 就無法正常工作了。

if (gDvm.dbgRegistry != NULL) {

visitHashTable(visitor, gDvm.dbgRegistry, ROOT_DEBUGGER, arg);

}

 

// 所有interned的字符串,interned string是虛擬機中保證的只有唯一一份拷貝的字符串

if (gDvm.literalStrings != NULL) {

visitHashTable(visitor, gDvm.literalStrings, ROOT_INTERNED_STRING, arg);

}

    

// 所有的JNI全局引用對象(JNI global references),JNI全局引用對象是

// JNI代碼中,通過NewGlobalRef函數創建的對象

dvmLockMutex(&gDvm.jniGlobalRefLock);

visitIndirectRefTable(visitor, &gDvm.jniGlobalRefTable,, ROOT_JNI_GLOBAL, arg);

dvmUnlockMutex(&gDvm.jniGlobalRefLock);

 

// 所有的JNI局部引用對象(JNI local references

// 關於JNI局部和全部變量的使用,可以參考下面的網頁鏈接:

// http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/refs.html

dvmLockMutex(&gDvm.jniPinRefLock);

visitReferenceTable(visitor, &gDvm.jniPinRefTable,, ROOT_VM_INTERNAL, arg);

dvmUnlockMutex(&gDvm.jniPinRefLock);

 

// 所有線程堆棧上的局部變量和其它對象,如線程本地存儲裏的對象等等

visitThreads(visitor, arg);

 

// 特殊的異常對象,如OOM異常對象需要在內存不夠的時候創建,爲了防止內存不夠而無法創建

// OOM對象,因此虛擬機會在啓動時事先創建這些對象。

(*visitor)(&gDvm.outOfMemoryObj,, ROOT_VM_INTERNAL, arg);

(*visitor)(&gDvm.internalErrorObj,, ROOT_VM_INTERNAL, arg);

(*visitor)(&gDvm.noClassDefFoundErrorObj,, ROOT_VM_INTERNAL, arg);

}

dvmHeapMarkRootSet是執行標註過程的主要代碼,在前文說過,通常的實現會在對象實例前面放置一個對象頭,裏面會存放是否標註過的標誌,而在Android系統裏,採取的是分離式策略,而是將標註用的標誌位放到HeapSource裏的"markbits"這個位圖索引結構,筆者猜測這麼做的目的是爲了節省內存。圖 14 - 17是dvmHeapMarkRootSet函數快要標註完存活對象時(正在標註最後一個對象H),GC內存堆的數據結構。

 14 - 17 GC執行完標註過程後的HeapSource結構

其中"livebits"位圖索引還是維護堆上已用的內存信息;而"markbits"這個位圖索引則指向存活的對象,在圖 14 - 17中, A、C、F、G、H對象需要保留,因此"markbits"分別指向他們(最後的H對象尚在標註過程中,因此沒有指針指向它);而"markstack"就是在標註過程中跟蹤當前需要處理的對象要用到的標誌棧了,此時其保存了正在處理的對象F、G和H。

  1. 在標註(Mark)過程中,調用dvmHeapScanMarkedObjects和dvmHeapProcessReferences函數(/dalvik/vm/alloc/MarkSweep.cpp:776)將實現了finalizer的對象添加到finalizer對象隊列中,以便在下次GC中執行這些對象的finalize函數。
  2. 標識出所有的垃圾內存之後,調用dvmHeapSweepSystemWeaks和dvmHeapSweepUnmarkedObjects(/dalvik/vm/alloc/MarkSweep.cpp:902)等函數清理內存,但並不壓縮內存,這是因爲Android的GC是基於dlmalloc之上實現的,GC將所有的內存分配和釋放的操作都轉交給dlmalloc來處理。在這個過程中, Android系統不做壓縮內存處理,據說是爲了節省執行的CPU指令,從而達到延長電池壽命的目的,因此dvmCollectGarbageInternal做了一個小技巧,調用dvmHeapSourceSwapBitmaps函數(/dalvik/vm/alloc/Heap.cpp:575)將"livebits"和"markbits"的指針互換,這樣就不需要在清理完垃圾對象後再次維護"livebits"位圖索引了,如圖 14 - 18所示:

 14 - 18 GC清理完內存後堆上的數據結構

  1. 做完上面的操作之後,GC線程再通過dvmResumeAllThreads函數喚醒所有的線程(/dalvik/vm/alloc/Heap.cpp:624)。
  1. 雖然GC可以自動回收不再使用的內存,但有很多資源是虛擬機也無法管理的,如進程打開的數據庫連接、網絡端口以及文件等。針對這些資源,GC線程可以在垃圾回收過程中,標示出其是垃圾,需要釋放,但是卻不清楚如何釋放它們,因此Java對象提供了一個名爲finalize的函數,以便對象實現自定義的清除資源的邏輯。

    如代碼清單14 - 1是一個實現finalize函數的對象,在Java中,finalize對象定義在System.Object類中,即意味着所有對象都有這個函數,當子類重載了這個函數,即向虛擬機表明自己需要與其他類型區別對待。

代碼清單14 - 8 實現finalize函數的簡單對象

1    class DemoClass {

2     public int X;

3    

4     public void testMethod() {

5         System.out.println("X: " + new Integer(X).toString());

6     }

7    

8     @Override

9     protected void finalize () throws Throwable {

10         System.out.println("finalize函數被調用了!");

11         // 實現自定義的資源清除邏輯!

12         super.finalize();

13     }

14    }

一些有C++編程經驗的讀者可能很容易將finalize函數與析構函數對應起來,但是兩者是完全不同的東西,在C++中,調用了析構函數之後,對象就被釋放了,然而在Java中,如果一個類型實現了finalize函數,其會帶來一些不利影響,首先對象的存活週期會更長,至少需要兩次垃圾回收才能銷燬對象;第二對象同時會延長其所引用到的對象存活週期。如代碼清單14 - 2中(示例代碼javagc-simple)在第3行創建並使用了DemoClass以在內存中生成一些垃圾,並執行三次GC。

代碼清單14 - 9 實現finalize函數的簡單對象

1    public class gcdemo {

2     public static void main(String[] args) throws Exception {

3         generateGarbage();

4         System.gc();

5         Thread.sleep(1000);

6    

7         System.gc();

8         Thread.sleep(1000);

9    

10         System.gc();

11         Thread.sleep(1000);

12     }

13    

14     public static void generateGarbage() {

15         DemoClass g = new DemoClass();

16         g.X =123;

17         g.testMethod();

18     }

19    }

連接好設備,打開logcat日誌,並執行示例代碼根目錄中的run.sh,得到的輸出類似圖 14 - 8,每一行輸出對應代碼清單14 - 2中的一次System.gc調用,可以看到第一次GC過程中釋放了223個對象,如果運行示例程序javagc,會發現第一次GC之後,DemoClass的finalize函數就會被調用 – 爲了避免System.out.println中的字符串對象影響GC的輸出,圖 14 - 8是javagc-simple的輸出結果。第二次GC過程中又釋放了34個對象,其中就有DemoClass的實例,以及其所引用到的其它對象。這時所有垃圾對象都被回收了,因此在執行第三次GC過程時,沒有回收到任何內存。

 14 - 19 程序中使用了實現finalize函數對象之後實施三次GC的結果

前文講到Android源碼中通過dvmHeapScanMarkedObjects函數在GC堆上掃描垃圾對象,並將finalizable對象添加到finalize隊列中,其具體過程如下:

  1. dvmHeapScanMarkedObjects函數(/dalvik/vm/alloc/MarkSweep.cpp:595)將所有識別出來的可以被GC Root引用的對象放到名爲"mark stack"的堆棧中,再調用processMarkStack函數處理需要特殊處理的對象。
  2. processMarkStack函數(/dalvik/vm/alloc/MarkSweep.cpp:471)調用scanObject函數處理"mark stack"中的每個對象。
  3. scanObject函數(/dalvik/vm/alloc/MarkSweep.cpp:454)首先判斷對象是保存Java類型信息的類型對象,還是數組對象,還是普通的Java對象,針對這三種對象進行不同的處理。由於finalize對象是普通的Java對象,因此這裏我們只看相應的scanDataObject函數。
  4. scanDataObject函數(/dalvik/vm/alloc/MarkSweep.cpp:438)先掃描對象的各個成員,並標記其所有引用到的對象,最後調用delayReferenceReferent函數根據對象的類型,將其放入相應的待釋放隊列中,如對象是fianlizeable對象的話,則放入finalizerReferences隊列中(/dalvik/vm/alloc/MarkSweep.cpp:426);如對象是WeakReference對象的話,則將其放入weakReferences隊列中(/dalvik/vm/alloc/MarkSweep.cpp:424)。
  5. dvmHeapProcessReferences函數(/dalvik/vm/alloc/MarkSweep.cpp#776)在垃圾對象收集完畢後,負責將finalize隊列從虛擬機的native端傳遞到Java端。其調用enqueueFinalizerReferences函數通過JNI方式將finalize對象的引用傳遞到Java端的一個java.lang.ref.ReferenceQueue當中,詳細的調用方式請參見enqueueFinalizerReferences函數(/dalvik/vm/alloc/MarkSweep.cpp:729)和enqueueReference函數(/dalvik/vm/alloc/MarkSweep.cpp:653)。
  6. 而在JVM虛擬機啓動時,dvmStartup函數(/dalvik/vm/Init.cpp:1557)會在準備好Java程序運行所需的所有環境之後,調用dvmGcStartupClasses函數(/dalvik/vm/alloca/Alloc.cpp:71)啓動幾個與GC相關的後臺Java線程 ,這些線程在java.lang.Daemons中定義(/libcore/luni/src/main/java/java/lang/Daemons.java),其中一個線程就是執行java對象finalize函數的HeapWorker線程,之所以要將收集到的java finalize對象引用從虛擬機(native)一端傳遞到Java端,是因爲finalize函數是由java語言編寫的,函數裏可能會用到很多java對象。這也是爲什麼如果對象實現了finalize函數,不僅會使其生命週期至少延長一個GC過程,而且也會延長其所引用到的對象的生命週期,從而給內存造成了不必要的壓力。

發佈了16 篇原創文章 · 獲贊 7 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章