一篇文章帶你了解 Java 自動內存管理機制及性能優化

一圖帶你看完本文

一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

一、運行時數據區域

首先來看看Java虛擬機所管理的內存包括哪些區域,就像我們要了解一個房子,我們得先知道這個房子大體構造。根據《Java虛擬機規範(Java SE 7 版)》的規定,請看下圖:

一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

Java 虛擬機運行時數據區

1.1 程序計數器

程序計數器是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。

  • 由於 Java 虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)都只會執行一條線程中的指令。
  • 為了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。
  • 此內存區域是唯一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 情況的區域。

1.2 Java 虛擬機棧

與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是 Java 方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。請看下圖:

一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

Java 虛擬機棧

  • 有人把 Java 內存區分為堆內存和棧內存,而所指的“棧”就是這裡的虛擬機棧,或者說是虛擬機棧中局部變量表部分。
  • 局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用和 returnAddress 類型(指向了一條字節碼指令的地址),其中64位長度的 long 和 double 類型的數據佔用2個局部變量空間,其餘數據類型只佔用1個。
  • 操作數棧也常被稱為操作棧,它是一個後入先出棧。當一個方法剛剛執行的時候,這個方法的操作數棧是空的,在方法執行的過程中,會有各種字節碼指向操作數棧中寫入和提取值,也就是入棧與出棧操作。
  • 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接。在Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用為參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析。另外一部分將在每一次的運行期期間轉化為直接引用,這部分稱為動態連接。
  • 當一個方法執行完畢之後,要返回之前調用它的地方,因此在棧幀中必須保存一個方法返回地址。方法退出的過程實際上等同於把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的局部變量表和操作數棧,把返回值(如果有的話)壓入調用都棧幀的操作數棧中,調用PC計數器的值以指向方法調用指令後面的一條指令等。
  • 虛擬機規範允許具體的虛擬機實現增加一些規範裡沒有描述的信息到棧幀中,例如與高度相關的信息,這部分信息完全取決於具體的虛擬機實現。在實際開發中,一般會把動態連接,方法返回地址與其它附加信息全部歸為一類,稱為棧幀信息。
  • 在 Java 虛擬機規範中,規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出 StackOverflowError 異常;如果虛擬機棧可以動態擴展,當擴展時無法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。

1.2.1 虛擬機棧溢出

  1. 如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出 StackOverflowError 異常。
  2. 如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出 OutOfMemoryError 異常。
  • 當棧空間無法繼續分配時,到底是內存太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。
  • 系統分配給每個進程的內存是有限制的,除去 Java 堆、方法區、程序計數器,如果虛擬機進程本身耗費的內存不計算在內,剩下內存就由虛擬機棧和本地方法棧“瓜分”了。每個線程分配到的棧容量越大,可以建立的線程數量自然就越少,建立線程時就越容易把剩下的內存耗盡。
  • 出現 StackOverflowError 異常時有錯誤棧可以閱讀,棧深度在大多數情況下達到1000~2000完全沒有問題,對於正常的方法調用(包括遞歸),這個深度應該完全夠用了。
  • 但是,如果是建立過多線程導致的內存溢出,在不能減少線程數或者更換 64 位虛擬機的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程。

1.3 本地方法棧

  • 本地方法棧與虛擬機棧所發揮的作用非常相似,它們之間的區別是虛擬機棧為虛擬機執行 Java 方法服務,而本地方法棧則為虛擬機棧使用到的 Native 方法服務。
  • 與虛擬機棧一樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。

1.4 Java 堆

Java 堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裡分配內存(但是,隨著技術發展,所有對象都分配在堆上也漸漸變得不是那麼“絕對”了)。請看下圖:

一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

Generational Heap Memory 模型

  • 對於大多數應用來說,Java 堆是 Java 虛擬機所管理的內存中最大的一塊。
  • Java 堆是垃圾收集器管理的主要區域,也被稱為“GC堆”。
  • Java 堆可以細分為新生代、老年代、永久代;再細緻一點可以分為 Eden、From Survivor、To Survivor、Tenured、Permanent 。
  • Java 堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像磁盤空間一樣。
  • 從內存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩衝區(TLAB)。
  • 如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出 OutOfMemoryError 異常。

