Android 垃圾回收机制★★★
1.垃圾回收機制
垃圾回收,也叫GC(Garbage Collection),指的是釋放垃圾占用的空間,防止內存泄露。有效的使用可以使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。
我們知道JVM的內存區域主要分為程序計數器、虛擬機棧、本地方法棧、方法區、堆。那么哪個才是GC作用的區域呢?答案是堆區,前面幾塊數據區域都不進行 GC。對象實例和數組都是在堆上分配的,GC 也主要對這兩類數據進行回收。
一般來說,程序使用內存的方式遵循先向操作系統申請一塊內存、使用內存、使用完畢之后釋放內存歸還給操作系統。在傳統的C/C++等要求顯式釋放內存的編程語言中,記得在合適的時候釋放內存。而Java等編程語言都提供了基于垃圾回收算法的內存管理機制,我們不需要手動釋放對象的內存,JVM 中的垃圾回收器(Garbage Collector)會自動回收。
Android如今使用的虛擬機名叫Android Runtime,簡稱Art,而Art的其中一大職責就是負責垃圾回收。Art會在適當的時機觸發GC操作,一旦進行GC操作,就會將一些不再使用的對象進行回收。
?
2.如何判定垃圾
目前主要有兩種判定算法:引用計數算法和可達性分析算法。Art采用的是第二種算法。
①引用計數算法
引用計數算法通過在對象頭中分配一個空間來保存該對象被引用的次數。如果該對象被其它對象引用,則它的引用計數加1,如果刪除對該對象的引用,那么它的引用計數就減1,當該對象的引用計數為0時,該對象就會被回收。
需要說明的是,引用有四種類型分別是強引用、軟引用、弱引用和虛引用。引用的類型會影響到垃圾的回收。
(1)強引用:通過new來創建一個新對象時返回的引用就是一個強引用,若一個對象通過一系列強引用可到達,它就是強可達的(strongly reachable),那么它就不可能被系統垃圾回收機制回收。
(2)軟引用:垃圾回收機制運行時,系統內存空間足夠不會被回收,不足夠會被回收。軟引用和弱引用的區別在于,若一個對象是弱引用可達,無論當前內存是否充足它都會被回收,而軟引用可達的對象在內存不充足時才會被回收,因此軟引用要比弱引用“強”一些;
(3)弱引用:垃圾回收機制運行時,不管系統內存是否足夠,都會被回收。
(4)虛引用:幾乎等于沒有引用,以至于我們通過虛引用甚至無法獲取到被引用的對象,虛引用存在的唯一作用就是當它指向的對象被回收后,虛引用本身會被加入到引用隊列中,用作記錄它指向的對象已被回收。
下面通過實例來演示和說明:
String obj = new String("Android");
該段代碼先創建一個字符串Android,其內存分在堆中,并且這個時候"Android"有一個引用,就是obj,其指向字符串Android。
如果此時將obj設置為null,這時候“Android”字符串的引用次數就為0了,在引用計數垃圾回收中,意味著此時就要進行垃圾回收了。
obj = null;
此時演示的示意圖如下所示,即將進行垃圾回收。
引用計數算法有一個致命問題就是不能解決循環引用問題。
②可達性分析算法
可達性算法的原理是以一系列叫做 GC Root 的對象為起點出發,引出它們指向的下一個節點,再以下個節點為起點,引出此節點指向的下一個結點(這樣通過 GC Root 串成的一條線就叫引用鏈),直到所有的結點都遍歷完畢。如果相關對象不在任意一個以 GC Root 為起點的引用鏈中,則這些對象會被判斷為垃圾,會被 GC 回收。
?如上圖所示,用可達性算法可以解決Java對象循環引用導致的引用計數法無法回收的問題。因為從GC Root出發沒有到達obj5和obj6的有效路徑,所以obj5和obj6可以回收。
obj5和obj6對象可被回收,就一定會被GC回收嗎?并不是,對象從判定可回收到回收需要經歷下面兩個階段:
第一個階段是可達性分析,分析該對象是否可達。
第二個階段是當對象沒有重寫finalize()方法或者finalize()方法已經被調用過,虛擬機認為該對象不可以被救活,因此回收該對象。(finalize()方法在垃圾回收中的作用是,給該對象一次救活的機會)。當發生GC時,會先判斷對象是否執行了 finalize 方法,如果未執行,則會先執行 finalize 方法,我們可以在此方法里將當前對象與 GC Roots 關聯,這樣執行 finalize 方法之后,GC 會再次判斷對象是否可達,如果不可達,則會被回收,如果可達,則不回收!
注意: finalize 方法只會被執行一次,如果第一次執行 finalize 方法此對象變成了可達確實不會回收,但如果對象再次被 GC,則會忽略 finalize 方法,對象會被回收!這一點切記!
有了上面垃圾對象的判定,還要考慮一個問題,就是stop the world,因為垃圾回收的時候,需要整個的引用狀態保持不變,否則判定是垃圾。所以,GC的時候,其他所有的程序執行都要處于暫停狀態,卡住了。幸運的是,這個卡頓是非常短的,對程序的影響也是微乎其微,所以GC的卡頓問題由此而來,也是無可避免的。
?
3.GC Root對象
通過可達性算法,成功解決了引用計數所無法解決的問題-“循環依賴”,只要無法與 GC Root 建立直接或間接的連接,系統就會判定為可回收對象。這樣就引申出了另一個問題,哪些屬于 GC Root?
在 Java 語言中,可作為 GC Root 的對象包括以下4種:
①虛擬機棧(棧幀中的局部變量表)中引用的對象
②方法區中類靜態屬性引用的對象
③方法區中常量引用的對象
④本地方法棧中 JNI(即一般說的 Native 方法)引用的對象
注意:全局變量同靜態變量不同,它不會被當作 GC Root。
下面分別介紹這4種GC Root:
①虛擬機棧(幀棧中局部變量表)中引用的對象
public class GCRootStackLocaltable {
? ? public GCRootStackLocaltable(String name){? ?
? ? }? ??
? ? public static void main(String[] args){
? ? ? ? GCRootStackLocaltable obj = new GCRootStackLocaltable("Localtable");
? ? ? ? obj = null;
? ? }??
}
上面實例中的obj即為GC Root,當obj置為null時,Localtable對象也斷掉了與 GC Root 的引用鏈,該對象將被回收。
②方法區中類靜態屬性引用的對象
public class GCRootMethodAreaStaticPro {
? ? public static GCRootMethodAreaStaticPro instance;?
? ? public GCRootMethodAreaStaticPro(String name) {? ? ??
? ? }
? ? @SuppressWarnings("static-access")
? ? public static void main(String[] args){
? ? ? ? GCRootMethodAreaStaticPro obj = new GCRootMethodAreaStaticPro("Localtable");
? ? ? ? obj.instance = new GCRootMethodAreaStaticPro("staticProperty");
? ? ? ? obj = null;
? ? }?
}
此時上面實例中的obj為GC Root,當obj置為null后,經過GC垃圾回收,obj所指向的Localtable對象由于無法與 GC Root 建立關系被回收。而instance作為類的靜態屬性,也屬于GC Root,staticProperty對象依然與 GC Root 建立著連接,所以此時 staticProperty 對象并不會被回收。注意這里的GC Root是誰。
③方法區中常量引用的對象
public class GCRootMethodAreaConstat {
? ? public static final GCRootMethodAreaConstat mFinalObj = new GCRootMethodAreaConstat("objFinal");? ? ? ? ?? ? ? ??
? ? public GCRootMethodAreaConstat(String name){? ? ??
? ? }??
? ? public static void main(String[] args){
? ? ? ? GCRootMethodAreaConstat obj = new GCRootMethodAreaConstat("Localtable");
? ? ? ? obj = null;
? ? }
}
此時實例中的mFinalObj為方法區中的常量的引用,作為GC Root使用。此時的obj也為GC Root(虛擬機棧局部變量表),當obj置為null后,Localtable與GC Root斷開將會被回收,但是objFinal不會被回收。
④本地方法棧中引用的對象
所謂本地方法就是一個 java 調用非 java 代碼的接口,該方法并非 Java 實現的,可能由 C 或 Python等其他語言實現的, Java 通過 JNI 來調用本地方法, 而本地方法是以庫文件的形式存放的(在 WINDOWS 平臺上是 DLL 文件形式,在 UNIX 機器上是 SO 文件形式)。通過調用本地的庫文件的內部方法,使 JAVA 可以實現和本地機器的緊密聯系,調用系統級的各接口方法。
任何 Native 接口都會使用某種本地方法棧,實現的本地方法接口是使用 C 連接模型的話,那么它的本地方法棧就是 C 棧。當線程調用 Java 方法時,虛擬機會創建一個新的棧幀并壓入 Java 棧。然而當它調用的是本地方法時,虛擬機會保持 Java 棧不變,不再在線程的 Java 棧中壓入新的幀,虛擬機只是簡單地動態連接并直接調用指定的本地方法。
?JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
? ?// 緩存String的class
? ?jclass jc = (*env)->FindClass(env, STRING_PATH);
}
如上代碼所示,當 java 調用以上本地方法時,jc 會被本地方法棧壓入棧中, jc 就是我們說的本地方法棧中 JNI 的對象引用,因此只會在此本地方法執行完成后才會被釋放。
?
4.觸發GC操作的原因
①GC_CONCURRENT: 當應用程序的堆內存快要滿的時候,系統會自動觸發GC操作來釋放內存。
②GC_FOR_MALLOC: 當應用程序需要分配更多內存,可是現有內存已經不足的時候,系統會進行GC操作來釋放內存。
③GC_HPROF_DUMP_HEAP: 當生成HPROF文件的時候,系統會進行GC操作。
④GC_EXPLICIT: 主動通知系統去進行GC操作,比如調用System.gc()方法來通知系統。或者在DDMS中,通過工具按鈕也是可以顯式地告訴系統進行GC操作的。(不要大量使用)
?
當應用程序空閑時,即沒有應用線程在運行時,GC會被調用,Java垃圾回收線程就是一個典型的守護線程, 當我們的程序中不再有任何運行中的Thread,程序就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是Java虛擬機上僅剩的線程時,Java虛擬機會自動離開。 它始終在低級別的狀態中運行,用于實時監控和管理系統中的可回收資源。
只有程序需要更多額外內存或應用程序空閑時,垃圾回收機制才會進行垃圾回收;只有當一個對象處于不可達狀態時,系統才會真正回收該對象所占有的資源(堆內存和方法區)。
程序無法精確控制垃圾回收的運行(但我們可以通知系統進行垃圾回收——System.gc(),但系統是否進行垃圾回收仍然不確定),只負責回收堆內存的對象,回收任何對象之前會調用它的finalize()方法。
?
對象的三種狀態:
①可達狀態:有一個以上的變量引用一個對象
②可恢復狀態:不再有任何變量引用它,垃圾回收時系統會調用所有可恢復狀態的對象的finalize()方法進行資源清理,如果重新有引用變量引用該對象會變為可達狀態,否則進入不可達狀態。
③不可達狀態:沒有變量引用,且finalize()方法也沒使該對象變成可達狀態,永久失去引用。
?
5.垃圾回收算法
常見的垃圾回收算法有引用計數法(Reference Counting)、標注并清理(Mark and Sweep GC)、拷貝(Copying GC)和逐代回收(Generational GC)等算法。Art采用了兩種算法,標注并清理和拷貝GC。
①引用計數回收法(Reference Counting GC)
(Android系統不使用該算法)引用計數法的原理很簡單,即記錄每個對象被引用的次數。每當創建一個新的對象,或者將其它指針指向該對象時,引用計數都會累加一次;而每當將指向對象的指針移除時,引用計數都會遞減一次,當引用次數降為0時,刪除對象并回收內存。
通常對象的引用計數都會跟對象放在一起,系統在分配完對象的內存后,返回的對象指針會跳過引用計數部分。
然而引用計數回收算法有一個很大的弱點,就是無法有效處理循環引用的問題,比如A和B對象同時互相引用,計數都是1,即使A、B不再被使用,JVM也不會檢測到,而且每次對象被引用等操作時還要觸發計數,因此額外開銷在所難免。
②標記清除算法(Mark and Sweep GC)
(Android系統使用該算法)在這個算法中,程序在運行的過程中不停的創建新的對象并消耗內存,直到內存用光,這時再要創建新對象時,系統暫停其它組件的運行,觸發GC線程啟動垃圾回收過程。內存回收的原理很簡單,就是從"GC Roots"集合開始,將內存整個遍歷一次,保留所有可以被GC Roots直接或間接引用到的對象,而剩下的對象都當作垃圾被回收。
過程分兩步:
第一步 Mark標記階段:找到內存中所有GC Root對象,只要是和 GC Root 對象直接或者間接相連則標記為灰色(也就是存活對象),否則標記為黑色(也就是垃圾對象)。
第二步 Sweep清除階段:當遍歷完所有的GC Root之后,則將標記為垃圾的對象直接清除。
注意:如果對象引用的層次過深,遞歸調用消耗完虛擬機內GC線程的棧空間,從而導致棧空間溢出(StackOverflow)異常,為了避免這種情況的發生,在具體實現時,通常是用一個叫做標注棧(Mark Stack)的數據結構來分解遞歸調用。一開始,標注棧(Mark Stack)的大小是固定的,但在一些極端情況下,如果標注棧的空間也不夠的話,則會分配一個新的標注棧(Mark Stack),并將新老棧用鏈表連接起來。與引用計數法中對象的內存布局類似,對象是否被標注的標志也是保存在對象頭里的,如下圖:
標記清除算法的優點:實現簡單,不需要將對象進行移動,而且很好的處理了引用計數中的循環引用問題,在內存足夠的前提下,對程序幾乎沒有任何額外的性能開支。
缺點:這個算法在執行垃圾回收過程中,可能產生大量的內存碎片,提高了垃圾回收的頻率。
③復制算法(Copying)
(Android系統使用該算法)這也是標注法的一個變種, GC內存堆實際上分成乒和乓兩部分。一開始,所有的內存分配請求都由乒部分滿足,其維護"下個對象分配的起始位置"指針,分配內存僅僅就是操作下這個指針而已,當乒的內存快用完時,采用標注(Mark)算法識別出存活的對象,并將它們拷貝到乓部分,后續的內存分配請求都在乓部分完成,而乓里的內存用完后,再切換回乒部分,使用內存就跟打乒乓球一樣。
也就是說,將現有的內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中。之后,清除正在使用的內存塊中的所有對象,交換兩個內存的角色,完成垃圾回收。
復制算法之前,內存分為 A/B 兩塊,并且當前只使用內存 A,內存的狀況如下圖所示:
標記完之后,所有可達對象都被按次序復制到內存 B 中,并設置 B 為當前使用中的內存。內存狀況如下圖所示:
復制算法的優點:內存分配速度快,按順序分配內存即可,實現簡單、運行高效,不用考慮內存碎片。
缺點:可用的內存大小縮小為原來的一半,對象存活率高時會頻繁進行復制。
?
Art中標記復制算法的具體實現:
?如果內存中多數對象都是存活的,復制法將會產生大量的內存間復制的開銷,而這正是因為該算法只把內存區域分為了兩個區域,這就會導致出現復制絕大部分的存活對象只為了清理掉一小部分垃圾的情況,這種做法無異于在家里打掃衛生,為了些許灰塵,把灰塵所在一邊的所有家具搬到沒有灰塵的另一邊后才打掃衛生,這是一種代價極其高昂的清理垃圾方法。因此,針對這種情況,Art采用的是該算法優化后的版本,把內存劃分為多個區域(Region),一個區域大小為256KB,如下圖所示:
?
?深綠色標明老年代中的存活對象,淺綠色標明新生代中的存活對象,紅色標明待清理的垃圾,此外,老年代和新生代都聚集在各自的區域,并沒有出現老年代和新生代混合在一個區域的情況。
這種做法顯而易見的好處如下:
? (1)當一個區域沒有垃圾的時候,就可以不進行垃圾清理。
? (2)當一個區域因為只有一兩個垃圾而要進行垃圾清理的時候,代價也不會太過于高昂,因為一個區域大小才256KB,本來存儲的對象就不多,因為一兩個垃圾而復制三四個對象還是可以接受的,這就和在家里打掃衛生時因為掃把夠不著椅子底下的灰塵,從而把椅子移開后才進行清理一樣可以令人接受。
區域命名規則:
? Evacuated:疏散;撤離;排泄;騰出(房子等)
(1)當一個區域有垃圾,需要被Evacuated的時候,Art則將該區域命名為Evacuated Region。
(2)當一個區域沒有垃圾,不需要被Evacuated的時候,Art則將該區域命名為Unevacuated Region。
?(3)當一個區域沒有存儲對象的時候,Art則將該區域命名為Unused Region。
(4)當一個區域原先為Unused Region,但是要作為其它Evacuated Region中存活對象復制目的地的時候,Art則將該區域命名為Evacuation Region。(存活對象即那些沒有被Art判定為垃圾的對象)
?舉個例子,假設有兩個區域,存儲了對象的區域1和沒有存儲對象的區域2,Art在使用可達性分析算法后,發現區域1有垃圾,將區域1命名為Evacuated Region,但區域1里面還有存活對象,由于區域2沒有存儲對象,Art決定將這些存活對象要復制到區域2,那么此時區域2就會被Art命名為Evacuation Region。
Art垃圾回收算法的并發性:
?垃圾回收算法具有并發性,也就是說垃圾回收線程是與主線程并發進行的,在一個垃圾回收周期只有一次短暫的GC暫停,時間為幾毫秒,所以用戶大多數情況下是無法感知的,并不會出現”stop the world“現象。
④標記-壓縮(整理)算法 (Mark-Compact)
這個是前面標注清理法的一個變種,系統在長時間運行的過程中,反復分配和釋放內存很有可能會導致內存堆里的碎片過多,從而影響分配效率,因此有些采用此算法的實現(Android系統中并沒有采用這個做法),在清理(SWEEP)過程中,還會執行內存中移動存活的對象,使其排列的更緊湊。在這種算法中,需要先從根節點開始對所有可達對象做一次標記,之后,它并不簡單地清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。最后,清理邊界外所有的空間。因此標記壓縮也分兩步完成:
第一步 Mark標記階段:找到內存中所有GC Root對象,只要是和 GC Root 對象直接或者間接相連則標記為灰色(也就是存活對象),否則標記為黑色(也就是垃圾對象)。
第二步 Compact 壓縮階段:將剩余存活對象按順序壓縮到內存的某一端。
舉例說明一下該算法:
?上圖中可以被GC Root訪問到的對象有A、C、D、E、F、H六個對象,為了避免內存碎片問題并滿足快速分配對象的要求,GC線程移動這六個對象,使內存使用更為緊湊,如下圖:
?由于GC線程移動了存活下來對象的內存位置,其必須更新其他線程中對這些對象的引用,由于A引用了E,移動之后,就必須更新這個引用,在更新過程中,必須中斷正在使用A的線程,防止其訪問到錯誤的內存位置而導致無法預料的錯誤。
標記壓縮算法的優點:這種方法既避免了碎片的產生,又不需要兩塊相同的內存空間,因此,其性價比較高。
缺點:所謂壓縮操作,仍需要進行局部對象移動,所以一定程度上還是降低了效率。
?
3.JVM內存模型和分代回收策略
堆是JVM管理的內存中最大的一塊。Java 虛擬機根據對象存活的周期不同,把堆內存劃分為幾塊,一般分為新生代、老年代,這就是 JVM 的內存分代策略。
①新生代
新生成的對象優先存放在新生代中,新生代對象存活率很低,在新生代中,常規應用進行一次垃圾收集一般可以回收 70%~95% 的空間,回收效率很高。新生代中一般采用的 GC 回收算法是復制算法。 新生代又可以繼續細分為 3 部分:Eden、Survivor0(簡稱 S0)、Survivor1(簡稱S1)。這 3 部分按照 8:1:1 的比例來劃分新生代。
絕大多數剛剛被創建的對象會存放在 Eden 區。
當 Eden 區第一次滿的時候,會進行垃圾回收(Minor GC)。首先將Eden區的垃圾對象回收清除,并將存活的對象復制到S0,此時S1是空的。
下一次 Eden 區滿時,再執行一次垃圾回收。此次會將 Eden 和 S0 區中所有垃圾對象清除,并將存活對象復制到 S1,此時 S0 變為空。
如此反復在 S0 和 S1 之間切換幾次(默認 15 次)之后,如果還有存活對象。說明這些對象的生命周期較長,則將它們轉移到老年代中。
注意:設置兩個Survivor區最大的好處就是解決內存碎片化。
②老年代
一個對象如果在新生代存活了足夠長的時間而沒有被清理掉,則會被復制到老年代。老年代的內存大小一般比新生代大,能存放更多的對象。如果對象比較大(比如長字符串或者大數組),并且新生代的剩余空間不足,則這個大對象會直接被分配到老年代上。我們可以使用 -XX:PretenureSizeThreshold 來控制直接升入老年代的對象大小,大于這個值的對象會直接分配在老年代上。
老年代因為對象的生命周期較長,一般采用標記壓縮的回收算法。等到"老一代對象池"也快要被填滿時,虛擬機此時再在"老一代對象池"中執行垃圾回收過程釋放內存(Major GC)。在逐代GC算法中,由于"年輕一代對象池"中的回收過程很快 – 只有很少的對象會存活,而執行時間較長的"老一代對象池"中的垃圾回收過程執行不頻繁,實現了很好的平衡,因此大部分虛擬機,如JVM、.NET的CLR都采用這種算法。
注意:對于老年代可能存在這么一種情況,老年代中的對象有時候會引用到新生代對象。這時如果要執行新生代 GC,則可能需要查詢整個老年代上可能存在引用新生代的情況,這顯然是低效的。所以,老年代中維護了一個 512 byte 的 card table,所有老年代對象引用新生代對象的信息都記錄在這里。每當新生代發生 GC 時,只需要檢查這個 card table 即可,大大提高了性能。
舉例說明:
由于每次GC都是在單獨的對象池中執行的,當GC Root之一R3被釋放后,在"年輕一代對象池"中執行GC過程時,R3所引用的對象f、g、h、i和j都會被當做垃圾回收掉,這樣就導致"老一代對象池"中的對象c有一個無效引用。
?為了避免這種情況,在"年輕一代對象池"中執行GC過程時,也需要將對象C當做GC Root之一。一個名為"Card Table"的數據結構就是專門設計用來處理這種情況的,"Card Table"是一個位數組,每一個位都表示"老一代對象池"內存中一塊4KB的區域(之所以取4KB,是因為大部分計算機系統中,內存頁大小就是4KB)。當用戶代碼執行一個引用賦值時,虛擬機不會直接修改內存,而是先將被賦值的內存地址與"老一代對象池"的地址空間做一次比較,如果要修改的內存地址是"老一代對象池"中的地址,虛擬機會修改"Card Table"對應的位為 1,表示其對應的內存頁已經修改過 - 不干凈(dirty)了,如下圖:
當需要在 "年輕一代對象池"中執行GC時, GC線程先查看"Card Table"中的位,找到不干凈的內存頁,將該內存頁中的所有對象都加入GC Root。雖然初看起來,有點浪費, 但是據統計,通常從老一代的對象引用新一代對象的幾率不超過1%,因此"Card Table"的算法是一小部分的時間損失換取空間。
?
對象進入老年代的四種情況:
①對象經過幾次垃圾回收,熬到設定的年齡閾值(默認為15),就會晉升到老年代。
②大對象直接進入老年代(超過了JVM中-XX:PretenureSizeThreshold參數的設置)
所以在寫程序的時候要盡量避免大對象,更要盡量避免朝生夕死的大對象,經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來安置他們。
③Survivor區中如果有相同年齡的對象所占空間大于幸存者區的一半,那么年齡大于等于該年齡的對象就可以直接進入老年代。(動態對象年齡判定)
在一次新生代GC后,Survivor區域中的幾個年齡對象加起來超過了Survivor區內存的一半,那么根據動態年齡判定規則,從最小的年齡加起,比如年齡1+年齡2+年齡3的對象大小總和,超過了Survivor區內存的一半,此時年齡3以上的對象就會晉升老年代。
④新生代GC后,存活下來的對象太多,Survivor區放不下,此時對象直接晉升老年代。(空間分配擔保)
?
下面來看看垃圾回收時這些空間是如何進行交互的:
①首先,所有新生成的對象都是放在年輕代的Eden分區的,初始狀態下兩個Survivor分區都是空的。年輕代的目標就是盡可能快速的收集掉那些生命周期短的對象。
?②當Eden區滿的的時候,小垃圾收集Minor GC就會被觸發。
?③當Eden分區進行清理的時候,會把引用對象移動到第一個Survivor分區,無引用的對象刪除。
?④在下一個小垃圾收集的時候,在Eden分區中會發生同樣的事情:無引用的對象被刪除,引用對象被移動到另外一個Survivor分區(S1)。此外,從上次小垃圾收集過程中第一個Survivor分區(S0)移動過來的對象年齡增加,然后被移動到S1。當所有的幸存對象移動到S1以后,S0和Eden區都會被清理。注意到,此時的Survivor分區存儲有不同年齡的對象。
⑤在下一個小垃圾收集,同樣的過程反復進行。然而,此時Survivor分區的角色發生了互換,引用對象被移動到S0,幸存對象年齡增大。Eden和S1被清理。
⑥這幅圖展示了從年輕代到老年代的提升。當進行一個小垃圾收集之后,如果此時年老對象到達了某一個年齡閾值(例子中使用的是8),JVM會把他們從年輕代提升到老年代。
⑦隨著小垃圾收集的持續進行,對象將會被持續提升到老年代。
⑧這樣幾乎涵蓋了年輕一代的整個過程。最終,在老年代將會進行大垃圾收集Major GC,這種收集方式會清理-壓縮老年代空間。
也就是說,剛開始會先在新生代內部反復的清理,頑強不死的移到老生代清理,最后都清不出空間,就爆炸了。
?
4.內存優化,減少GC開銷的措施
根據上述GC的機制,程序的運行會直接影響系統環境的變化,從而影響GC的觸發。若不針對GC的特點進行設計和編碼,就會出現內存駐留等一系列負面影響。為了避免這些影響,基本的原則就是盡可能地減少垃圾和減少GC過程中的開銷。具體措施包括以下幾個方面:
①不要顯式調用System.gc()
此函數建議JVM進行主GC,雖然只是建議,而非一定,但很多情況下它會觸發主GC,從而增加主GC的頻率,也即增加了間歇性停頓的次數。
②盡量減少臨時對象的使用
臨時對象在跳出函數調用后,會成為垃圾。少用臨時變量就相當于減少了垃圾的產生,從而延長了出現上述第二個觸發條件出現的時間,減少了主GC的機會。
③對象不用時最好顯式置為Null
一般而言,為Null的對象都會被作為垃圾處理,所以將不用的對象顯式地設為Null,有利于GC收集器判定垃圾,從而提高了GC的效率。
④盡量使用StringBuffer,而不用String來累加字符串
String是固定長的字符串對象,累加String對象時,并非在一個String對象中擴增,而是重新創建新的String對象,如Str5=Str1+Str2+Str3+Str4,這條語句執行過程中會產生多個垃圾對象,因為每次作“+”操作時都必須創建新的String對象,但這些過渡對象對系統來說是沒有實際意義的,只會增加更多的垃圾。避免這種情況可以改用StringBuffer來累加字符串,因為StringBuffer是可變長的,它在原有基礎上進行擴增,不會產生中間對象。
⑤能用基本類型如int、long,就不用Integer、Long對象
基本類型變量占用的內存資源比相應對象占用的少得多,如果沒有必要,最好使用基本變量。
⑥盡量少用靜態對象變量
靜態變量屬于全局變量,不會被GC回收,它們會一直占用內存。
⑦分散對象創建或刪除的時間
集中在短時間內大量創建新對象,特別是大對象,會導致突然需要大量內存,JVM在面臨這種情況時,只能進行主GC,以回收內存或整合內存碎片,從而增加主GC的頻率。集中刪除對象,道理也是一樣的,它使得突然出現了大量的垃圾對象,空閑空間必然減少,從而大大增加了下一次創建新對象時強制主GC的機會。
⑧bitmap、游標Cursor、IO或者文件流等不用時候,記得回收。尤其是圖片的加載而占用內存較大,可以做圖片質量或者物理大小的壓縮。
⑨避免在頻繁繪制的onDraw方法里創建對象
⑩設置過的監聽不用時,及時移除。如在destroy時及時remove,尤其以addListener開頭的,在destroy中都需要remove。
⑩HashMap由于創建時候內存生成16位存儲Entry節點的數據,一個Entry占32B,也就是說即使里面沒有任何元素,也要分配一塊內存空間給它。且存儲數據每次大于當前容量最大值,HashMap都會以*2容量的方式去擴容。所以在Android中,HashMap是比較費內存的。
⑩防止單例類長久持有不用對象的引用,導致對象無法回收,特別是傳入上下文對象的單例類,可以嘗試傳入ApplicationComtext。
⑩非靜態內部類導致內存泄露,比如Activity中創建Handler,可以嘗試弱引用去拿到外部的對象引用。
⑩廣播的注銷,頁面Activity退出時未執行完的Thread的注銷或者Timer還在執行定時任務的注銷等。
⑩Activity結束時,需要Cancel掉屬性動畫。
?
?
總結
以上是生活随笔為你收集整理的Android 垃圾回收机制★★★的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win10删除五笔
- 下一篇: 单元测试自动生成测试用例