Android內存管理分析總結

提綱.png

一.操作系統相關基礎知識

1.物理內存、虛擬內存、邏輯地址與交換空間

物理內存(RAM):加載到內存地址寄存器中的內存又叫“硬件內存”,是內存單元真正的地址(也叫物理地址)。RAM作為進程運行不可或缺的資源,對系統和穩定性有著決定性的影響。另外,RAM的一部分被操作系統留作他用,比如顯存等。

邏輯地址:由CPU控制生成的地址,是一個程序級別的概念。這里引用一個淺顯的例子——我們在C語言指針編程中,可以讀取指針變量本身的值(&操作),這里取得的值就是邏輯地址——也就是說,這個(&操作)取得的值是CPU控制生成的一個邏輯地址,并不是這個指針變量在RAM中的真正地址。
??那么,我們為什么要這么一個并不是真正地址的邏輯地址呢?深層次的原因這里不予以探究,但是一個比較淺顯的原因就是,邏輯地址的分配非常靈活——在一個數組中,我們通過邏輯地址可以保證數組中元素地址的連續性。當然這個邏輯地址最終還是要通過一定的方式映射到RAM中的物理地址上,這個物理地址才是元素存儲的真正地址,而這個物理地址,不一定是連續的。

虛擬內存:是操作系統級別的概念,指計算機呈現出要比實際擁有的內存大得多的內存量。它使得每個應用程序都認為自己擁有獨立且連續的可用的內存空間(一段連續完整的地址空間),這個內存大小跟操作系統的位數有關。比如32位系統,邏輯內存的最大為2^23。而實際上,它通常是被映射到多個物理內存段(在真正的物理地址上不一定是連續的),還有部分暫時存儲在外部磁盤存儲器上,在需要時再加載到內存中來。
??上一段我們我們說了半天的邏輯地址,筆者的理解就是虛擬內存中的地址。OK,現在我們知道了虛擬內存有兩個特點——一個是在虛擬內存中虛擬地址/邏輯地址是連續的,便于靈活分配;二是虛擬內存可以是計算機呈現出比實際內存大的多的內存。那么為什么虛擬內存會呈現出這么大的內存的神奇功能呢?或者說這多出來的額內存是哪來的?這就要用到我們接下來講的交換(Swap)空間

交換(Swap)空間:在系統中運行的每個進程都需要使用到內存,但不是每個進程都需要每時每刻使用系統分配的內存空間。當系統運行所需內存超過實際的物理內存,內核會釋放某些進程所占用但未使用的部分或所有物理內存,將這部分釋放的數據存儲在磁盤上直到進程下一次調用,并將釋放出的內存提供給有需要的進程使用。
??引用一個容易理解但不是很恰當的比喻:你不需要很長的軌道就可以讓一列火車從上海開到北京。你只需要足夠長的鐵軌(比如說3公里)就可以完成這個任務。采取的方法是把后面的鐵軌立刻鋪到火車的前面,只要你的操作足夠快并能滿足要求,列車就能象在一條完整的軌道上運行。
??swap和虛擬內存結伴而來的。如果系統是64位,最大虛擬內存可以是2的64次方,沒有計算機會有這么大的內存。當內存不夠用的時候只能映射到磁盤。linux專門開辟了一個swap磁盤分區,當物理內存不夠用的時候(程序并不知道),將內存中很久不使用的內存區域交換到swap區。也即是說:用作虛擬內存的磁盤空間稱為交換空間(swap空間)。

2.進程的地址空間

在32位操作系統中,進程的地址空間是0到4GB。這里我們需要強調一點:進程所擁有的內存空間指的是“虛擬內存”,虛擬地址/邏輯地址與進程息息相關,不同進程里同一個虛擬地址指向的物理地址不一定是相同的,所以離開進程談虛擬內存沒有任何意義。下圖展示了Linux進程地址空間(虛擬內存)的組成:

進程內存示意圖.png
Stack(棧)與Heap(堆)

Stack空間(進棧和出棧)由操作系統控制,其主要儲存函數地址、函數參數、局部變量等等,所以Stack空間不需要很大,一般幾MB大小。
??Heap空間由程序員控制,主要包括實例域、靜態域、數組元素等,儲存空間比較大,一般為幾百NB到幾GB。正是由于Heap空間由程序員管理,所以容易出現使用不當的問題(如內存泄漏),當然,Heap內存也是系統GC發生的區域。

