1.前言
Android的虛擬機是根據(jù)移動設(shè)備的特點基于Java虛擬機(JVM)改進而來,雖然沒有保留規(guī)范,但作為Java語言的使用者,了解一下JVM的規(guī)范還是有必要的。
2.JVM內(nèi)存模型
JVM在執(zhí)行Java程序時,會把它管理的內(nèi)存劃分為若干個的區(qū)域,每個區(qū)域都有自己的用途和創(chuàng)建銷毀時間。如下圖所示,可以分為兩大部分,線程私有區(qū)和共享區(qū):

2.1.線程私有區(qū)
- 程序計數(shù)器。當(dāng)同時進行的線程數(shù)超過CPU數(shù)或其內(nèi)核數(shù)時,就要通過時間片輪詢分派CPU的時間資源,不免發(fā)生線程切換。這時,每個線程就需要一個屬于自己的計數(shù)器來記錄下一條要運行的指令。如果將是Java方法,則記錄執(zhí)行的字節(jié)碼地址;是本地方法,則計數(shù)器為空。
- 虛擬機棧,與線程同時創(chuàng)建。每個方法執(zhí)行時都會創(chuàng)建一個棧幀來存儲方法的信息,新調(diào)用的方法入棧,返回的出棧,所以棧的大小決定方法調(diào)用的可達深度。若需要的棧深度大于可用深度時,則StackOverflowError;若棧進行擴展,但內(nèi)存不夠時,OutOfMemoryError。
- 本地方法棧,與虛擬機棧作用相似。但它不是為Java方法服務(wù)的,而是本地方法(C語言)。由于規(guī)范對這塊沒有強制要求,不同虛擬機實現(xiàn)方法不同。
2.2.線程共享區(qū)
此區(qū)域是用來存儲被各線程共享的數(shù)據(jù)的。
- 方法區(qū),用于存放加載類的元數(shù)據(jù)信息,如常量、靜態(tài)變量和即時編譯器編譯后的代碼。若要分代,算是永久代,以前類大多“static”的,很少被卸載或收集,現(xiàn)回收廢棄常量和無用的類。其中運行時常量池存放編譯生成的各種常量。
- 堆,存放對象實例和數(shù)組,是垃圾回收的主要區(qū)域,分為新生代和老年代。剛創(chuàng)建的對象在新生代的Eden區(qū)中,經(jīng)過GC后進入新生代的S0區(qū)中,再經(jīng)過GC進入新生代的S1區(qū)中,15次GC后仍存在就進入老年代。這是按照一種回收機制進行劃分的,不是固定的。若堆的空間不夠?qū)嵗峙?,則OutOfMemoryError。
2.3.注意事項
棧是運行時單位,代表著邏輯,內(nèi)含基本數(shù)據(jù)類型和堆中對象引用,所在區(qū)域連續(xù),沒有碎片;堆是存儲單位,代表著數(shù)據(jù),可被多個棧共享(包括成員中基本數(shù)據(jù)類型、引用和引用對象),所在區(qū)域不連續(xù),會有碎片。
3.垃圾回收
我們都知道調(diào)用 System.gc() 方法只是通知系統(tǒng)去回收,是否回收不能確定。
3.1.回收的判斷
JVM中,將一個對象真正回收需經(jīng)歷兩次標記過程,每次都是先判斷對象有沒有被持有引用,再判斷對象是否必要執(zhí)行 finalize() 方法。
- 持有判斷。最先使用是引用計數(shù)算法,當(dāng)對象有一個引用,即增加一個計數(shù);刪除一個引用,即減少一個計數(shù)。計數(shù)為零的對象,判斷為不可用,但是無法處理循環(huán)引用的問題。現(xiàn)主流的都是可達性分析算法,通過將一系列稱為GC Roots的對象作為起始點,開始向下搜索,走過的路徑則是引用鏈。若所有GC Roots都與某對象無引用鏈相連,即不可達時,判斷為不可用。
注意:GC Roots對象包括:虛擬機棧(棧幀中的本地變量表)中引用對象;方法區(qū)中類靜態(tài)屬性引用的對象;方法區(qū)中常量池引用的對象;本地方法棧(一般的本地方法,即JNI)中引用的對象。 - 必要判斷。當(dāng)對象沒有重寫 finalize() 方法或者 finalize() 方法已被虛擬機調(diào)用過,都將視為“沒有必要執(zhí)行”。否則此對象將放置在F-Queue的隊列中,由一個虛擬機自動建立的、低優(yōu)先級的Finalizer線程去觸發(fā)該方法,但不承諾等待它運行結(jié)束,以防執(zhí)行緩慢或為死循環(huán),導(dǎo)致隊列其它對象永久等待,乃至內(nèi)存回收系統(tǒng)崩潰。
- 對F-Queue中對象進行二次標記。只要有對象重新與GC Roots對象關(guān)聯(lián),就會被移出隊列,否則GC回收。
3.2.垃圾收集算法
當(dāng)確定哪些垃圾可以被回收后,需要做的就是高效地進行垃圾回收。由于JVM沒有給出明確的規(guī)定,各廠商實現(xiàn)方式不同,這里只討論常見垃圾收集算法的核心思想。
- 標記-清除算法,最基礎(chǔ)的算法,分為兩個階段。標記階段:標出所有需要被回收的對象;清除階段:回收被標記對象所占用的空間。但容易產(chǎn)生大量內(nèi)存碎片,導(dǎo)致無足夠空間分配給大對象,從而提前觸發(fā)垃圾收集動作。
- 復(fù)制算法,為了解決標記-清除算法的缺陷。將可用內(nèi)存按容量分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)一塊用完時,復(fù)制可用對象至另一塊并清除自己的內(nèi)存空間,從而避免出現(xiàn)內(nèi)存碎片。但可用內(nèi)存為實際的一半,利用率低;且當(dāng)存活對象很多時,效率也會降低。
- 標記-整理算法,吸取以上兩種算法優(yōu)點。標記階段與標記-清除算法同階段一致,整理階段則將存活對象移向一端,再清理邊界以外空間。
- 分代收集算法,目前主流。根據(jù)對象存活的生命周期,將內(nèi)存劃分為兩大區(qū)域。老年代:每次垃圾收集只有少量對象需回收,一般采用標記-整理算法;新生代:每次垃圾收集都有大量對象需回收,大部分采用復(fù)制算法。但空間上不是等大的兩塊,而是一塊大的Eden區(qū)域,兩塊小的Survivor區(qū)域,每次只使用一大一小兩區(qū)域。垃圾回收時,它們將內(nèi)部存活對象都移至空閑的小區(qū)域并清理自己。
3.3.垃圾收集器
垃圾收集算法是理論,而垃圾收集器是實現(xiàn)。下面根據(jù)海子的文章列出HotSpot(JDK 7)提供的幾種垃圾收集器。
- Serial/Serial Old 收集器是最基本最古老的收集器,它是一個單線程收集器,并且在它進行垃圾收集時,必須暫停所有用戶線程。Serial收集器是針對新生代的收集器,采用的是Copying算法;Serial Old收集器是針對老年代的收集器,采用的是Mark-Compact算法。它的優(yōu)點是實現(xiàn)簡單高效,但是缺點是會給用戶帶來停頓。
- ParNew 收集器是Serial收集器的多線程版本,使用多個線程進行垃圾收集。
- Parallel Scavenge/Parallel Old 收集器是多線程(并行)收集器。Parallel Scavenge收集器是針對新生代的收集器,它在回收期間不需要暫停其他用戶線程,其采用的是Copying算法,該收集器與前兩個收集器有所不同,它主要是為了達到一個可控的吞吐量;Parallel Old收集器是針對老年代的收集器,使用多線程和Mark-Compact算法。
- CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,它是一種并發(fā)收集器,采用的是Mark-Sweep算法。
- G1收集器是當(dāng)今收集器技術(shù)發(fā)展最前沿的成果,它是一款面向服務(wù)端應(yīng)用的收集器,它能充分利用多CPU、多核環(huán)境。因此它是一款并行與并發(fā)收集器,并且它能建立可預(yù)測的停頓時間模型。
4.內(nèi)存分配
內(nèi)存分配主要是在堆上分配,由于涉及到分配時某區(qū)域空間不足等問題,需結(jié)合垃圾收集器和JVM相關(guān)參數(shù),所以規(guī)則不是固定的。
5.總結(jié)
到這里,基本上可以在寫代碼時大致知道對象的內(nèi)存情況,所以一定要注意避免內(nèi)存泄露及其導(dǎo)致的內(nèi)存溢出問題。由于內(nèi)容較深,大家可以參考這個系列文章。