1.4.1 Java 堆溢出

  • Java 堆用於存儲對象實例,只要不斷地創建對象,並且保證 GC Roots 到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生內存溢出異常。
  • Java 堆內存的 OOM 異常是實際應用中常見的內存溢出異常情況。當出現 Java 堆內存溢出時,異常堆棧信息 “java.lang.OutOfMemoryError” 會跟著進一步提示 “Java heap space” 。
  • 通常是先通過內存映像分析工具對 Dump 出來的堆轉儲快照進行分析,重點是確認內存中的對象是否是必要的,也就是要先分清楚到底是出現了內存洩漏還是內存溢出。
  • 如果是內存洩漏,可進一步通過工具查看洩露對象到 GC Roots 的引用鏈。於是就能找到洩露對象的類型信息及 GC Roots 引用鏈的信息,就可以比較準確地定位出洩露代碼的位置。
  • 如果不存在洩露,就是內存中的對象確實都還必須存活著,那就應當檢查虛擬機的堆參數(-Xmx 與 -Xms),與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的情況,嘗試減少程序運行期的內存消耗。

1.5 方法區

方法區與 Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

  • Java 虛擬機規範對方法區的限制非常寬鬆,除了和 Java 堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。
  • 這區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
  • 當方法區無法滿足內存分配需求時,將拋出 OutOfMemoryError 異常。

1.5.1 運行時常量池

  • 運行時常量池是方法區的一部分。
  • 常量池用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。
  • 運行時常量池相對於 Class 文件常量池的一個重要特徵是具備動態性,Java 語言並不要求常量一定只有編譯期才能產生,也就是並非預置入 Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是 String 類的 intern() 方法。
  • 當常量池無法再申請到內存時會拋出 OutOfMemoryError 異常。在 OutOfMemoryError 後面跟隨的提示信息時 “PermGen space” 。

