Android中緩存理解(二)

PhatomReference的作用

Java對象的狀態(tài)

  • 可達(dá)狀態(tài):有一個以上的引用變量引用(強(qiáng)引用);
  • 可恢復(fù)狀態(tài):不存在任何引用,且JVM在回收之前會調(diào)用finalize()清理資源后才真正回收,如果此期間使對象重獲引用變?yōu)榭蛇_(dá)狀態(tài);
  • 不可達(dá)狀態(tài):不存在任何引用,且JVM執(zhí)行完finalize()后仍無引用,變?yōu)椴豢蛇_(dá)狀態(tài),JVM開始回收該對象。

參考Java的內(nèi)存回收機(jī)制

對象的周期階段

當(dāng)新建一個對象時,會置位該對象的一個內(nèi)部標(biāo)識finalizable,當(dāng)某一點(diǎn)GC檢查到該對象不可達(dá)時,就把該對象放入finalize queue(F queue),GC會在對象銷毀前執(zhí)行finalize方法并且清空該對象的finalizable標(biāo)識。

簡而言之,一個簡單的對象生命周期為,Unfinalized Finalizable Finalized Reclaimed。

Object.finalize()

對象的finalize()被JVM調(diào)用后,才算對象被銷毀。

一般對象通過覆寫Object.finalize(),實(shí)現(xiàn)在對象回收之前,對GC無法回收的內(nèi)存進(jìn)行釋放。

  1. Object.finalize()是JVM調(diào)用的,而JVM在回收時不保證一定會調(diào)用它,并且不能確定執(zhí)行的時機(jī);
  2. JVM執(zhí)行Object.finalize()過程中會導(dǎo)致嚴(yán)重的內(nèi)存消耗和性能損失;
  3. 由于需要覆寫才能被執(zhí)行,很可能在實(shí)現(xiàn)時重獲自身引用...不安全且效率低。
public class A {  
    static A a;  
    public void finalize() {  
        a = this;  
    }  
} 

上述代碼如果JVM調(diào)用了finalize(),那么原本應(yīng)該被回收的對象重生,GC無法回收該對象。

所以如果想通過Object.finalize()來實(shí)現(xiàn)對象相關(guān)資源的釋放是不可靠的。一般使用PhatomReference來完成對象回收之前的資源釋放。

關(guān)于finalize()不安全,效率低可以參考Effective Java Item7:Avoid Finalizers,解釋為什么finalize是不安全的,不建議使用

PhatomReference回收過程

PhatomReference與SoftReference(WeakReference)最大區(qū)別在于JVM回收它們所引用的對象進(jìn)行的處理:

JVM會把SoftReference(WeakReference)中refernet字段設(shè)置為null,然后再將其加入ReferenceQueue中,此時所引用的對象處于可回收的狀態(tài);

而JVM不會在把PhatomReference加入隊(duì)列前做相同的處理,而是直接加入,此時所引用的對象還不可回收。

referent字段是Reference中的私有字段。參考java中虛引用PhantomReference與弱引用WeakReference(軟引用SoftReference)的差別

大致的流程:

  1. JVM的DC線程發(fā)現(xiàn)只要虛引用的對象時,直接將虛引用加入隊(duì)列,此時PhatomReference所引用的對象還存在PhatomReference對他的引用;
  2. 當(dāng)程序通過隊(duì)列的poll(),將該虛引用從隊(duì)列中刪除時,它所引用的對象(包括虛引用本身)不再要引用,此時DC可以進(jìn)行回收。

綜上所述,可以看到JVM的GC線程不能夠?qū)μ撘每刂频膶ο筮M(jìn)行自動回收,而需要程序手動回收。

回收對象前的處理

虛引用即無法通過get()獲取對象的強(qiáng)引用,也無法自動被回收,那么它存在的意義呢?它的意義就是跟蹤對象回收活動,在對象被回收之前做一些處理。例如資源釋放等等。

