同樣的盾计,先來個思維導(dǎo)圖預(yù)覽一下本文結(jié)構(gòu)。
一脊髓、運行時數(shù)據(jù)區(qū)域
首先來看看Java虛擬機所管理的內(nèi)存包括哪些區(qū)域辫愉,就像我們要了解一個房子,我們得先知道這個房子大體構(gòu)造将硝。根據(jù)《Java虛擬機規(guī)范(Java SE 7 版)》的規(guī)定恭朗,請看下圖:
1.1 程序計數(shù)器
程序計數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器依疼。
- 由于 Java 虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的痰腮,在任何一個確定的時刻,一個處理器(對于多核處理器來說是一個內(nèi)核)都只會執(zhí)行一條線程中的指令律罢。
- 為了線程切換后能恢復(fù)到正確的執(zhí)行位置膀值,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間計數(shù)器互不影響误辑,獨立存儲沧踏,我們稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。
- 此內(nèi)存區(qū)域是唯一一個在 Java 虛擬機規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域稀余。
1.2 Java 虛擬機棧
與程序計數(shù)器一樣悦冀,Java 虛擬機棧也是線程私有的,它的生命周期與線程相同睛琳。虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表盒蟆、操作數(shù)棧踏烙、動態(tài)鏈接、方法出口等信息历等。每一個方法從調(diào)用直至執(zhí)行完成的過程讨惩,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。請看下圖:
- 有人把 Java 內(nèi)存區(qū)分為堆內(nèi)存和棧內(nèi)存寒屯,而所指的“椉瞿恚”就是這里的虛擬機棧,或者說是虛擬機棧中局部變量表部分寡夹。
- 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean处面、byte、char菩掏、short魂角、int、float智绸、long野揪、double)、對象引用和 returnAddress 類型(指向了一條字節(jié)碼指令的地址)瞧栗,其中64位長度的 long 和 double 類型的數(shù)據(jù)占用2個局部變量空間斯稳,其余數(shù)據(jù)類型只占用1個。
- 操作數(shù)棧也常被稱為操作棧迹恐,它是一個后入先出棧挣惰。當一個方法剛剛執(zhí)行的時候,這個方法的操作數(shù)棧是空的系草,在方法執(zhí)行的過程中通熄,會有各種字節(jié)碼指向操作數(shù)棧中寫入和提取值唆涝,也就是入棧與出棧操作找都。
- 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接廊酣。在Class文件的常量池中存有大量的符號引用能耻,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)。這些符號引用一部分會在類加載階段或第一次使用的時候轉(zhuǎn)化為直接引用亡驰,這種轉(zhuǎn)化稱為靜態(tài)解析晓猛。另外一部分將在每一次的運行期期間轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接凡辱。
- 當一個方法執(zhí)行完畢之后,要返回之前調(diào)用它的地方,因此在棧幀中必須保存一個方法返回地址凛剥。方法退出的過程實際上等同于把當前棧幀出棧,因此退出時可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧磕秤,把返回值(如果有的話)壓入調(diào)用都棧幀的操作數(shù)棧中,調(diào)用PC計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等捧韵。
- 虛擬機規(guī)范允許具體的虛擬機實現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀中市咆,例如與高度相關(guān)的信息,這部分信息完全取決于具體的虛擬機實現(xiàn)再来。在實際開發(fā)中蒙兰,一般會把動態(tài)連接,方法返回地址與其它附加信息全部歸為一類芒篷,稱為棧幀信息搜变。
- 在 Java 虛擬機規(guī)范中,規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度针炉,將拋出 StackOverflowError 異常痹雅;如果虛擬機棧可以動態(tài)擴展糊识,當擴展時無法申請到足夠的內(nèi)存绩社,就會拋出 OutOfMemoryError 異常。
1.2.1 虛擬機棧溢出
- 如果線程請求的棧深度大于虛擬機所允許的最大深度赂苗,將拋出 StackOverflowError 異常愉耙。
- 如果虛擬機在擴展棧時無法申請到足夠的內(nèi)存空間,則拋出 OutOfMemoryError 異常拌滋。
- 當椘友兀空間無法繼續(xù)分配時,到底是內(nèi)存太小败砂,還是已使用的椂脑空間太大,其本質(zhì)上只是對同一件事情的兩種描述而已昌犹。
- 系統(tǒng)分配給每個進程的內(nèi)存是有限制的坚芜,除去 Java 堆、方法區(qū)斜姥、程序計數(shù)器鸿竖,如果虛擬機進程本身耗費的內(nèi)存不計算在內(nèi),剩下內(nèi)存就由虛擬機棧和本地方法椫簦“瓜分”了缚忧。每個線程分配到的棧容量越大,可以建立的線程數(shù)量自然就越少杈笔,建立線程時就越容易把剩下的內(nèi)存耗盡闪水。
- 出現(xiàn) StackOverflowError 異常時有錯誤棧可以閱讀蒙具,棧深度在大多數(shù)情況下達到1000~2000完全沒有問題球榆,對于正常的方法調(diào)用(包括遞歸)峰弹,這個深度應(yīng)該完全夠用了。
- 但是芜果,如果是建立過多線程導(dǎo)致的內(nèi)存溢出鞠呈,在不能減少線程數(shù)或者更換 64 位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程右钾。
1.3 本地方法棧
- 本地方法棧與虛擬機棧所發(fā)揮的作用非常相似蚁吝,它們之間的區(qū)別是虛擬機棧為虛擬機執(zhí)行 Java 方法服務(wù),而本地方法棧則為虛擬機棧使用到的 Native 方法服務(wù)舀射。
- 與虛擬機棧一樣窘茁,本地方法棧區(qū)域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
1.4 Java 堆
Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域脆烟,在虛擬機啟動時創(chuàng)建山林。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存(但是邢羔,隨著技術(shù)發(fā)展驼抹,所有對象都分配在堆上也漸漸變得不是那么“絕對”了)。請看下圖:
- 對于大多數(shù)應(yīng)用來說拜鹤,Java 堆是 Java 虛擬機所管理的內(nèi)存中最大的一塊框冀。
- Java 堆是垃圾收集器管理的主要區(qū)域,也被稱為“GC堆”敏簿。
- Java 堆可以細分為新生代明也、老年代、永久代惯裕;再細致一點可以分為 Eden温数、From Survivor、To Survivor蜻势、Tenured撑刺、Permanent 。
- Java 堆可以處于物理上不連續(xù)的內(nèi)存空間中咙边,只要邏輯上是連續(xù)的即可猜煮,就像磁盤空間一樣。
- 從內(nèi)存分配的角度來看败许,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩沖區(qū)(TLAB)。
- 如果在堆中沒有內(nèi)存完成實例分配淑蔚,并且堆也無法再擴展時市殷,將會拋出 OutOfMemoryError 異常。
1.4.1 Java 堆溢出
- Java 堆用于存儲對象實例刹衫,只要不斷地創(chuàng)建對象醋寝,并且保證 GC Roots 到對象之間有可達路徑來避免垃圾回收機制清除這些對象搞挣,那么在對象數(shù)量到達最大堆的容量限制后就會產(chǎn)生內(nèi)存溢出異常。
- Java 堆內(nèi)存的 OOM 異常是實際應(yīng)用中常見的內(nèi)存溢出異常情況音羞。當出現(xiàn) Java 堆內(nèi)存溢出時囱桨,異常堆棧信息 “java.lang.OutOfMemoryError” 會跟著進一步提示 “Java heap space” 。
- 通常是先通過內(nèi)存映像分析工具對 Dump 出來的堆轉(zhuǎn)儲快照進行分析嗅绰,重點是確認內(nèi)存中的對象是否是必要的舍肠,也就是要先分清楚到底是出現(xiàn)了內(nèi)存泄漏還是內(nèi)存溢出。
- 如果是內(nèi)存泄漏窘面,可進一步通過工具查看泄露對象到 GC Roots 的引用鏈翠语。于是就能找到泄露對象的類型信息及 GC Roots 引用鏈的信息,就可以比較準確地定位出泄露代碼的位置财边。
- 如果不存在泄露肌括,就是內(nèi)存中的對象確實都還必須存活著,那就應(yīng)當檢查虛擬機的堆參數(shù)(-Xmx 與 -Xms)酣难,與機器物理內(nèi)存對比看是否還可以調(diào)大谍夭,從代碼上檢查是否存在某些對象生命周期過長、持有狀態(tài)時間過長的情況憨募,嘗試減少程序運行期的內(nèi)存消耗慧库。
1.5 方法區(qū)
方法區(qū)與 Java 堆一樣,是各個線程共享的內(nèi)存區(qū)域馋嗜,它用于存儲已被虛擬機加載的類信息齐板、常量、靜態(tài)變量葛菇、即時編譯器編譯后的代碼等數(shù)據(jù)甘磨。
- Java 虛擬機規(guī)范對方法區(qū)的限制非常寬松,除了和 Java 堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴展外眯停,還可以選擇不實現(xiàn)垃圾收集济舆。
- 這區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載。
- 當方法區(qū)無法滿足內(nèi)存分配需求時莺债,將拋出 OutOfMemoryError 異常滋觉。
1.5.1 運行時常量池
- 運行時常量池是方法區(qū)的一部分。
- 常量池用于存放編譯期生成的各種字面量和符號引用齐邦,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放椎侠。
- 運行時常量池相對于 Class 文件常量池的一個重要特征是具備動態(tài)性,Java 語言并不要求常量一定只有編譯期才能產(chǎn)生措拇,也就是并非預(yù)置入 Class 文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池我纪,運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是 String 類的 intern() 方法。
- 當常量池無法再申請到內(nèi)存時會拋出 OutOfMemoryError 異常浅悉。在 OutOfMemoryError 后面跟隨的提示信息時 “PermGen space” 趟据。
1.6 直接內(nèi)存
- 直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機規(guī)范中定義的內(nèi)存區(qū)域术健。但是這部分內(nèi)存也被頻繁地使用汹碱,而且也可能導(dǎo)致 OutOfMemoryError 異常出現(xiàn)。
- NIO 類荞估,一種基于通道與緩沖區(qū)的 I/O 方式咳促,它可以使用 Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內(nèi)存的引用進行操作泼舱。這樣能在一些場景中顯著提高性能等缀,因為避免了在 Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)。
- 本機直接內(nèi)存的分配不會受到 Java 堆大小的限制娇昙,但是尺迂,既然是內(nèi)存,肯定還是會受到本機總內(nèi)存(包括 RAM 以及 SWAP 區(qū)或者分頁文件)大小以及處理器尋址空間的限制冒掌。
- 由 DirectMemory 導(dǎo)致的內(nèi)存溢出噪裕,一個明顯的特征是在 Heap Dump 文件中不會看見明顯的異常,如果我們發(fā)現(xiàn) OOM 之后 Dump 文件很小股毫,而程序中有直接或間接使用了 NIO 膳音,那就可以考慮檢查一下是不是這方面的原因。
二铃诬、內(nèi)存分配策略
對象的內(nèi)存分配祭陷,往大方向講,就是在堆上分配(但也可能經(jīng)過 JIT 編譯后被拆散為標量類型并間接地棧上分配)趣席,對象主要分配在新生代的 Eden 區(qū)上兵志,如果啟動了本地線程分配緩沖,將按線程優(yōu)先在 TLAB 上分配宣肚。少數(shù)情況下也可能會直接分配在老年代中想罕,分配的規(guī)則并不是固定的,其細節(jié)取決于當前使用的是哪一種垃圾收集器組合霉涨,還有虛擬機中與內(nèi)存相關(guān)的參數(shù)的設(shè)置按价。
2.1 對象優(yōu)先在 Eden 分配
大多數(shù)情況下,對象在新生代 Eden 區(qū)中分配笙瑟。當 Eden 區(qū)沒有足夠的空間進行分配時楼镐,虛擬機將發(fā)起一次 Minor GC 。舉個例子逮走,看下面的代碼:
private static final int _1MB = 1024 * 1024;
/**
* VM 參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
private static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB];//出現(xiàn)一次 Minor GC
}
執(zhí)行上面的testAllocation() 代碼鸠蚪,當分配 allocation4 對象的語句時會發(fā)生一次 Minor GC 今阳,這次 GC 的結(jié)果是新生代 6651KB 變?yōu)?148KB ,而總內(nèi)存占用量則幾乎沒有減少(因為 allocation1师溅、allocation2茅信、allocation3 三個對象都是存活的,虛擬機幾乎沒有找到可回收的對象)墓臭。這次 GC 發(fā)生的原因是給 allocation4 分配內(nèi)存時蘸鲸,發(fā)現(xiàn) Eden 已經(jīng)被占用了 6MB ,剩余空間已不足以分配 allocation4 所需的 4MB 內(nèi)存窿锉,因此發(fā)生 Minor GC 酌摇。GC 期間虛擬機又發(fā)現(xiàn)已有的 3 個 2MB 大小的對象全部無法放入 Survivor 空間(從上圖中可看出 Survivor 空間只有 1MB 大小)嗡载,所以只好通過分配擔保機制提前轉(zhuǎn)移到老年代去窑多。
2.2 大對象直接進入老年代
- 所謂的對象是指,需要大量連續(xù)內(nèi)存空間的 Java 對象洼滚,最典型的大對象就是那種很長的字符串以及數(shù)組埂息。經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來“安置”它們。
- 虛擬機提供了一個 -XX:PretenureSizeThreshold 參數(shù)遥巴,令大于這個設(shè)置值的對象直接在老年代分配千康。這樣做的目的是避免在 Eden 區(qū)及兩個 Survivor 區(qū)之間發(fā)生大量的內(nèi)存復(fù)制(新生代采用復(fù)制算法收集內(nèi)存)。
2.3 長期存活的對象將進入老年代
既然虛擬機采用了分代收集的思想來管理內(nèi)存铲掐,那么內(nèi)存回收時就必須能識別到哪些對象應(yīng)放在新生代拾弃,哪些對象應(yīng)放在老年代中。為了做到這點摆霉,虛擬機給每個對象定義了一個對象年齡計數(shù)器豪椿。如果對象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然存活,并且能被 Survivor 容納的話携栋,將被移動到 Survivor 空間中搭盾,并且對象年齡設(shè)為 1 。對象在 Survivor 區(qū)中每“熬過”一次 Minor GC刻两,年齡就增加1歲增蹭,當它的年齡增加到一定程度(默認15歲),就會被晉升到老年代中磅摹。對象晉升老年代的年齡閾值滋迈,可以通過參數(shù) -XX:MaxTenuringThreshold 設(shè)置。
2.4 動態(tài)對象年齡判定
為了能更好地適應(yīng)不同程序的內(nèi)存狀況户誓,虛擬機并不是永遠地要求對象的年齡必須達到了 MaxTenuringThreshold 才能晉升老年代饼灿,如果在 Survivor 空間中相同年齡所有對象大小的總和大于 Survivor 空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代帝美,無須等到 MaxTenuringThreshold 中的要求的年齡碍彭。
2.5 空間分配擔保機制
- 在發(fā)生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立庇忌,那么 Minor GC 可以確保是安全的舞箍。如果不成立,則虛擬機會查看 HandlePromotionFailure 設(shè)置值是否允許擔保失敗皆疹。如果允許疏橄,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于略就,將嘗試著進行一次 Minor GC ,盡管這次 Minor GC 是有風險的捎迫;如果小于,或者 HandlePromotionFailure 設(shè)置不允許冒險表牢,那這次也要改為進行一次 Full GC窄绒。
- 上面提到的“冒險”指的是,由于新生代使用復(fù)制收集算法崔兴,但為了內(nèi)存利用率彰导,只使用其中一個 Survivor 空間來作為輪換備份,因此當出現(xiàn)大量對象在 Minor GC 后仍然存活的情況恼布,把 Survivor 無法容納的對象直接進入老年代螺戳。老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間折汞,一共有多少對象會活下來在實際完成內(nèi)存回收之前是無法明確知道的倔幼,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經(jīng)驗值,與老年代的剩余空間進行比較爽待,決定是否進行 Full GC 來讓老年代騰出更多空間损同。
- 取平均值進行比較其實仍然是一種動態(tài)概率的手段,也就是說鸟款,如果某次 Minor GC 存活后的對象突增膏燃,遠遠高于平均值的話,依然會導(dǎo)致?lián)J ?/li>
- 如果出現(xiàn)了HandlePromotionFailure 失敗何什,那就只好在失敗后重新發(fā)起一次 Full GC组哩。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將 HandlePromotionFailure 開關(guān)打開处渣,避免 Full GC 過于頻繁伶贰。
- 但在 JDK 6 Update 24 之后,HandlePromotionFailure 參數(shù)不會再影響到虛擬機的控件分配擔保策略罐栈,只要老年代的連續(xù)空間大于新生代對象總大小或者歷次晉升的平均大小就會進行 Minor GC ,否則將進行 Full GC黍衙。
三、內(nèi)存回收策略
- 新生代 GC(Minor GC) :指發(fā)生在新生代的垃圾收集動作荠诬,因為 Java 對象大多都具備朝生夕滅的特性琅翻,所以 Minor GC 非常頻繁位仁,一般回收速度也比較快。
- 老年代 GC(Major GC / Full GC):值發(fā)生在老年代的 GC方椎,出現(xiàn)了 Major GC聂抢,經(jīng)常會伴隨至少一次的 Minor GC(但非絕對)。Major GC 的速度一般會比 Minor GC 慢 10 倍以上辩尊。
3.1 內(nèi)存回收關(guān)注的區(qū)域
- 上面已經(jīng)介紹 Java 內(nèi)存運行時區(qū)域的各個部分涛浙,其中程序計數(shù)器康辑、虛擬機棧摄欲、本地方法棧3個區(qū)域隨線程而生,隨線程而滅疮薇。
- 棧中的棧幀隨著方法的進入和退出而有條不紊地執(zhí)行者出棧和入棧操作胸墙。每一個棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時就已知的。
- 因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性按咒,在這幾個區(qū)域內(nèi)就不需要過多考慮回收的問題迟隅,因為方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟隨著回收了励七。
- 而 Java 堆和方法區(qū)則不一樣智袭,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存也可能不一樣掠抬,我們只有在程序處于運行期間時才能知道會創(chuàng)建哪些對象吼野,這部分內(nèi)存的分配和回收都是動態(tài)的,垃圾收集器所關(guān)注的是這部分內(nèi)存两波。
3.2 對象存活判斷
3.2.1 引用計數(shù)算法
- 給對象添加一個引用計數(shù)器瞳步,每當有一個地方引用它時,計數(shù)器值就加1腰奋;當引用失效時单起,計數(shù)器值就減1;任何時刻計數(shù)器為 0 的對象就是不可能再被使用的劣坊。
- 這種算法的實現(xiàn)簡單嘀倒,判定效率也很高,在大部分情況下它都是一個不錯的算法局冰,但它很難解決對象之間相互循環(huán)引用的問題测蘑。
- 舉個例子,對象 objA 和 objB 都有字段 instance锐想,賦值令 objA.instance = objB 及 objB.instance = objA ,除此之外帮寻,這兩個對象再無任何引用,實際上赠摇,這兩個對象已經(jīng)不可能再被訪問固逗,但是它們因為相互引用著對方浅蚪,導(dǎo)致它們的引用計數(shù)都不為 0,于是引用計數(shù)算法無法通知 GC 收集器回收它們烫罩。
3.2.2 可達性分析算法
- 這個算法的基本思路就是通過一系列額稱為“GC Roots” 的對象作為起始點惜傲,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈贝攒,當一個對象到 GC Roots 沒有任何引用鏈相連或者說這個對象不可達時盗誊,則證明此對象是不可用的。
- 在 Java 語言中隘弊,可作為 GC Roots 的對象包括以下:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中 JNI 引用的對象
請看下圖:
3.3 方法區(qū)的回收
- 方法區(qū)(HotSpot 虛擬機中的永久代)的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類哈踱。回收廢棄常量與回收 Java 堆的對象非常類似梨熙。
- 判定一個類是否是“無用的類”需要同時滿足下面3個條件:
- 該類的所有的實例都已經(jīng)被回收开镣,也就是 Java 堆中不存在該類的任何實例。
- 加載該類的 ClassLoader 已經(jīng)被回收咽扇。
- 該類對應(yīng)的 java.lang.Class 對象沒有在任何地方被引用邪财,無法在任何地方通過反射訪問該類的方法。
- 虛擬機可以對滿足上述3個條件的無用類進行回收质欲,這里說的僅僅是“可以”树埠,而并不是和對象一樣,不使用了就必然回收嘶伟。
3.4 垃圾收集算法
3.4.1 標記—清除算法
- 算法分為 “標記” 和 “清除” 兩個階段:首先標記出所有需要回收的對象怎憋,在標記完成后統(tǒng)一回收所有被標記的對象。
- 它主要有兩個不足的地方:一個是效率問題奋早,標記和清除兩個過程的效率都不高盛霎;另一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片耽装,空間碎片太多可能會導(dǎo)致以后在程序運行過程中需要分配較大對象時愤炸,無法找到足夠的連續(xù)內(nèi)存而得不到提前觸發(fā)另一次垃圾收集動作。
- 這是最基礎(chǔ)的收集算法掉奄,后續(xù)的收集算法都是基于這種思路并對其不足進行改進而得到的规个。
3.4.2 復(fù)制算法
- 為了解決效率問題,“復(fù)制”算法應(yīng)運而生姓建,它將可用內(nèi)存按容量劃分為大小相等的兩塊诞仓,每次只使用其中一塊。
- 當這一塊的內(nèi)存用完了速兔,就將還存活著的對象復(fù)制到另外一塊上面墅拭,然后再把已使用過的內(nèi)存空間一次清理掉。
- 這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收涣狗,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況谍婉,只要移動堆頂指針舒憾,按順序分配內(nèi)存即可,實現(xiàn)簡單穗熬,運行高效镀迂。
- 不足之處是,將內(nèi)存縮小為原來的一半唤蔗,代價太高探遵。
舉個優(yōu)化例子:新生代中的對象98%是“朝生夕死”的,所以并不需要按照 1:1 的比例來劃分內(nèi)存空間妓柜,而是將內(nèi)存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間箱季,每次使用 Eden 和其中一塊 Survivor。當回收時领虹,將 Eden 和 Survivor 中還存活著的對象一次性地復(fù)制到另一塊 Survivor 空間上规哪,最后清理掉 Eden 和剛才用過的 Survivor 空間。
再舉個優(yōu)化例子:將 Eden 和 Survivor 的大小比例設(shè)為 8:1 塌衰,也就是每次新生代中可用內(nèi)存空間為整個新生代容器的 90%,只有10% 的內(nèi)存作為保留區(qū)域蝠嘉。當然 98% 的對象可回收只是一般場景下的數(shù)據(jù)最疆,我們沒有辦法保證每次回收都只有不多于 10% 的對象存活,當 Survivor 空間不夠用時蚤告,需要依賴其他內(nèi)存(這里指老年代)進行分配擔保(空間分配擔保機制在上面努酸,了解一下)。
3.4.3 標記—整理算法
復(fù)制收集算法在對象存活率較高時就要進行較多的復(fù)制操作杜恰,效率將會變低获诈。所以在老年代一般不能直接選用復(fù)制收集算法。
- 根據(jù)老年代的特點心褐,“標記—整理” 算法應(yīng)運而生舔涎。
- 標記過程仍然與 “標記—清除” 算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理逗爹,而是讓所有存活的對象都向一端移動亡嫌,然后直接清理掉端邊界以外的內(nèi)存。
3.4.4 分代收集算法
- 根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊掘而,一般是把 Java 堆分為新生代和老年代挟冠,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴ā?/li>
- 在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去袍睡,只有少量存活知染,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集斑胜。
- 而老年代中因為對象存活率高控淡、沒有額外空間對它進行分配擔保色瘩,就必須使用 “標記—清除” 或者 “標記—整理” 算法來進行回收。
- 當前商業(yè)虛擬機的垃圾收集都采用 “分代收集” 算法逸寓。
四居兆、編程中的內(nèi)存優(yōu)化
相信大家在編程中都會注意到內(nèi)存使用的問題,下面我就簡單列一下在實際操作當中需要注意的地方竹伸。
4.1 減小對象的內(nèi)存占用
- 使用更加輕量的數(shù)據(jù)結(jié)構(gòu)
我們可以考慮使用 ArrayMap / SparseArray 而不是 HashMap 等傳統(tǒng)數(shù)據(jù)結(jié)構(gòu)泥栖。(我在老項目中,根據(jù) Lint 提示勋篓,將 HashMap 替換成 ArrayMap / SparseArray 之后吧享,在 Android Profiler 中顯示運行時內(nèi)存比之前直接少了幾M,還是挺可觀的譬嚣。)
- 避免使用 Enum
- 減小 Bitmap 對象的內(nèi)存占用
- inSampleSize :縮放比例钢颂,在把圖片載入內(nèi)存之前,我們需要先計算出一個合適的縮放比例拜银,避免不必要的大圖載入殊鞭。
- decode format:解碼格式,選擇 ARGB_8888 / RBG_565 / ARGB_4444 / ALPHA_8尼桶,存在很大差異操灿。
- 使用更小的圖片:盡量使用更小的圖片不僅僅可以減少內(nèi)存的使用,還可以避免出現(xiàn)大量的 InflationException泵督。
4.2 內(nèi)存對象的重復(fù)利用
- 復(fù)用系統(tǒng)自帶的資源:Android系統(tǒng)本身內(nèi)置了很多的資源趾盐,例如字符串/顏色/圖片/動畫/樣式以及簡單布局等等,這些資源都可以在應(yīng)用程序中直接引用小腊。
- 注意在 ListView / GridView 等出現(xiàn)大量重復(fù)子組件的視圖里面對 ConvertView 的復(fù)用
- Bitmap 對象的復(fù)用
- 避免在 onDraw 方法里面執(zhí)行對象的創(chuàng)建:類似 onDraw() 等頻繁調(diào)用的方法救鲤,一定需要注意避免在這里做創(chuàng)建對象的操作,因為他會迅速增加內(nèi)存的使用秩冈,而且很容易引起頻繁的 GC本缠,甚至是內(nèi)存抖動。
- StringBuilder:在有些時候漩仙,代碼中會需要使用到大量的字符串拼接的操作搓茬,這種時候有必要考慮使用 StringBuilder 來替代頻繁的 “+” 。
4.3 避免對象的內(nèi)存泄露
- 注意 Activity 的泄漏
- 內(nèi)部類引用導(dǎo)致 Activity 的泄漏
- Activity Context 被傳遞到其他實例中队他,這可能導(dǎo)致自身被引用而發(fā)生泄漏卷仑。
- 考慮使用 Application Context 而不是 Activity Context :對于大部分非必須使用 Activity Context 的情況(Dialog 的 Context 就必須是 Activity Context),我們都可以考慮使用 Application Context 而不是 Activity 的 Context麸折,這樣可以避免不經(jīng)意的 Activity 泄露锡凝。
- 注意臨時 Bitmap 對象的及時回收:例如臨時創(chuàng)建的某個相對比較大的 bitmap 對象,在經(jīng)過變換得到新的 bitmap 對象之后垢啼,應(yīng)該盡快回收原始的 bitmap窜锯,這樣能夠更快釋放原始 bitmap 所占用的空間张肾。
- 注意監(jiān)聽器的注銷:在 Android 程序里面存在很多需要 register 與 unregister 的監(jiān)聽器,我們需要確保在合適的時候及時 unregister 那些監(jiān)聽器锚扎。自己手動 add 的 listener吞瞪,需要記得及時 remove 這個 listener。
- 注意緩存容器中的對象泄漏:我們?yōu)榱颂岣邔ο蟮膹?fù)用性把某些對象放到緩存容器中驾孔,可是如果這些對象沒有及時從容器中清除芍秆,也是有可能導(dǎo)致內(nèi)存泄漏的。
- 注意 WebView 的泄漏:通常根治這個問題的辦法是為 WebView 開啟另外一個進程翠勉,通過 AIDL 與主進程進行通信妖啥,WebView 所在的進程可以根據(jù)業(yè)務(wù)的需要選擇合適的時機進行銷毀,從而達到內(nèi)存的完整釋放对碌。
- 注意 Cursor 對象是否及時關(guān)閉
4.4 內(nèi)存使用策略優(yōu)化
- 資源文件需要選擇合適的文件夾進行存放
- Try catch 某些大內(nèi)存分配的操作:在某些情況下荆虱,我們需要事先評估那些可能發(fā)生 OOM 的代碼,對于這些可能發(fā)生 OOM 的代碼朽们,加入 catch 機制怀读,可以考慮在 catch 里面嘗試一次降級的內(nèi)存分配操作。例如 decode bitmap 的時候华坦,catch 到 OOM,可以嘗試把采樣比例再增加一倍之后,再次嘗試 decode。
- 謹慎使用 static 對象:因為static的生命周期過長,和應(yīng)用的進程保持一致,使用不當很可能導(dǎo)致對象泄漏。
- 特別留意單例對象中不合理的持有:因為單例的生命周期和應(yīng)用保持一致骏全,使用不合理很容易出現(xiàn)持有對象的泄漏棺棵。
- 珍惜Services資源:建議使用 IntentService
- 優(yōu)化布局層次,減少內(nèi)存消耗:越扁平化的視圖布局,占用的內(nèi)存就越少粱锐,效率越高。我們需要盡量保證布局足夠扁平化,當使用系統(tǒng)提供的 View 無法實現(xiàn)足夠扁平的時候考慮使用自定義 View 來達到目的。
- 謹慎使用 “抽象” 編程
- 使用 nano protobufs 序列化數(shù)據(jù)
- 謹慎使用依賴注入框架
- 謹慎使用多進程
- 使用 ProGuard 來剔除不需要的代碼
- 謹慎使用第三方 libraries
- 考慮不同的實現(xiàn)方式來優(yōu)化內(nèi)存占用
五桂敛、內(nèi)存檢測工具
最后給推薦幾個內(nèi)存檢測的工具伟恶,具體使用方法,可以自行搜索朴爬。當然除了下面這些工具,應(yīng)該還有更多更好用的工具橡淆,只是我還沒有發(fā)現(xiàn)召噩,如有建議,可以在文章下面評論留言逸爵,大家一起學(xué)習分享一下具滴。
- Systrace
- Traceview
- Android Studio 3.0 的 Android Profiler 分析器
- LeakCanary
后續(xù)
學(xué)習資料
- 《深入理解Java虛擬機:JVM高級特性與最佳實踐》
- Android性能優(yōu)化典范