3.Android中的進程

Android中的進程主要分為native進程和Java進程——native進程指的是采用C/C++實現的,不包括Dalvik實例的Linux進程java進程:實例化了Dalvik虛擬機的Linux進程,我們開發的APP就是出于java進程中的。
??我們上面說過,Heap(堆)內存是由程序員控制的。我們使用malloc、C++ new和java new所申請的空間都是heap空間,只不過C/C++申請的內存空間在native heap中,而java申請的內存空間則在dalvik heap中。在平時的開發中,我們打交道的最多的就是dalvik heap,我們的實例域、靜態域、數組元素等都是在dalvik的heap中,虛擬機的GC也發生在其中。

二. DVM(Dalvik虛擬機)

1.DVM與JVM

(1)什么JVM?

JVM是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。它有自己完善的(虛擬)硬件架構(如處理器、堆棧、寄存器等),還具有相應的指令系統。使用“Java虛擬機”程序就是為了支持與操作系統無關、在任何系統中都可以運行的程序
??Java語言的一個非常重要的特點就是與平臺的無關性。而使用Java虛擬機是實現這一特點的關鍵。一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入Java語言虛擬機后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼,就可以在多種平臺上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。這就是Java能夠“一次編譯,到處運行”的原因。

(2)什么是DVM?

Dalvik是Google公司自己設計用于Android平臺的Java虛擬機,也就是說,本質上,Dalvik也是一個Java虛擬機。是Android中Java程序的運行基礎。
??其指令集基于寄存器架構,執行其特有的文件格式——dex字節碼來完成對象生命周期管理、堆棧管理、線程管理、安全異常管理、垃圾回收等重要功能。它的核心內容是實現庫(libdvm.so),大體由C語言實現。

(3)JVM 與 Dalvik VM的關系

上面我們說JVM是一個虛構出來的計算機,這種說法并不是很準確——嚴格來說,JVM是一種規范,或者說實現這種規范的實例,從這個角度來說,Dalvik VM也是一個 特殊的JVM,這點在上面也有提到。說的更通俗一點,能符合規范正確執行Java的.class文件的就是JVM;那么Android開發包中的dx與Dalvik VM結合起來,就可以看成是一個JVM了(要把一個東西稱為“JVM”必須要通過JCK(Java Compliance Kit)的測試并獲得授權后才能行,所以嚴格來說dx + Dalvik VM不能叫做JVM,因為沒授權)。

2.JVM和DVM的區別與聯系

(1).Dalvik VM是基于寄存器的架構(reg based),而JVM是堆棧結構(stack based)。

這里的寄存器架構和堆棧結構指的是計算機指令系統,計算機指令系統分為四種:堆棧型,累加器型,寄存器-儲存器型和寄存器-寄存器型。四種分類的依據是操作數的來源。堆棧型默認的操作數都在棧頂,累加器型默認一個操作數是累加器,寄存器-存儲器型的操作數可以是寄存器或者內存。寄存器-寄存器型除了訪存指令,操作數都是寄存器。
??x86一開始并沒有使用太多的通用寄存器,原因之一(注意,只是之一)是當時的編譯器無力進行寄存器分配,讓編譯器自動決定程序中眾多變量哪些應該裝入寄存器、哪些應該換出、哪些變量應該映射到同一個寄存器上,并不是一件易事,JVM采用堆棧結構的原因之一就是不信任編譯器的寄存器分配能力,轉而使用堆棧結構,躲開寄存器分配的難題。
??如今的CPU早就有足夠的晶體管來支持復雜設計,為了性能著想,大量使用寄存器型的指令,原因在于寄存器離CPU最近,所以延時最短,取指最快,有利于主頻提高。

那么基于棧與基于寄存器的架構,誰更快呢?intel的X86還保留有累加器指令和堆棧型指令,這是為了歷史兼容。很多現今的處理器,除了load和store指令訪存外,只支持對寄存器操作,不支持對堆棧以及內存的直接操作——這也從側面反映出基于寄存器比基于棧的架構更與實際的處理器接近

