Android記憶體分配/回收的一個問題-為什麼記憶體使用很少的時候也GC

看書的小蝸牛發表於2017-10-21

Android應用建立在Java虛擬機器之上的,Google為了保證同時多個APP執行並及時喚醒,就為每個虛擬機器設定了最大可使用記憶體,通過adb命令可以檢視相應的幾個引數,

* [dalvik.vm.heapgrowthlimit]: [192m]
* [dalvik.vm.heapmaxfree]: [8m]
* [dalvik.vm.heapminfree]: [512k]
* [dalvik.vm.heapsize]: [512m]
* [dalvik.vm.heapstartsize]: [8m]
* [dalvik.vm.heaptargetutilization]: [0.75]複製程式碼

其中dalvik.vm.heapsize是最大可以使用的記憶體,這個數值同廠商跟版本都有關係,隨著配置的提高,都在逐漸增大,既然虛擬機器能使用的最大記憶體是dalvik.vm.heapsize,那麼在申請記憶體的時候是不是一直到最大值才會GC呢?答案肯定是否定的,從我們檢測的曲線來看,在記憶體使用很低的時候,也會GC,看下圖APP執行時情況:

記憶體檢測曲線
記憶體檢測曲線

從上圖看到,1,2,3這三個點好像是都發生了GC,但是這個時候,APP記憶體的佔用並不是很高,距離最大記憶體還有很遠,那麼這個時候為什麼會發生記憶體GC呢,其實直觀上也比較好理解,如果一直等到最大記憶體才GC,那麼就會有兩個弊端:首先,記憶體資源浪費,造成系統效能降低,其次,GC時記憶體佔用越大,耗時越長,應儘量避免。那GC的時機到底是什麼時候呢?是不是每次記憶體塊分配的時候都會GC,這個應該也是否定的,本文就來簡單的瞭解下記憶體分配、GC、記憶體增長等機制。

Android Dalvik虛擬機器分配及GC

首先看一下虛擬機器的配置引數的意義,上面只講述了dalvik.vm.heapstartsize,是最大記憶體申請尺寸,

  • dalvik.vm.heapgrowthlimit和dalvik.vm.heapsize都是java虛擬機器的最大記憶體限制,一般heapgrowthlimit< heapsize,如果在Manifest中的application標籤中宣告android:largeHeap=“true”,APP直到heapsize才OOM,否則達到heapgrowthlimit就OOM
  • dalvik.vm.heapstartsize Java堆的起始大小,指定了Davlik虛擬機器在啟動的時候向系統申請的實體記憶體的大小,後面再根據需要逐漸向系統申請更多的實體記憶體,直到達到MAX
  • dalvik.vm.heapminfree 堆最小空閒值,GC後
  • dalvik.vm.heapmaxfree堆最大空閒值
  • dalvik.vm.heaptargetutilization 堆目標利用率

後面三個值用來確保每次GC之後Java堆已經使用和空閒的記憶體有一個合適的比例,這樣可以儘量地減少GC的次數,堆的利用率為U,最小空閒值為MinFree位元組,最大空閒值為MaxFree位元組,假設在某一次GC之後,存活物件佔用記憶體的大小為LiveSize。那麼這時候堆的理想大小應該為(LiveSize / U)。但是(LiveSize / U)必須大於等於(LiveSize + MinFree)並且小於等於(LiveSize + MaxFree),否則,就要進行調整,調整的其實是軟上限softLimit,

static size_t getUtilizationTarget(const HeapSource* hs, size_t liveSize)
{
    size_t targetSize = (liveSize / hs->targetUtilization) * HEAP_UTILIZATION_MAX;

    if (targetSize > liveSize + hs->maxFree) {
        targetSize = liveSize + hs->maxFree;
    } else if (targetSize < liveSize + hs->minFree) {
        targetSize = liveSize + hs->minFree;
    }
    return targetSize;
}複製程式碼

以上就是計算公式的原始碼,假設liveSize = 150M,targetUtilization=0.75,maxFree=8,minFree=512k,那麼理想尺寸200M,而200M很明顯超過了150+8,那麼這個時候,堆的尺寸就應該調整到158M,這個softLimit軟上限也是下次申請記憶體時候是否需要GC的一個重要指標,請看以下場景:

場景一:當前softLimit=158M,liveSize = 150M,如果這個時候,需要分配一個100K記憶體的物件

由於當前的上限是158M,記憶體是可以直接分配成功的,分配之後,由於空閒記憶體8-100K>512k,也不需要調整記憶體,這個時候,不存在GC,

Dalvik虛擬的記憶體分配策略--足夠.jpg
Dalvik虛擬的記憶體分配策略--足夠.jpg

場景二:當前softLimit=158M,liveSize = 150M,如果這個時候,需要分配的記憶體是7.7M

由於當前的上限是158M,記憶體是可以直接分配成功的,分配之後,由於空閒記憶體8-7.7M < 512k,那就需要GC,同時調整softLimit

Dalvik虛擬的記憶體分配策略--不夠.jpg
Dalvik虛擬的記憶體分配策略--不夠.jpg

場景三:當前softLimit=158M,liveSize = 150M,如果這個時候,需要分配的記憶體是10M

由於當前的上限是158M,記憶體分配失敗,需要先GC,GC之後調整softLimit,再次請求分配,如果還是失敗,將softLimit調整為最大,再次請求分配,失敗就再GC一次軟引用,再次請求,還是失敗那就是OOM,成功後要調整softLimit

Dalvik虛擬的記憶體分配策略--不夠GC.jpg
Dalvik虛擬的記憶體分配策略--不夠GC.jpg

所以,Android在申請記憶體的時候,可能先分配,也可能先GC,也可能不GC,這裡面最關鍵的點就是記憶體利用率跟Free記憶體的上下限,下面簡單看原始碼瞭解下堆記憶體分配流程:

   static void *tryMalloc(size_t size)
    {
        void *ptr;
        <!--1 首次請求分配記憶體-->
        ptr = dvmHeapSourceAlloc(size);
        if (ptr != NULL) {
            return ptr;
        }
        <!--2 分配失敗,GC-->
        if (gDvm.gcHeap->gcRunning) {
            dvmWaitForConcurrentGcToComplete();
        } else {
          gcForMalloc(false);
        }
        <!--再次分配-->
        ptr = dvmHeapSourceAlloc(size);
        if (ptr != NULL) {
            return ptr;
        }
         <!--還是分配失敗,調整softLimit再次分配-->
        ptr = dvmHeapSourceAllocAndGrow(size);
        if (ptr != NULL) {
            size_t newHeapSize;
       <!--分配成功後要調整softLimit-->
            newHeapSize = dvmHeapSourceGetIdealFootprint();
            return ptr;
        }
         <!--還是分配失敗,GC力加強,回收soft引用,-->
        gcForMalloc(true);
        <!--再次請求分配,如果還是失敗,那就OOM了-->
        ptr = dvmHeapSourceAllocAndGrow(size);
        if (ptr != NULL) {
            return ptr;
        }
        dvmDumpThread(dvmThreadSelf(), false);            return NULL;  
        }複製程式碼

總結

本文主要說的一個問題就是,為什麼不等到最大記憶體在GC,以及普通GC的可能時機,當然,對於記憶體的GC是更加複雜的,不在本文的討論範圍之內,同時這個也解釋頻繁的分配大記憶體會導致GC抖動的原因,畢竟,如果你超過了maxFree ,就一定GC,有興趣可以自行深入分析。

作者:看書的小蝸牛
原文連結:Android記憶體分配/回收的一個問題-為什麼低記憶體的時候也GC
僅供參考,歡迎指正

相關文章