了解了PhatomReference回收的過程,可以通過隊(duì)列的判斷(poll方法)在對象被回收之前進(jìn)行處理。這樣的實(shí)現(xiàn)不僅安全,而且高效。

注意在PhatomReference被加入隊(duì)列之前,所引用的對象已經(jīng)處于Reclaimed(或者說是不可及狀態(tài)),下一步就是被JVM回收,而并不是JVM調(diào)用finalize()才開始回收。所以在隊(duì)列中發(fā)現(xiàn)PhatomReference保證了對象已經(jīng)Finalized(銷毀,不可能再重獲)并且一定能被回收。

相比使用SoftReference或WeakReference去清理對象占用的相關(guān)資源(通過finalize方法)更加高效,而且實(shí)現(xiàn)起來更安全(finalize方法不一定執(zhí)行等因素)。

weakReference或SoftReference被放入綁定的隊(duì)列是因?yàn)槠湟玫膶ο罂杉靶园l(fā)生改變,處于可恢復(fù)狀態(tài)(也就是對象周期階段的finalizable),下一步就是JVM調(diào)用finalize(),最后回收。由于可以在finalize()中重獲引用,所以加入隊(duì)列不能作為對象被回收的依據(jù)。

總結(jié)

PhatomReference所引用的對象只有處于Reclaimed時才會被加入隊(duì)列(已經(jīng)被Finalized處于不可及狀態(tài))。此時可以在對象被回收之前執(zhí)行清理工作,比finalize()更加靈活,高效,安全。從而更精細(xì)的控制對象的生命周期。

參考Java引用類型分析、深入理解ReferenceQueue GC finalize Reference

Java應(yīng)用程序

Java應(yīng)用是一個進(jìn)程,都持有自己的虛擬機(jī)。而在Java編程中,由于虛擬機(jī)屬于單進(jìn)程多線程,所以并發(fā)編程是關(guān)于線程的開發(fā)。

例如通過Java內(nèi)置工具 jps指令可以打印出當(dāng)前正在運(yùn)行Java程序進(jìn)程ID號。

由此關(guān)于Java中Piped I/O必須在同一個虛擬機(jī)中的兩個線程才能用于傳輸信息。

而在Android開發(fā)中,Dalvik虛擬機(jī)啟動時可以通過-XX:HeapGrowthLimit來給Dalvik的GrowthLimit設(shè)置大小,因?yàn)槊總€應(yīng)用程序是一個進(jìn)程,都持有自己的Dalvik。

Android虛擬機(jī)

Android虛擬機(jī)主要有兩個,ART和Dalvik。

ART在GC上做的比Dalvik好太多了,不光是GC的效率,減少Pause時間,而且還在內(nèi)存分配上對大內(nèi)存的有單獨(dú)的分配區(qū)域,同時還能有算法在后臺做內(nèi)存整理,減少內(nèi)存碎片。

對于開發(fā)者來說ART下基本可以避免很多類似GC導(dǎo)致的卡頓問題了。另外根據(jù)谷歌自己的數(shù)據(jù)來看,ART相對Dalvik內(nèi)存分配的效率提高了10倍,GC的效率提高了2-3倍。

以下參考自Android GC 那點(diǎn)事

Dalvik

Java堆

Dalvik運(yùn)行時數(shù)據(jù)區(qū)中堆得結(jié)構(gòu)主要由兩部分組成:Active堆和Zygote堆。

  • Active堆,是Zygote進(jìn)程Fork第一個子進(jìn)程之前創(chuàng)建的。
  • Zygote堆,用來管理Zygote進(jìn)程啟動過程中預(yù)加載和創(chuàng)建的對象。

對于Android操作系統(tǒng)來說,以后所有的應(yīng)用程序進(jìn)程都是被Zygote進(jìn)程Fork出來的,并且持有各自的Dalvik虛擬機(jī)。

Fork是類Unix系統(tǒng)創(chuàng)建新進(jìn)程的方法。如果需要創(chuàng)建新進(jìn)程,就通過Fork來創(chuàng)建一個自身的副本,該副本就是子進(jìn)程,而且會創(chuàng)建單獨(dú)的地址空間。這樣一來確保子進(jìn)程擁有父進(jìn)程所有內(nèi)存段的精確副本。