①.dvm速度快!寄存器存取速度比棧快的多,dvm可以根據硬件實現最大的優化,比較適合移動設備。JAVA虛擬機基于棧結構,程序在運行時虛擬機需要頻繁的從棧上讀取寫入數據,這個過程需要更多的指令分派與內存訪問次數,會耗費很多CPU時間。
??②.指令數小!dvm基于寄存器,所以它的指令是二地址和三地址混合,指令中指明了操作數的地址;jvm基于棧,它的指令是零地址,指令的操作數對象默認是操作數棧中的幾個位置。這樣帶來的結果就是dvm的指令數相對于jvm的指令數會小很多,jvm需要多條指令而dvm可能只需要一條指令。

(2).Dalvik 執行速度比 JVM 快,但移植性稍差.

Dalvik 執行速度比 JVM 快的原因,上面已經做了一些說明,這里在綜合移植性說一下。在一個解釋器上執行VM指令,包含三個步驟:指令分派、訪問操作數和執行計算。

①.指令分派
??指令分派負責從內存中讀取 VM 指令,然后跳轉到相應的解釋器代碼中。上面提到過,完成同樣的事情,基于棧的虛擬機需要更多的指令,意味著更多的指令分派和 內存訪問次數,這是 JVM 的執行性能不如 Dalvik VM 的原因之一。

②.訪問操作數
??訪問操作數是指讀取和寫回源操作數和目的操作數。Dalvik VM 通過虛擬寄存器來訪問操作數, 由于具有相近的血緣, Dalvik 的虛擬寄存器在映射到物理寄存器方面具有更充分的優勢, 這也是 Dalvik VM 性能較佳的一個原因。
??JVM 的操作數通過操作數棧來訪問, 而因為指令中沒有使用任何通用寄存器,在虛擬機的實現中可以比較自由的分配實際機器的寄存器,因而可移植性高。
??作為一個優化,操作數棧也可以由編譯器映射到物理寄存器上,減少數據移動的開銷。

③.指令執行

(3).Dalvik執行的是特有的DEX文件格式,而JVM運行的是.class文件格式.*

在Java程序中,Java類會被編譯成一個或多個class文件,然后打包到jar文件中,接著Java虛擬機會從相應的class文件和jar文件中獲取對應的字節碼;Android應用雖然也使用Java語言,但是在編譯成class文件后,還會通過DEX工具將所有的class文件轉換成一個dex文件,Dalvik虛擬機再從中讀取指令和數據。

JVM與DVM字節碼.png

優勢:
??class文件去冗余:class文件存在很多的冗余信息,dex工具會去除冗余信息(多個class中的字符串常量合并為一個,比如對于Ljava/lang/Oject字符常量,每個class文件基本都有該字符常量,存在很大的冗余),并把所有的.class文件整合到.dex文件中。減少了I/O操作,提高了類的查找速度。

缺點:
??方法數受限:多個class文件變成一個dex文件所帶來的問題就是方法數超過65535時報錯,由此引出MultiDex技術。

dex與class.jpg

可以看到,這里最終生成了一個.odex文件,odex是為了在運行過程中進一步提高性能,對dex文件的進一步優化,優化后的文件大小會有所增加,應該是原DEX文件的1-4倍。

更多關于.class文件與.dex文件的知識可以參照這篇文章:深入理解Android(二):Java虛擬機Dalvik,這篇文章中對這兩個文件的結構做了非常詳細的分析,這里就不多說了。

(4).Dalvik可以允許多個instance 運行,也就是說每一個Android 的App是獨立跑在一個VM中.
一個應用,一個進程,一個Dalvik!

Zygote是一個虛擬機進程,同時也是一個虛擬機實例的孵化器,它通過init進程啟動。首先會孵化出System_Server,他是android絕大多系統服務的守護進程,它會監聽socket等待請求命令,當有一個應用程序啟動時,就會向它發出請求,zygote就會FORK出一個新的應用程序進程。
??這樣做的好處是:Zygote進程是在系統啟動時產生的,它會完成虛擬機的初始化,庫的加載,預置類庫的加載和初始化等等操作,而在系統需要一個新的虛擬機實例時,Zygote通過復制自身,最快速的提供個進程;另外,對于一些只讀的系統庫,所有虛擬機實例都和Zygote共享一塊內存區域,大大節省了內存開銷。
??每一個app啟動的時候,就會有自己的進程與Dalvik虛擬機實例。而這樣做的好處是一個App crash只會影響到自身的VM,不會影響到其他。 Dalvik的設計是每一個Dalvik的VM都是Linux下面的一個進程。那么這就需要高效的IPC。另外每一個VM是單獨運行的好處還有可以動態active/deactive自己的VM而不會影響到其他VM。

