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ù)器。當同時進行的線程數(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ū)嵗峙洌瑒tOutOfMemoryError杏死。
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ù)算法幽崩,當對象有一個引用,即增加一個計數(shù)寞钥;刪除一個引用慌申,即減少一個計數(shù)。計數(shù)為零的對象理郑,判斷為不可用蹄溉,但是無法處理循環(huán)引用的問題。現(xiàn)主流的都是可達性分析算法您炉,通過將一系列稱為GC Roots的對象作為起始點柒爵,開始向下搜索,走過的路徑則是引用鏈赚爵。若所有GC Roots都與某對象無引用鏈相連棉胀,即不可達時,判斷為不可用囱晴。
注意:GC Roots對象包括:虛擬機棧(棧幀中的本地變量表)中引用對象膏蚓;方法區(qū)中類靜態(tài)屬性引用的對象;方法區(qū)中常量池引用的對象畸写;本地方法棧(一般的本地方法,即JNI)中引用的對象氓扛。 - 必要判斷枯芬。當對象沒有重寫 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.垃圾收集算法
當確定哪些垃圾可以被回收后,需要做的就是高效地進行垃圾回收瓜客。由于JVM沒有給出明確的規(guī)定适瓦,各廠商實現(xiàn)方式不同,這里只討論常見垃圾收集算法的核心思想谱仪。
- 標記-清除算法玻熙,最基礎(chǔ)的算法,分為兩個階段疯攒。標記階段:標出所有需要被回收的對象嗦随;清除階段:回收被標記對象所占用的空間。但容易產(chǎn)生大量內(nèi)存碎片卸例,導(dǎo)致無足夠空間分配給大對象称杨,從而提前觸發(fā)垃圾收集動作。
- 復(fù)制算法筷转,為了解決標記-清除算法的缺陷姑原。將可用內(nèi)存按容量分為大小相等的兩塊,每次只使用其中的一塊呜舒。當一塊用完時锭汛,復(fù)制可用對象至另一塊并清除自己的內(nèi)存空間,從而避免出現(xiàn)內(nèi)存碎片袭蝗。但可用內(nèi)存為實際的一半唤殴,利用率低;且當存活對象很多時到腥,效率也會降低朵逝。
- 標記-整理算法,吸取以上兩種算法優(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收集器是當今收集器技術(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)容較深扁凛,大家可以參考這個系列文章忍疾。