Cow策略

在創(chuàng)建應(yīng)用程序的過程中,Dalvik虛擬機(jī)采用Cow策略復(fù)制Zygote進(jìn)程的地址空間。

Cow策略:當(dāng)未復(fù)制Zygote進(jìn)程的地址空間時,Zygote和應(yīng)用進(jìn)程使用同一塊分配對象的堆。當(dāng)應(yīng)用進(jìn)程或Zygote進(jìn)程對該堆進(jìn)行寫操作時,內(nèi)核開始執(zhí)行真正的復(fù)制操作。使得Zygote和應(yīng)用進(jìn)程擁有各自的拷貝。

為什么是寫操作時開始復(fù)制

是因?yàn)樵谝粔K共享內(nèi)存上,有一方修改了共享變量的值,為了實(shí)現(xiàn)數(shù)據(jù)同步(屬于內(nèi)核操作),必須進(jìn)行復(fù)制,確保共享變量在每一方中副本相同。

也就是在創(chuàng)建應(yīng)用程序時,對堆內(nèi)存進(jìn)行寫操作。此時內(nèi)核就開始執(zhí)行復(fù)制操作,保證子進(jìn)程擁有父進(jìn)程精確副本。這個過程就是Zygote進(jìn)程Fork子進(jìn)程,采用Cow策略將Zygote進(jìn)程的內(nèi)容拷貝給子進(jìn)程。

減少或避免Copy

因?yàn)閺?fù)制操作十分耗時,所以應(yīng)該盡量去減少或避免它,所以Dalvik運(yùn)行時數(shù)據(jù)區(qū)中堆內(nèi)存劃分為Active和Zygote堆。

在Zygote進(jìn)程Fork第一個子進(jìn)程之前,把堆內(nèi)存劃分為兩塊:已使用部分和未使用部分。分別稱為Zygote堆和Active堆。

這樣做的目的是每當(dāng)創(chuàng)建一個子進(jìn)程時,只需要把Zygote堆中的內(nèi)容復(fù)制給應(yīng)用程序進(jìn)程。而后續(xù)Zygote進(jìn)程和應(yīng)用程序進(jìn)程都是在Active堆中分配對象。減少了Zygote堆的寫操作,從而減少了執(zhí)行寫時拷貝的操作(后續(xù)都是在Active中執(zhí)行寫,內(nèi)核拷貝的也是Active堆內(nèi)容)。

這樣的劃分還有一個好處,Zygote堆內(nèi)是Zygote進(jìn)程啟動過程中預(yù)加載的類,資源和對象。意味著它們可以在Zygote進(jìn)程和應(yīng)用程序進(jìn)程中做到長期共享,還能減少對內(nèi)存的需求。

GC指標(biāo)

  • Starting Size,在啟動Dalvik時,系統(tǒng)分配一塊初始大小堆內(nèi)存給虛擬機(jī)使用??梢酝ㄟ^-Xms設(shè)置;
  • GrowthLimit,是系統(tǒng)給每一個程序的最大堆上限,超過這個上限,程序就會OOM??梢酝ㄟ^-XX:HeapGrowthLimit設(shè)置;
  • Maximum Size,不受控情況下的最大堆內(nèi)存大小,起始就是我們在用largeheap屬性的時候,可以從系統(tǒng)獲取的最大堆大小??梢酝ㄟ^-Xms設(shè)置。

設(shè)置不同堆內(nèi)存大小是通過不同的指令,在虛擬機(jī)啟動時設(shè)置的。

最熟悉的就是在AndroidMainFest.xml文件中給Maximum Size賦值largeheap屬性獲取到系統(tǒng)分配給應(yīng)用最大堆內(nèi)存的大小。如果Android在分配對象內(nèi)存時,超過了Maximum,就會OOM。

還有一些指標(biāo):

  • MinFree,堆最小利用率;
  • MaxFree,堆最大利用率;
  • TargetUtilization,目標(biāo)利用率。

假設(shè)當(dāng)前虛擬機(jī)GC后,堆中存活對象的占用內(nèi)存大小為LiveSize,那么該堆內(nèi)存的理想大小應(yīng)為(LiveSize / TargetUtilization)。但是理想大小不應(yīng)該小于(MinFree + LiveSize),不應(yīng)該大于(MaxFree + LiveSize)。每次虛擬機(jī)GC后都會盡量把堆利用率向目標(biāo)利用率靠攏。

當(dāng)程序去嘗試給大對象分配堆內(nèi)存,甚至去擴(kuò)大堆內(nèi)存大小。此時虛擬機(jī)GC活動,存活的對象變少了(例如局部變量由于方法的出棧被清除等,導(dǎo)致引用的對象被回收)。但是虛擬機(jī)的目標(biāo)利用率沒有改變,而GC就是為了使堆內(nèi)存利用率向目標(biāo)利用率靠攏,導(dǎo)致堆內(nèi)存擴(kuò)容失敗,甚至減小,從而導(dǎo)致頻繁的GC。

GC類型

  • GC_FOR_MALLOC,在堆內(nèi)存上分配對象內(nèi)存不足時觸發(fā)GC;
  • GC_CONCURRENT,當(dāng)應(yīng)用程序的堆內(nèi)存達(dá)到一定量,或者可以理解為快要滿的時候,系統(tǒng)會自動觸發(fā)GC操作來釋放內(nèi)存;
  • GC_BEFORE_OOM,在準(zhǔn)備拋OOM異常之前進(jìn)行的最后努力而觸發(fā)的GC;
  • GC_EXPLICT,程序調(diào)用System.gcVMRuntime.gc方法或者收到SIGUSR1信號時觸發(fā)的GC。

前三者都是在給對象分配堆內(nèi)存過程中觸發(fā)的。并發(fā)GC和非并發(fā)GC的區(qū)別主要在于前者在GC過程中,有條件地掛起和喚醒非GC線程,而后者在執(zhí)行GC的過程中,一直都是掛起非GC線程的。從而可以看出并發(fā)GC的程序又更好的響應(yīng)性。

對象的分配和GC觸發(fā)時機(jī)

  1. 調(diào)用dvmHeapSourceAlloc方法分配指定大小的堆內(nèi)存,成功返回地址給調(diào)用者。這個方法不會改變堆當(dāng)前大小,屬于輕量級內(nèi)存分配操作。
  2. 上一步失敗,那么執(zhí)行GC。如果GC線程正在運(yùn)行時,調(diào)用dvmWaitForConcurrentGCToComplete等待GC完成;否則調(diào)用gcForMalloc執(zhí)行GC,并且傳入false參數(shù)表示不回收軟引用。
  3. GC完畢后,再次執(zhí)行第一步嘗試輕量級內(nèi)存分配操作,成功就返回地址給調(diào)用者。
  4. 上一步失敗,調(diào)用dvmHeapSourceAllocAndGrow方法進(jìn)行分配。該方法首先會考慮將堆內(nèi)存當(dāng)前大小調(diào)整到Dalvik啟動時指定的Java堆最大值,然后去分配內(nèi)存。如果成功,就返回地址比給調(diào)用者。
  5. 上一步失敗,調(diào)用gcForMalloc,并且傳入true參數(shù),回收SoftReference引用的對象。
  6. GC完畢后,再次調(diào)用dvmHeapSourceAllocAndGrow方法進(jìn)行內(nèi)存分配。無論成功與否,就到此為止。
對象分配和GC觸發(fā)時機(jī)-Dalvik.png

Java中對象的回收前提是對象無法到達(dá)GC Root,而非強(qiáng)引用更進(jìn)一步的去控制對象的生命周期(即使可以到達(dá)GC Root,也有可能被回收)。

綜上所述,在對象的分配中會導(dǎo)致GC,第一次分配對象失敗系統(tǒng)會觸發(fā)GC但是不回收Soft的引用,如果再次分配還是失敗系統(tǒng)就會將Soft的內(nèi)存也給回收。前者觸發(fā)的GC是GC_FOR_MALLOC類型的GC,后者是GC_BEFORE_OOM類型的GC。而當(dāng)內(nèi)存分配成功后,系統(tǒng)會判斷當(dāng)前的內(nèi)存占用是否是達(dá)到了GC_CONCURRENT的閥值,如果達(dá)到了那么又會觸發(fā)GC_CONCURRENT。

總結(jié)

所以對于Dalvik虛擬機(jī)的手機(jī)來說,我們首先要盡量避免掉頻繁生成很多臨時小變量(比如說:getView, onDraw等函數(shù)中new對象),另一個又要盡量去避免產(chǎn)生很多長生命周期的大對象。

ART

Java堆

ART運(yùn)行時數(shù)據(jù)區(qū)中Java堆主要劃分為:Image Space,Zygote Space,Allocation Space和LargeObject Space。

  • Image Space,用于存放一些預(yù)加載的類;
  • Zygote Space,作用與Dalvik中的Zygote堆一樣;
  • Allocation Space,作用與Dalvik中的Active堆一樣;
  • LargeObject Space,一些離散地址的集合,用來分配一些大對象從而提高了GC的管理效率和整體性能。
LargeObject Space好處

小時候都玩過俄羅斯方塊,為了存放更多的方塊,需要合理安排好方塊放置的位置和方塊的方向。Java堆內(nèi)存分配也類似,合理的安排好對象的地址空間,可以提高堆內(nèi)存的使用率,從而減少GC。

Mark and Sweep算法-Dalvik.png

Dalvik使用標(biāo)記與清理算法,容易產(chǎn)生碎片。這時Active堆沒有合理的位置和空間(當(dāng)內(nèi)存中有大量不連續(xù)的小內(nèi)存段,圖中紅色)存放接下來的較大內(nèi)存(就像俄羅斯方塊游戲,無法將新的方塊填入有空隙的行,只能新建一個行,這樣使用率下降,需要去不斷的GC,釋放新的空間),再分配一個較大的對象時,例如decode一個圖片,很容易導(dǎo)致GC(去清理這些碎片)。

LargeObject Space用途-ART.png

由于ART有了LargeObject Space,大對象都分配到該區(qū)域,而Allocation Space分配較小的對象,從而使整個內(nèi)存使用率更高(就像俄羅斯方塊一樣,內(nèi)存形狀固定,在Allocation Space中合理安排較小的對象,可以提升整塊內(nèi)存的覆蓋率,也就是俄羅斯方塊中的規(guī)則每一行都填滿可以有更多的空間放更多的方塊)。

上述圖中波浪線表示程序代碼,藍(lán)色方塊表示較大對象,紅色表示小對象,綠色表示大對象。

GC類型

  • kGcCauseForAlloc: 當(dāng)要分配內(nèi)存的時候發(fā)現(xiàn)內(nèi)存不夠的情況下引起的GC,這種情況下的GC會Stop World.
  • kGcCauseBackground: 當(dāng)內(nèi)存達(dá)到一定的閥值的時候會去出發(fā)GC,這個時候是一個后臺GC,不會引起Stop World.
  • kGcCauseExplicit,顯示調(diào)用的時候進(jìn)行的gc,如果ART打開了這個選項(xiàng)的情況下,在system.gc的時候會進(jìn)行GC.
  • 其他更多。

stop world 是指在進(jìn)行垃圾回收的時候,需要將所有正在執(zhí)行的線程暫停(Stop World),保證GC線程的運(yùn)行。

對象的分配和GC觸發(fā)時機(jī)

這點(diǎn)和Dalvik虛擬機(jī)一樣,可以回憶一下Dalvik知識點(diǎn)。

ART對象分配和GC觸發(fā)時機(jī).png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容