3.Android為什么會出現OOM?

通過上面的介紹,我們對Dalvik虛擬機已經有了一些初步的了解,現在我門回到Android的內存管理中來。前面我們說過Heap(堆)內存是由程序員控制的,用C/C++申請的內存空間在native heap中,而java申請的內存空間則在dalvik heap中
??那么為什么會出現OOM的情況呢?這個是因為Android系統對dalvik虛擬機的heap大小作了硬性限制,當java進程申請的空間超過這個閾值時,就會拋出OOM異常(這個閾值可以是48M、24M、16M等,視機型而定)。
??也就是說,程序發生OMM并不表示RAM不足,而是因為程序申請的java heap對象超過了dalvik vm heapgrowthlimit。也就是說,在RAM充足的情況下,也可能發生OOM。
??這樣設計的目的是為了讓Android系統能同時讓比較多的進程常駐內存(RAM),這樣程序啟動時就不用每次都重新加載到內存,能夠給用戶更快的響應。迫使每個應用程序使用較小的內存,移動設備非常有限的RAM就能使比較多的app常駐其中

java程序發生OMM并不是表示RAM不足,如果RAM真的不足,Android的memory killer會起作用,當RAM所剩不多時,memory killer會殺死一些優先級比較低的進程來釋放物理內存,讓高優先級程序得到更多的內存

三.JVM / Dalvik VM垃圾回收機制

這里為什么要講JVM的垃圾回收機制?——前面我們說過,Davlik VM本質上也是一種JVM,因此我們從大處著手,并在其中我們會穿插講解一下Dalvik VM在具體實現上的一些不同。

1.JVM的基本架構

如圖,Java VM規則將JVM所管理的內存分為以下幾個部分:

JVM體系結構.png
(1).方法區

各個線程所共享的,用于存儲已被虛擬機加載類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。根據Java虛擬機規范的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

常量池

運行時常量池是方法區的一部分。用于存放編譯器生成的各種字面量和符號引用。運行期間也可以將新的常量放入常量池中,用得比較多的就是String類的intern()方法,當一個String實例調用intern時,Java查找常量池中是否有相同的Unicode的字符串常量,若有,則返回其引用;若沒有,則在常量池中增加一個Unicode等于該實例字符串并返回它的引用。

(2).Java堆
① Java堆

虛擬機管理內存中最大的一塊,被所有線程共享該區域用于存放對象實例,幾乎所有的對象(實例變量,數組)都在該區域分配,是內存回收的主要區域。每個對象都包含一個與之對應的class信息(我們常說的類類型,Clazz.getClass()等方式獲取)。【這里的“對象”,不包括基本數據類型】
??從內存回收角度看,由于現在的收集器大都采用分代收集算法,所以Java堆還可以細分為:新生代和老年代,再細分一點的話可以分為Eden空間、From Survivor空間、To Survivor空間等(這個后面會講)。根據Java虛擬機規范規定,Java堆可以處于物理上不連續的空間,只要邏輯上是連續的就行。如果在堆中沒有內存可分配時,并且堆也無法擴展時,將會拋出OutOfMemoryError異常。

② Dalvik堆

Dalvik VM的堆結構相對于JVM的堆結構有所區別,只而主要體現在Dalvik將堆分成了Active堆Zygote堆。之前我們有說過,這個Zygote是一個虛擬機進程,同時也是一個虛擬機實例的孵化器——那么同樣的,zygote堆是Zygote進程在啟動時的預加載的類、資源和對象;除此之外所有的對象,包括我們在代碼中創建的實例、靜態域和數組,都是儲存在Active堆里邊的
??為什么要把Dalvik堆分成Zygote堆和Active堆?這主要是因為Android通過fork方法創建一個新的zygote進程,為了盡可能的避免父進程和子進程之間的數據拷貝,fork方法使用寫時拷貝技術簡單講就是fork的時候不立即拷貝父進程的數據到子進程中,而是在子進程或者父進程對內存進行寫操作時才對內容進行復制
??Dalvik的Zygote堆存放的預加載類都是Android核心類和Java運行時庫,這部分很少被修改,大多數情況下父進程和子進程共享這塊區域,因此沒有必要對這部分類進行垃圾回收之類的修改,直接復制即可。而Active堆作為我們程序代碼中創建實例對象的存放堆,是垃圾回收的重點區域,因此將兩個堆分開。