1.6 直接內存

  • 直接內存並不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致 OutOfMemoryError 異常出現。
  • NIO 類,一種基於通道與緩衝區的 I/O 方式,它可以使用 Native 函數庫直接分配堆外內存,然後通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回複製數據。
  • 本機直接內存的分配不會受到 Java 堆大小的限制,但是,既然是內存,肯定還是會受到本機總內存(包括 RAM 以及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。
  • 由 DirectMemory 導致的內存溢出,一個明顯的特徵是在 Heap Dump 文件中不會看見明顯的異常,如果我們發現 OOM 之後 Dump 文件很小,而程序中有直接或間接使用了 NIO ,那就可以考慮檢查一下是不是這方面的原因。

二、內存分配策略

對象的內存分配,往大方向講,就是在堆上分配(但也可能經過 JIT 編譯後被拆散為標量類型並間接地棧上分配),對象主要分配在新生代的 Eden 區上,如果啟動了本地線程分配緩衝,將按線程優先在 TLAB 上分配。少數情況下也可能會直接分配在老年代中,分配的規則並不是固定的,其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。

2.1 對象優先在 Eden 分配

大多數情況下,對象在新生代 Eden 區中分配。當 Eden 區沒有足夠的空間進行分配時,虛擬機將發起一次 Minor GC 。舉個例子,看下面的代碼:

private static final int _1MB = 1024 * 1024;

/**

* VM 參數:-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];//出現一次 Minor GC

}

執行上面的testAllocation() 代碼,當分配 allocation4 對象的語句時會發生一次 Minor GC ,這次 GC 的結果是新生代 6651KB 變為 148KB ,而總內存佔用量則幾乎沒有減少(因為 allocation1、allocation2、allocation3 三個對象都是存活的,虛擬機幾乎沒有找到可回收的對象)。這次 GC 發生的原因是給 allocation4 分配內存時,發現 Eden 已經被佔用了 6MB ,剩餘空間已不足以分配 allocation4 所需的 4MB 內存,因此發生 Minor GC 。GC 期間虛擬機又發現已有的 3 個 2MB 大小的對象全部無法放入 Survivor 空間(從上圖中可看出 Survivor 空間只有 1MB 大小),所以只好通過分配擔保機制提前轉移到老年代去。

2.2 大對象直接進入老年代

  • 所謂的對象是指,需要大量連續內存空間的 Java 對象,最典型的大對象就是那種很長的字符串以及數組。經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。
  • 虛擬機提供了一個 -XX:PretenureSizeThreshold 參數,令大於這個設置值的對象直接在老年代分配。這樣做的目的是避免在 Eden 區及兩個 Survivor 區之間發生大量的內存複製(新生代採用複製算法收集內存)。

2.3 長期存活的對象將進入老年代

既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別到哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這點,虛擬機給每個對象定義了一個對象年齡計數器。如果對象在 Eden 出生並經過第一次 Minor GC 後仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並且對象年齡設為 1 。對象在 Survivor 區中每“熬過”一次 Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認15歲),就會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數 -XX:MaxTenuringThreshold 設置。

2.4 動態對象年齡判定

為了能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到 MaxTenuringThreshold 中的要求的年齡。

2.5 空間分配擔保機制

  • 在發生 Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼 Minor GC 可以確保是安全的。如果不成立,則虛擬機會查看 HandlePromotionFailure 設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試著進行一次 Minor GC ,儘管這次 Minor GC 是有風險的;如果小於,或者 HandlePromotionFailure 設置不允許冒險,那這次也要改為進行一次 Full GC。
  • 上面提到的“冒險”指的是,由於新生代使用複製收集算法,但為了內存利用率,只使用其中一個 Survivor 空間來作為輪換備份,因此當出現大量對象在 Minor GC 後仍然存活的情況,把 Survivor 無法容納的對象直接進入老年代。老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成內存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行 Full GC 來讓老年代騰出更多空間。
  • 取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次 Minor GC 存活後的對象突增,遠遠高於平均值的話,依然會導致擔保失敗。
  • 如果出現了HandlePromotionFailure 失敗,那就只好在失敗後重新發起一次 Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將 HandlePromotionFailure 開關打開,避免 Full GC 過於頻繁。
  • 但在 JDK 6 Update 24 之後,HandlePromotionFailure 參數不會再影響到虛擬機的控件分配擔保策略,只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行 Minor GC ,否則將進行 Full GC。

三、內存回收策略

  • 新生代 GC(Minor GC) :指發生在新生代的垃圾收集動作,因為 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。
  • 老年代 GC(Major GC / Full GC):值發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對)。Major GC 的速度一般會比 Minor GC 慢 10 倍以上。

3.1 內存回收關注的區域

  • 上面已經介紹 Java 內存運行時區域的各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅。
  • 棧中的棧幀隨著方法的進入和退出而有條不紊地執行者出棧和入棧操作。每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的。
  • 因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨著回收了。
  • 而 Java 堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的是這部分內存。

3.2 對象存活判斷

3.2.1 引用計數算法

  • 給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為 0 的對象就是不可能再被使用的。
  • 這種算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法,但它很難解決對象之間相互循環引用的問題。
  • 舉個例子,對象 objA 和 objB 都有字段 instance,賦值令 objA.instance = objB 及 objB.instance = objA ,除此之外,這兩個對象再無任何引用,實際上,這兩個對象已經不可能再被訪問,但是它們因為相互引用著對方,導致它們的引用計數都不為 0,於是引用計數算法無法通知 GC 收集器回收它們。

3.2.2 可達性分析算法

  • 這個算法的基本思路就是通過一系列額稱為“GC Roots” 的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連或者說這個對象不可達時,則證明此對象是不可用的。
  • 在 Java 語言中,可作為 GC Roots 的對象包括以下:
  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象
  2. 方法區中類靜態屬性引用的對象
  3. 方法區中常量引用的對象
  4. 本地方法棧中 JNI 引用的對象

請看下圖:

一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

可達性分析算法

3.3 方法區的回收

  • 方法區(HotSpot 虛擬機中的永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收 Java 堆的對象非常類似。
  • 判定一個類是否是“無用的類”需要同時滿足下面3個條件:
  1. 該類的所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  2. 加載該類的 ClassLoader 已經被回收。
  3. 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
  • 虛擬機可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是“可以”,而並不是和對象一樣,不使用了就必然回收。

3.4 垃圾收集算法

3.4.1 標記—清除算法

  • 算法分為 “標記” 和 “清除” 兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。
  • 它主要有兩個不足的地方:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而得不到提前觸發另一次垃圾收集動作。
  • 這是最基礎的收集算法,後續的收集算法都是基於這種思路並對其不足進行改進而得到的。
一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

“標記—清除”算法示意圖

3.4.2 複製算法

  • 為了解決效率問題,“複製”算法應運而生,它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中一塊。
  • 當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
  • 這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
  • 不足之處是,將內存縮小為原來的一半,代價太高。
一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

複製算法示意圖

舉個優化例子:新生代中的對象98%是“朝生夕死”的,所以並不需要按照 1:1 的比例來劃分內存空間,而是將內存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活著的對象一次性地複製到另一塊 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間。

再舉個優化例子:將 Eden 和 Survivor 的大小比例設為 8:1 ,也就是每次新生代中可用內存空間為整個新生代容器的 90%,只有10% 的內存作為保留區域。當然 98% 的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多於 10% 的對象存活,當 Survivor 空間不夠用時,需要依賴其他內存(這裡指老年代)進行分配擔保(空間分配擔保機制在上面,瞭解一下)。

3.4.3 標記—整理算法

複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。所以在老年代一般不能直接選用複製收集算法。

  • 根據老年代的特點,“標記—整理” 算法應運而生。
  • 標記過程仍然與 “標記—清除” 算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

“標記—整理”算法示意圖

3.4.4 分代收集算法

  • 根據對象存活週期的不同將內存劃分為幾塊,一般是把 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。
  • 在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。
  • 而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用 “標記—清除” 或者 “標記—整理” 算法來進行回收。
  • 當前商業虛擬機的垃圾收集都採用 “分代收集” 算法。

四、編程中的內存優化

相信大家在編程中都會注意到內存使用的問題,下面我就簡單列一下在實際操作當中需要注意的地方。

4.1 減小對象的內存佔用

  • 使用更加輕量的數據結構

我們可以考慮使用 ArrayMap / SparseArray 而不是 HashMap 等傳統數據結構。(我在老項目中,根據 Lint 提示,將 HashMap 替換成 ArrayMap / SparseArray 之後,在 Android Profiler 中顯示運行時內存比之前直接少了幾M,還是挺可觀的。)

  • 避免使用 Enum
  • 減小 Bitmap 對象的內存佔用
  1. inSampleSize :縮放比例,在把圖片載入內存之前,我們需要先計算出一個合適的縮放比例,避免不必要的大圖載入。
  2. decode format:解碼格式,選擇 ARGB_8888 / RBG_565 / ARGB_4444 / ALPHA_8,存在很大差異。
  • 使用更小的圖片:儘量使用更小的圖片不僅僅可以減少內存的使用,還可以避免出現大量的 InflationException。

4.2 內存對象的重複利用

  • 複用系統自帶的資源:Android系統本身內置了很多的資源,例如字符串/顏色/圖片/動畫/樣式以及簡單佈局等等,這些資源都可以在應用程序中直接引用。
  • 注意在 ListView / GridView 等出現大量重複子組件的視圖裡面對 ConvertView 的複用
  • Bitmap 對象的複用
  • 避免在 onDraw 方法裡面執行對象的創建:類似 onDraw() 等頻繁調用的方法,一定需要注意避免在這裡做創建對象的操作,因為他會迅速增加內存的使用,而且很容易引起頻繁的 GC,甚至是內存抖動。
  • StringBuilder:在有些時候,代碼中會需要使用到大量的字符串拼接的操作,這種時候有必要考慮使用 StringBuilder 來替代頻繁的 “+” 。

4.3 避免對象的內存洩露

  • 注意 Activity 的洩漏
  1. 內部類引用導致 Activity 的洩漏
  2. Activity Context 被傳遞到其他實例中,這可能導致自身被引用而發生洩漏。
  • 考慮使用 Application Context 而不是 Activity Context :對於大部分非必須使用 Activity Context 的情況(Dialog 的 Context 就必須是 Activity Context),我們都可以考慮使用 Application Context 而不是 Activity 的 Context,這樣可以避免不經意的 Activity 洩露。
  • 注意臨時 Bitmap 對象的及時回收:例如臨時創建的某個相對比較大的 bitmap 對象,在經過變換得到新的 bitmap 對象之後,應該儘快回收原始的 bitmap,這樣能夠更快釋放原始 bitmap 所佔用的空間。
  • 注意監聽器的註銷:在 Android 程序裡面存在很多需要 register 與 unregister 的監聽器,我們需要確保在合適的時候及時 unregister 那些監聽器。自己手動 add 的 listener,需要記得及時 remove 這個 listener。
  • 注意緩存容器中的對象洩漏:我們為了提高對象的複用性把某些對象放到緩存容器中,可是如果這些對象沒有及時從容器中清除,也是有可能導致內存洩漏的。
  • 注意 WebView 的洩漏:通常根治這個問題的辦法是為 WebView 開啟另外一個進程,通過 AIDL 與主進程進行通信,WebView 所在的進程可以根據業務的需要選擇合適的時機進行銷燬,從而達到內存的完整釋放。
  • 注意 Cursor 對象是否及時關閉

4.4 內存使用策略優化

  • 資源文件需要選擇合適的文件夾進行存放
  • Try catch 某些大內存分配的操作:在某些情況下,我們需要事先評估那些可能發生 OOM 的代碼,對於這些可能發生 OOM 的代碼,加入 catch 機制,可以考慮在 catch 裡面嘗試一次降級的內存分配操作。例如 decode bitmap 的時候,catch 到 OOM,可以嘗試把採樣比例再增加一倍之後,再次嘗試 decode。
  • 謹慎使用 static 對象:因為static的生命週期過長,和應用的進程保持一致,使用不當很可能導致對象洩漏。
  • 特別留意單例對象中不合理的持有:因為單例的生命週期和應用保持一致,使用不合理很容易出現持有對象的洩漏。
  • 珍惜Services資源:建議使用 IntentService
  • 優化佈局層次,減少內存消耗:越扁平化的視圖佈局,佔用的內存就越少,效率越高。我們需要儘量保證佈局足夠扁平化,當使用系統提供的 View 無法實現足夠扁平的時候考慮使用自定義 View 來達到目的。
  • 謹慎使用 “抽象” 編程
  • 使用 nano protobufs 序列化數據
  • 謹慎使用依賴注入框架
  • 謹慎使用多進程
  • 使用 ProGuard 來剔除不需要的代碼
  • 謹慎使用第三方 libraries
  • 考慮不同的實現方式來優化內存佔用

五、內存檢測工具

最後給推薦幾個內存檢測的工具,具體使用方法,可以自行搜索。當然除了下面這些工具,應該還有更多更好用的工具,只是我還沒有發現,如有建議,可以在文章下面評論留言,大家一起學習分享一下。

  • Systrace
  • Traceview
  • Android Studio 3.0 的 Android Profiler 分析器
  • LeakCanary

那麼如何學習才能快速入門並精通呢?

當真正開始學習的時候難免不知道從哪入手,導致效率低下影響繼續學習的信心。

但最重要的是不知道哪些技術需要重點掌握,學習時頻繁踩坑,最終浪費大量時間,所以有一套實用的視頻課程用來跟著學習是非常有必要的。

為了讓學習變得輕鬆、高效,今天給大家免費分享一套阿里架構師傳授的一套教學資源。幫助大家在成為架構師的道路上披荊斬棘。

這套視頻課程詳細講解了(Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構)等這些成為架構師必備的內容!

而且還把框架需要用到的各種程序進行了打包,根據基礎視頻可以讓你輕鬆搭建分佈式框架環境,像在企業生產環境一樣進行學習和實踐。

一篇文章帶你瞭解 Java 自動內存管理機制及性能優化

後臺私信回覆 “ 架構 ” 就可以馬上免費獲得這套價值一萬八的內部教材!


分享到:


相關文章: