背景介紹
- Java優(yōu)勢之一就是其具有垃圾回收機(jī)制不撑。在大部分情況下廷粒,JVM的GC(垃圾回收器)能夠幫助我們回那些不可到達(dá)的對象(就是未被引用的對象)祖搓。
- 當(dāng)然底桂,在一些情況下,我們?nèi)匀恍枰约喝メ尫艃?nèi)存(就是把對象引用置null筋遭,把容器打颤、數(shù)組清空),否則就會引起內(nèi)存泄漏漓滔,內(nèi)存泄漏嚴(yán)重時將容易引發(fā)
OutOfMemoryError
编饺,詳情見內(nèi)存泄漏。 - 此外响驴,由于GC會停止所有的線程透且,包括UI線程,所以頻繁的GC必然會導(dǎo)致畫面卡頓(Android中每16ms為一幀)豁鲤,因此還應(yīng)避免GC的頻繁發(fā)生秽誊。一個導(dǎo)致GC頻繁發(fā)生的原因就是內(nèi)存抖動,點擊鏈接看詳情琳骡。
- 所以锅论,理解Java的內(nèi)存機(jī)制,有助于幫助我們在寫代碼的過程中避免內(nèi)存泄漏楣号。
走進(jìn)內(nèi)存模型
Java內(nèi)存層級
;
;
程序計數(shù)器
程序計數(shù)器是線程私有的內(nèi)存區(qū)域最易,這個區(qū)域是Java虛擬機(jī)中唯一一個沒有限制OutOfMemoryError
的內(nèi)存區(qū)域怒坯。之所以需要它是因為Java的多線程機(jī)制是通過輪流切換分配處理器執(zhí)行時間來實現(xiàn)的,所以會涉及到線程的暫停和重啟藻懒,而在一個線程中如果正在執(zhí)行Java方法的話剔猿,這個計數(shù)器就回去記錄當(dāng)前正在執(zhí)行的虛擬機(jī)字節(jié)碼,一旦被暫停嬉荆,恢復(fù)只需要從程序計數(shù)器記錄的為止繼續(xù)執(zhí)行就可以归敬。
但是,如果線程中執(zhí)行的是一個Native方法员寇,那么程序計數(shù)器是不會去記錄的弄慰,所以此時的程序計數(shù)器為空。
虛擬機(jī)棧Stack
Java虛擬機(jī)棧也是線程私有的蝶锋。一條線程啟動就會為它建立一個虛擬機(jī)棧陆爽。
在線程中每有一個Java方法被調(diào)用就會創(chuàng)建一個 “棧幀” 。每個 “棧幀” 會保存執(zhí)行該方法所需的局部變量表(一般Java程序員喜歡用這個部分來代表棧)扳缕、操作數(shù)棧慌闭、動態(tài)鏈接以及方法出口等信息。
如果一個線程中有過多的 “棧幀” 要入到虛擬機(jī)棧中躯舔,即短時間內(nèi)調(diào)用了過多的方法驴剔,就會造成 -- 棧益處 -- ,即 StackOverflowError 錯誤粥庄。
在這個內(nèi)存區(qū)域中丧失,如果虛擬機(jī)需要擴(kuò)展內(nèi)存,但沒有申請到足夠的內(nèi)存惜互,就會拋出 OutOfMemoryError 錯誤布讹。
本地方法棧
和虛擬機(jī)棧有些類似,但它是為Native方法提供服務(wù)的训堆。
Java堆Heap
Java的堆內(nèi)存是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊描验。它是所有線程所共享的,用于存放對象實例和數(shù)組坑鱼,Java虛擬機(jī)的GC主要就發(fā)生在這個地方膘流。因此這塊區(qū)域也叫做"GC堆"。
Java堆的內(nèi)存可以按照垃圾回收算法【分代回收】分為【新生代區(qū)】和【老年區(qū)】鲁沥,進(jìn)一步的呼股,【新生代區(qū)】可以分為【Eden區(qū)】、【From Survivor區(qū)】和【To Survivor區(qū)】画恰。
從內(nèi)存角度來說卖怜,Java堆內(nèi)存又被劃分為線程共享的內(nèi)存區(qū)域和每個線程私有的內(nèi)存區(qū)域。
在Java堆區(qū)域中阐枣,如果沒有內(nèi)存分配給要創(chuàng)建的實例马靠,并且堆也不能夠再擴(kuò)展,就會拋出OutOfMemoryError
錯誤蔼两。
回收算法
在Java8之后甩鳄,Heap Segment真正意義上的是由Young Generiation和Old Generiation組成的。對象在其中是標(biāo)記復(fù)制算法來判定一個對象是否應(yīng)該被清理掉额划。
Heap Segment中發(fā)生的GC稱為Major GC妙啃,只會影響Heap Segment區(qū)。
Young Generiation中的GC變化 — 復(fù)制算法
這個區(qū)域發(fā)生的GC稱為Minor GC
俊戳。
- 當(dāng)對象被創(chuàng)建后揖赴,首先會被加入eden區(qū)。當(dāng)eden區(qū)滿了之后抑胎,就會觸發(fā)一次GC燥滑,存活下來的對象會被復(fù)制到survivor區(qū)。
- 當(dāng)不為空的Survivor區(qū)滿了阿逃,同樣會觸發(fā)一次GC铭拧。
- 當(dāng)短時間內(nèi)有大量對象創(chuàng)建和釋放同樣會造成內(nèi)存抖動,會觸發(fā)CG恃锉。
- 如圖所示搀菩,survivor有兩個區(qū)域,其中一個總是保持為空破托。
- 現(xiàn)假設(shè)兩個Survivor區(qū)分別為S0肪跋,S1,并且首次GC時土砂,eden區(qū)中存活的對象被復(fù)制到S0中州既。當(dāng)再次發(fā)生GC時,S0和eden中仍然存活的對象就會被復(fù)制到空的S1中瘟芝,此時S0為空易桃;再次發(fā)生GC時,S1和eden中存活的對象將被復(fù)制到S0中锌俱,此時S1為空晤郑;再次發(fā)生GC...就是這樣進(jìn)行的。當(dāng)一個對象被來回復(fù)制轉(zhuǎn)移的次數(shù)達(dá)到閥值(默認(rèn)為15次贸宏,可以通過使用
-XX:MaxTenuringThreshold
該命令來調(diào)整閥值)時造寝,這個對象將被復(fù)制到Old Generiation區(qū)中,此時該對象將會變的相對安全吭练,因為Old Segment區(qū)的GC頻率相對較低诫龙。
Old Segment中的GC變化
這個區(qū)域發(fā)送的GC成為Full GC
。
- 該區(qū)域滿了之后會觸發(fā)一次GC鲫咽,在該次GC中签赃,一些年齡較大的對象會被清理掉谷异。
- 若多次觸發(fā)GC后,該區(qū)域仍然處于滿的狀態(tài)锦聊,則會拋出
OutOfMemoryError
歹嘹。 - 以兩種情況下,新建對象會被直接復(fù)制到該區(qū)域中:
- 當(dāng)新建對象所需要的內(nèi)存大于1/2的單個survivor區(qū)內(nèi)存時孔庭。比如一些很長的對象尺上;
- 當(dāng)新建對象被該區(qū)中的對象引用時,或者引用了該區(qū)域中的對象圆到。
方法區(qū)
Java的方法區(qū)和Java的堆內(nèi)存一樣是被線程所共有的怎抛。它主要存放虛擬機(jī)加載的類信息、常量芽淡、靜態(tài)變量马绝、即時編譯產(chǎn)生的代碼等。
一些地方會將方法區(qū)合并到Java堆中一起去說吐绵。把它作為“永久代”迹淌。這在Hot-Spot虛擬機(jī)而言成立,但是一般來說是不成立的己单。
Java的方法區(qū)如果內(nèi)存不夠分配的話唉窃,也是會拋出OutOfMemoryError
錯誤的。也就是如果加載過多類到方法區(qū)的話纹笼,可能會造成方法區(qū)內(nèi)存益處纹份。
對象的可到達(dá)性
在GC檢查對象的是否可以回收時,是根據(jù)對象是否可到達(dá)引用練頂端的GC Roots
對象來判斷的廷痘。GC Roots
對象一般是虛擬機(jī)棧中變量表中引用的對象蔓涧、類靜態(tài)屬性引用的對象、常量對象笋额、JNI傳到底層的對象元暴。就是說,一個對象如果溯源不到這幾種類型的對象的話兄猩,就認(rèn)為它是無法到達(dá)的茉盏,那么它將會在GC時被回收。
新的引用類型
在JDK 1.2之后枢冤,Java擴(kuò)充了4種引用類型定義:
強(qiáng)應(yīng)用類型
即我們平時通過new關(guān)鍵字創(chuàng)建出來的的對象的引用鸠姨,只要強(qiáng)引用還存在,那么這些對象就一定不回被回收淹真,即使時拋出OutOfMemoryError
讶迁。什么時候強(qiáng)引用會不存在呢?當(dāng)一個方法執(zhí)行完核蘸,棧幀中的變量表將會被清理巍糯,在該方法中創(chuàng)建使用的臨時強(qiáng)引用就會被清理掉啸驯,之后,原本它指向的對象就被變的不可到達(dá)鳞贷。
軟引用類型
用來描述一些有用但不是必須的對象坯汤,即通過SoftReference
創(chuàng)建的對象,它們將會在原本確定要發(fā)生內(nèi)存溢出前的一次GC中被回收搀愧,如果回收完內(nèi)存還是不夠,Java堆就會拋出OutOfMemoryError
錯誤疆偿。就是說咱筛,在觸發(fā)內(nèi)存溢出發(fā)生前,這些對象是和強(qiáng)引用一樣杆故,只要引用還在迅箩,就不會被回收。
弱引用類型
用來描述一些不必須的對象处铛,即通過WeakReference
創(chuàng)建的對象饲趋。弱引用對象的生命周期只有一次GC。
虛引用類型
一個對象的存在與否完全不受虛引用的影響撤蟆,它唯一的用處就是可以用來監(jiān)測一個對象是否被回收奕塑。
方法區(qū)中的-運(yùn)行時常量池
運(yùn)行時常量池主要存放類中編譯時期生成的常量,當(dāng)然也可以動態(tài)的往里面添加家肯。
比如:
"abc".intern();
這個方法首先會檢查運(yùn)行時常量池中是否有這個字符串龄砰,有的話取出來用,沒有的話生成一個并存到常量池中讨衣。
再比如换棚,運(yùn)行過程中生成通過static
修飾的String時,也會加入到常量池中反镇。對于String而言固蚤,常量 + 常量
生成的也是常量,但是常量 + 變量
生成的就是變量了歹茶。
關(guān)于Dalvik虛擬機(jī)
Dalvik虛擬機(jī)是Google按照J(rèn)VM虛擬機(jī)規(guī)范定制的虛擬機(jī)夕玩,它更符合移動設(shè)備的環(huán)境要求。與標(biāo)準(zhǔn)虛擬機(jī)不同:
- Dalvik編譯生成的是
.dex
文件辆亏,這種格式的文件體積更小风秤。而JVM規(guī)范的是.class
文件。 - Dalvik虛擬機(jī)是基于寄存器的扮叨,而JVM規(guī)范是基于棧的缤弦,所以速度方面會有優(yōu)勢。比如上面的說的標(biāo)準(zhǔn)Java虛擬機(jī)中彻磁,它的虛擬機(jī)棧就為線程的運(yùn)行提供了服務(wù)碍沐。而Dalvik虛擬機(jī)中狸捅,使用寄存器去儲存運(yùn)行指令,同時寄存器也提供了程序計數(shù)器累提。寄存器是處理器的一部分哦尘喝!
- Dalvik虛擬機(jī)允許在內(nèi)存中創(chuàng)建都個實例,以隔離不同的應(yīng)用程序斋陪。這樣朽褪,當(dāng)一個應(yīng)用程序在自己的進(jìn)程中崩潰后,不會影響其它進(jìn)程的運(yùn)行无虚。
關(guān)于ART虛擬機(jī)
ART虛擬機(jī)在Android 5.0以后是被默認(rèn)開啟的缔赠,此時Dalvik已經(jīng)被Google放棄維護(hù)了。它與Dalvik虛擬機(jī)的不同:
- ART虛擬機(jī)在應(yīng)用程序安裝時就會把字節(jié)碼通過dex2oat工具直接轉(zhuǎn)成機(jī)器碼儲存友题,這個過程叫做AOT(Ahead-Of-Time)嗤堰。而Dalvik是在每次啟動應(yīng)用程序時,通過傳統(tǒng)的JIT(JUST IN TIME)模式將字節(jié)碼轉(zhuǎn)成機(jī)器碼度宦。顯然踢匣,這樣速度會慢不少。當(dāng)然戈抄,ART虛擬機(jī)的占用內(nèi)存也會更大些离唬。
- ART虛擬機(jī)在進(jìn)行GC時采用了并法的模式。
- 在傳統(tǒng)的GC模式下呛凶,當(dāng)虛擬機(jī)觸發(fā)一次GC男娄,會先暫停所有線程,然后檢查所有對象漾稀,將符合回收條件的對象進(jìn)行標(biāo)記模闲,然后進(jìn)行回收,最后再恢復(fù)線程崭捍,這樣的話gc速度會快些尸折,但是遇到內(nèi)存抖動,就會卡頓了殷蛇。同時实夹,傳統(tǒng)的GC算法導(dǎo)致了【內(nèi)存碎片化】嚴(yán)重,在一次回收后粒梦,很多內(nèi)存塊都會出現(xiàn)不連續(xù)的情況亮航,這樣會導(dǎo)致尋址變得困難,從而拖慢程序運(yùn)行速度匀们。
- 而ART虛擬的垃圾回收算法允許GC時對對象的標(biāo)記和一些對象的清理工作并發(fā)進(jìn)行缴淋。同時,ART引入了【移動垃圾回收器】技術(shù),使得碎片化內(nèi)存能夠被對齊重抖,從而能稍微節(jié)約一些內(nèi)存空間露氮。
總結(jié)
- Heap Segment被劃分為兩塊:Young Generiation和Old Generiation。
- Young Genertiation中又被劃分為Eden區(qū)和兩個Survivor區(qū)钟沛,對象在其中采用標(biāo)記復(fù)制算法來判定一個對象是應(yīng)該清理還是移到Old Generiation中畔规。該內(nèi)存區(qū)域發(fā)生GC的頻率較高。
- Old Generiation發(fā)生GC的頻率相對較低恨统。當(dāng)有大對象被創(chuàng)建叁扫,或者和該區(qū)域有關(guān)的對象被創(chuàng)建時,它將會被直接移動到該區(qū)域中畜埋。
看到這里的童鞋快獎勵自己一口辣條吧陌兑!
想要看CoorChice更多的文章,可以加個關(guān)注哦由捎!