(3).Java 棧 / Java虛擬機棧(Java Virtual Machine Stacks)

線程私有,它的生命周期與線程相同。 Java虛擬機棧描述的是Java方法(區別于native的本地方法)執行的內存模型每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動作鏈接、方法出口等信息每個方法被調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程
??我們所用的大多數 JVM 都是基于 Java 棧的運行機制,而有一個例外的實現,Google 移動設備操作系統 Android 的虛擬機 Dalvik 則是基于寄存器的機制(Dalvik 雖然支持 Java 語言開發,但從虛擬機的角度看,并不符合 Java VM 標準),關于虛擬機實現時,棧和寄存器機制的比較,請參考論文“Virtual Machine Showdown: Stack Versus Registers”;

Java棧,劃分為操作數棧、棧幀數據和局部變量區,方法中分配的局部變量在棧中,同時每一次方法的調用都會在棧中分配棧幀。對于基于棧的 Java 虛擬機,方法的調用和執行伴隨著壓棧和出棧操作。每個線程有各自獨立的棧,由虛擬機來管理棧的大小,但我們應該對它的大小有個概念。棧的大小是把雙刃劍,如果太小,可能會導致棧溢出,特別是在該線程內有遞歸、大的循環時出現溢出的可能性更大,如果過大,就會影響到可創建棧的數量,如果是多線程的應用,就會導致內存溢出。

來看一段字節碼在 Java 棧中的執行示例,100 與 98 相加:

iload_0   // 載入局部變量 0,整型,壓入棧中
iload_1   // 載入局部變量 1,整型,壓入棧中
iadd      // 彈出兩個整型數,相加,將結果壓入棧
istore_2  // 彈出整型數,存入局部變量 2
整數加法運算 Java 棧行為.jpg
(4).本地方法棧(Native Method Stacks)

本地方法棧與Java虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機所使用到的Native方法服務。說白了,這是 Java 調用操作系統本地庫的地方,用來實現 JNI(Java Native Interface,Java 本地接口)

(5).程序計數器(Program Counter Register)

程序計數器是一塊較小的內存空間,它的作用可以看做是當前線程所執行字節碼的行號指示器,字節碼解釋器工作時通過改變該計數器的值來選擇下一條需要執行的字節碼指令。是線程私有,生命周期與線程相同。

2.垃圾回收算法相關

(1).可回收對象的判定
①.引用計數算法

給對象添加一個引用計數器,每當有一個地方引用它的時候,計數器的值就加1;當引用失效的時候,計數器的值就減1;任何時刻計數器為0的對象是不可能再被引用的。
??這種方法實現簡單,判斷效率也很高;但是該算法有一個致命的缺點就是難以解決對象相互引用的問題:試想有兩個對象,相互持有對方的引用,而沒有別的對象引用到這兩者,那么這兩個對象就是無用的對象,理應被回收,但是由于他們互相持有對方的引用,因此他們的引用計數器不為0,因此他們不能被回收。

②.可達性分析算法

為了解決上面循環引用的問題,Java采用了一種全新的算法——可達性分析算法。這個算法的核心思想是,通過一系列稱為“GC Roots”的對象作為起始點,從這些結點開始向下搜索,搜索所走過的路徑成為“引用鏈”,當一個對象到GC Roots沒有一個對象相連時,則證明此對象是不可用的(不可達)。

可達性分析.png

