1. 程式人生 > >Android 記憶體優化(一)

Android 記憶體優化(一)

記憶體結構

記憶體結構

  • 方法區
    執行緒共享,儲存類的資訊、常量、靜態變數、編譯後的程式碼
  • 堆區
    執行緒共享,所有物件的例項,陣列
  • 棧區
    執行緒私有,區域性變量表,操作棧,動態連結,方法出口,物件引用

記憶體管理

在 Android 中我們採用的是 paging(分頁)memory-mapping(mmapping-記憶體對映) 機制來管理記憶體的。通過這種機制我們把資料分成固定大小的區塊,當需要時就從硬碟中提取出來,載入到主存中,當不需要時就移回硬碟中。

記憶體容量

在 Android 中每一個 app 都限定了一個 heap size 的最大值。這個值會因為不同的裝置而有所不同。如果 app 到達了這個值後繼續申請記憶體的會就會引起 OutOfMemoryError 錯誤。我們可以通過

Runtime.getRuntime().maxMemory();

來獲取當前 app 可獲取的最大的 heap size。
如果擴大這個最大值,可以在 AndroidManifest 的 Application 中設定

android:largeHeap="true"

通過這種方法,就能擴大 heap size 的最大值。

GC(Garbage collection)

在 app 的執行過程中我們會建立很多的物件來實現不同的功能,這麼多的物件為什麼沒有觸發 OOM 呢?這是因為 Java 為我們提供了 GC(垃圾回收)機制。它能幫助我們銷燬掉不再使用的物件,為其他物件騰出空間。在這裡我們討論的是針對堆區和方法區中的GC,棧區中的空間只要利用棧來管理就行了十分方便。

堆區

判斷物件是否可回收

引用計數演算法

給每個物件新增一個引用計數器,每有一個物件引用它了,就 +1 ,失效時 -1 。當計數器為 0 的物件就是不再使用的,GC 就會對其進行回收。
這個方法很方便,但是,它很難解決物件相互迴圈引用的問題,如下圖,即使 Instance1 已經與原有引用斷開了關係,但是和其他物件形成了互相迴圈引用,計數器都為1,這樣就不能被回。

引用計數法

根搜尋法

這個方法利用了有向圖的概念,若不能從 GC-Root 到達的物件就為可回收的物件。改,方法就解決了物件相互迴圈引用的問題,只要 GC-Root 不可達即視為可回收。Java 就是採用了這種方法。

根搜尋法

GC-Root :

  • 虛擬機器棧中區域性變數引用的物件
  • 類靜態屬性引用的物件
  • 常量引用的物件
  • JNI中引用的物件

當檢測到物件不可達時,GC 不會立即對其進行回收,而是先對其進行標記。若發現沒有執行 finalize 方法,就會讓其進入 F-Queue 佇列,開啟另一個執行緒執行其 finalize 方法,若執行後發現它添加回 GC-Root 的引用鏈中了就不再理會,沒有則第二次標記,然後回收。執行過 finalize 的則直接第二次標記,然後回收。

GC-Root回收過程

引用的分類

強引用

在程式碼中普遍存在的

Object obj = new Object();

只要強引用還在,GC 就不會回收掉。當記憶體不足時,虛擬機器寧願丟擲 OOM 也不回收強引用

軟引用

在記憶體足夠的情況下不回收,在記憶體不夠用的時候才會回收

SoftReference ref = new SoftReference(new Object());
弱引用

當 GC 發生時就會被回收

WeakReference ref = new WeakReference(new Object());
虛引用

虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

方法區

回收的內容主要有:廢棄的常量和無用的類

  • 廢棄常量回收和堆中的物件回收時類似。
  • 無用的類
  • 該類的所有例項都已經卑回收,即堆區中無該類的任何例項
  • 該類的 ClassLoader 都已經被回收
  • 該類對應的 java.lang.Class 物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

記憶體回收演算法

標記清除(Mark-Sweep)

  • 標記:標記出不可達物件
  • 清除:回收標記的物件
  • 優點:簡單,易實現
  • 確定:容易產生記憶體碎片
    適用於小物件

複製演算法(Copying)

把記憶體區域分為容量相等的兩塊區域,每次只使用其中一塊,當一塊記憶體空間用完了,就把存活的物件複製到另一塊中,然後把剛使用過的記憶體空間一次清除
- 優點:簡單,不會有碎片
- 缺點:空間使用率低,當存活物件多時,效率會降低。

標記整理演算法(Mark-Compact)

與 Mark-Sweep 相似,只是標記後,對存活的物件進行了整理,把他們向一端移動,然後清除掉端邊界以外的記憶體。
適合存活物件多,回收物件少的情況。

分代(Generation)

結合了 Copying 和 Mark-Compact .Java 就是採用了分代收集法。

generation

  • 年輕代(Young Generation)
    新 new 出來的物件就會分配到這裡,採用 minor garbage collection(Copying)
    1. 新 new 一個物件時,這個物件會分配到 Eden
    2. 當 Eden 滿了的時候引用可達的物件就會進入 S0(Survivor),然後清空 Eden
    3. 當 S0 也滿了後,將 S0 中引用可達的物件移入 S1 ,請空 S0 。當兩個存活區切換了幾次後,任存活的物件複製進老年代
    4. Eden:S0:S1 = 8:1:1

  • 老年代(Old Generation)
    老年代空間會比年輕代要大,GC(Major GC|Full GC - Mark-Compact) 發生的次數也比年輕代少。大物件可直接進入老年代。採用標記整理法。

  • 永久代(Permanent Generation)
    包含應用的類/方法資訊,以及 JRE 庫的類和方法資訊

Stop-The-World

總的來說,GC 對於記憶體的管理來說是十分有好處的。但是,在 GC 的過程中會暫停主執行緒(我們稱為 stop-the-world)。我們知道,Android 所有 UI 的重新整理都是在主執行緒上的,如果 GC 的時長過長,或頻繁的GC(記憶體抖動)就會導致 UI 的重新整理的過程卡頓,超過 16ms 這時使用者就會感覺到介面的卡頓。