在Java語言中,可作為GC Roots的對象包括下面幾種:

  • 上面說的JVM棧(棧幀數據中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • Native 方法棧中JNI引用的對象。

需要注意一點,即使在可達性分析算法中不可達對象,也并非是“非死不可”的,要真正宣告一個對象的死亡,至少需要經歷兩次標記的過程
??如果一個對象在進行可達性分析之后發現沒有與GC Roots相連的引用鏈,那么他將會第一次標記并且****。當對象沒有復寫finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機講著兩種情況都視為“沒有必要執行finalize()方法”
??如果這個對象被判定為有必要執行finalize()方法,那么這個對象會被加入一個“F-Queue”隊列中,并在稍后由一個虛擬機建立的、優先級低的Finalize線程,去觸發這個方法,但并不承諾會等待他運行結束。
??finalize()方法是對象逃脫死亡厄運的最后一次機會,稍后的GC會對在“F-Queue”隊列中的對象進行第二次小規模的標記;
??如果對象要在finalize()中拯救自己,只需要重新與引用鏈上的對象就行關聯即可,那么在第二次標記時它將被移出“即將回收”的集合;
??如果對象這個時候還是沒有逃脫,那基本上他就真的被回收了。

③.引用

無論是引用計數法還是可達性分析算法,判斷對象的存活與否都與“引用”有關。在JDK1.2之前,“引用”的解釋為:如果reference類型的數據中儲存的數值代表的是另外一塊內存的起始地址,就稱這個數據代表著一個引用。在JDK1.2之后,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用

  • 強引用:就是指在程序代碼之中普遍存在的,類似于“Object obj = new Object();”這樣的引用,只要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象。
  • 軟引用:用來描述一些還有用但并非必須的對象。對于軟引用關聯的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收的范圍,進行第二次回收——如果這次回收還沒有騰出足夠的內存,才會內存溢出拋出異常。在JDK1.2之后,提供了SoftReference來實現軟引用。
  • 弱引用:也是用來描述非必須對象的,但是他的強度比軟引用更弱一些。被弱引用引用的對象,只能生存到下一次GC之前,當GC發生時,無論無論當前內存是否足夠,都會回收掉被弱引用關聯的對象。JDK1.2之后,提供了WeakRefernce類來實現弱引用。
  • 虛引用:是最弱的一種引用,一個對象有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個對象的實例。為一個對象設置一個虛引用關聯的唯一目的就是能夠在這個對象唄收集器回收的的時候收到一個系統的通知
(2).Stop The World

有了上面的垃圾對象的判定,我們還要考慮一個問題,那就是Stop The World。垃圾回收的時候,需要保持整個引用狀態不變:假設一個對象沒有被標記到,或者沒有與GC Roots產生關聯,那么他被判定為垃圾,需要回收;但是等我一會執行回收的時候,他又被別的對象引用了——這樣的話整個GC過程就無法執行了。
??因此,在GC的過程中,其他所有程序進程處于暫停狀態,也就是俗稱的卡住了。所有的GC卡頓問題均由此而來,這個暫時無法解決。幸運的是,這個卡頓是非常短暫的,尤其是在Java堆的新生代(待會會講),對程序的影響微乎其微。

(3).幾種辣雞回收算法
①.標記清除算法 (Mark-Sweep)

標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間。
??這種算法的缺點是容易產生內存碎片,碎片太多可能會導致后續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作

標記清除算法.png
②.復制算法 (Copying)

復制算法將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用的另一半內存空間中的對象一次性全部清理掉,這樣一來就不容易出現內存碎片的問題。
??這種算法的優點就是,實現簡單,運行高效且不容易產生內存碎片;缺點也顯而易見:將可用內存縮小為了原來的一半,代價非常高昂。
??從算法原理我們可以看出Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那么Copying算法的效率將會大大降低(要復制的對象比較多)。

復制算法.png
③.標記整理算法 (Mark-Compact)

該算法標記階段和Mark-Sweep一樣,但是在完成標記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動,然后清理掉端邊界以外的內存
??這種算法特別適用于存活對象多,回收對象少的情況,因為回收的對象少,標記完了之后需要移動的對象就相對較少。

標記整理算法.png
④.分代回收算法

當前的商業虛擬機的垃圾收集器都采用“分代收集”算法,這種算法并沒有什么新的思想,只是根據對象的存活的周期不同將內存劃分為幾塊。
??前面我們說過,復制算法:適用于存活對象很少,回收對象多;標記整理算法:適用于存活對象多,回收對象很少的情況。這兩種算法情況正好互補!
??一般情況下我們把Java的對分為新生代和老年代,在新生代,每次垃圾收集時,都會有大批的對象死去,只有少量存活,因此適用復制算法;而在老年代,因為對象存活率高、沒有額外的空間對它進行分配擔保,就必須使用“標記-清除”或者“標記-整理”算法來進行回收。

3.Java堆內存模型

上面我們已經簡略的說過Java堆和Dalvik堆的區別,這里我們復習一下:Java堆用于存放對象實例,幾乎所有的對象(實例變量,數組)都在該區域分配,是內存回收的主要區域;Dalvik將堆分成了Active堆和Zygote堆,zygote堆是Zygote進程在啟動時的預加載的類、資源和對象;除此之外所有的對象,包括我們在代碼中創建的實例、靜態域和數組,都是儲存在Active堆里邊。

  • Java堆按照對象存活的時間可分為新生代和老年代
  • 新生代又分為三個部分:一個內存較大的Eden區,和兩個內存較小且大小相同的Survivor區,比例為8:1:1.
  • Eden區存放新生的對象
  • Survivor存放每次垃圾回收后存活的對象
新生代老生帶.png
(1).新生代又分為三個部分:一個內存較大的Eden區,和兩個內存較小且大小相同的Survivor區,比例為8:1:1.

對象的內存分配,主要分配在新生代的Eden(伊甸園)區上,當Eden區沒有足夠的空間進行分配時,虛擬機將發起一次“復制算法”的GC,在這個過程中,存活下來的對象被放到Survivor 0區;當第二次GC來臨的時候,Survivor 0空間的存活對象也需要再次用復制算法,放到Survivor 1空間,二把剛剛分配對象的Survivor 0空間和Eden空間清除;第三次GC時,又把Survivor 1空間的存活對象復制到Survivor 0的空間,就這樣來回倒騰。
??通過上面的分析我們不難理解新生代為什么這么分配了:Eden區是對象分配的主要區域,這是很頻繁的,尤其是大量的局部變量產生的臨時對象,因此他占的比例為8/10,至于為什么是8,這個我也不是很清楚,我們只需要知道這個區域確實占了很大比例就行;這個區域分配的對象大多數都是“朝生夕滅”,因此存活下來的對象較少,故采用“復制算法”; 至于兩個Survivor的比例為什么是1:1,這個應該很好理解。

(2).什么樣的對象會被移入老生帶?
①.新生代中經歷過15次GC的對象

虛擬機給每個對象定義了一個對象年齡計數器,如果對象在Eden出生并經過第一次GC后仍然存活,將被移動到Survivor空間中,并且對象的年齡設為1;對象在Survivor區中每“熬過”一個GC,年齡就增加1歲,當它年齡增加到一定程度(默認為15歲),就會晉升到老年帶中。

②.大對象直接進入老年代

所謂大對象是指,需要連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組,虛擬機提供了一個PretenureSizeThreshold參數,令大于這個這個值的對象直接在老生代中分配。這樣做主要是為了避免在Eden區和兩個Survivor區之間復制算法執行的時候產生大量的內存復制。

4.觸發GC的類型

了解這些是為了解決實際問題,Java虛擬機會把每次觸發GC的信息打印出來來幫助我們分析問題,所以掌握觸發GC的類型是分析日志的基礎。

GC_FOR_MALLOC: 表示是在堆上分配對象時內存不足觸發的GC。
GC_CONCURRENT: 當我們應用程序的堆內存達到一定量,或者可以理解為快要滿的時候,系統會自動觸發GC操作來釋放內存。
GC_EXPLICIT: 表示是應用程序調用System.gc、VMRuntime.gc接口或者收到SIGUSR1信號時觸發的GC。
GC_BEFORE_OOM: 表示是在準備拋OOM異常之前進行的最后努力而觸發的GC。

5.安卓分配與回收

Android系統并不會對Heap中空閑內存區域做碎片整理。系統僅僅會在新的內存分配之前判斷Heap的尾端剩余空間是否足夠,如果空間不夠會觸發gc操作,從而騰出更多空閑的內存空間
??在Android的高級系統版本里面針對Heap空間有一個Generational Heap Memory的模型,這個思想和JVM的逐代回收法很類似,就是最近分配的對象會存放在Young Generation區域,當這個對象在這個區域停留的時間達到一定程度,它會被移動到Old Generation,最后累積一定時間再移動到Permanent Generation區域。系統會根據內存中不同的內存數據類型分別執行不同的gc操作。

站在巨人的肩膀上摘蘋果:
理解Java垃圾回收機制
從虛擬機視角談 Java 應用性能優化
Dalvik 虛擬機和 Sun JVM 在架構和執行方面有什么本質區別?
JVM、DVM(Dalvik VM)和ART虛擬機對比
《深入理解Java虛擬機 第2版》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容