背景介紹
- Java優(yōu)勢(shì)之一就是其具有垃圾回收機(jī)制。在大部分情況下,JVM的GC(垃圾回收器)能夠幫助我們回那些不可到達(dá)的對(duì)象(就是未被引用的對(duì)象)。
- 當(dāng)然,在一些情況下,我們?nèi)匀恍枰约喝メ尫艃?nèi)存(就是把對(duì)象引用置null,把容器、數(shù)組清空),否則就會(huì)引起內(nèi)存泄漏,內(nèi)存泄漏嚴(yán)重時(shí)將容易引發(fā)
OutOfMemoryError,詳情見內(nèi)存泄漏。 - 此外,由于GC會(huì)停止所有的線程,包括UI線程,所以頻繁的GC必然會(huì)導(dǎo)致畫面卡頓(Android中每16ms為一幀),因此還應(yīng)避免GC的頻繁發(fā)生。一個(gè)導(dǎo)致GC頻繁發(fā)生的原因就是內(nèi)存抖動(dòng),點(diǎn)擊鏈接看詳情。
GC時(shí)線程被停止示意圖 - 所以,理解Java的內(nèi)存機(jī)制,有助于幫助我們?cè)趯懘a的過程中避免內(nèi)存泄漏。
走進(jìn)內(nèi)存模型
Java內(nèi)存層級(jí)

;

;
程序計(jì)數(shù)器
程序計(jì)數(shù)器是線程私有的內(nèi)存區(qū)域,這個(gè)區(qū)域是Java虛擬機(jī)中唯一一個(gè)沒有限制OutOfMemoryError的內(nèi)存區(qū)域。之所以需要它是因?yàn)镴ava的多線程機(jī)制是通過輪流切換分配處理器執(zhí)行時(shí)間來實(shí)現(xiàn)的,所以會(huì)涉及到線程的暫停和重啟,而在一個(gè)線程中如果正在執(zhí)行Java方法的話,這個(gè)計(jì)數(shù)器就回去記錄當(dāng)前正在執(zhí)行的虛擬機(jī)字節(jié)碼,一旦被暫停,恢復(fù)只需要從程序計(jì)數(shù)器記錄的為止繼續(xù)執(zhí)行就可以。
但是,如果線程中執(zhí)行的是一個(gè)Native方法,那么程序計(jì)數(shù)器是不會(huì)去記錄的,所以此時(shí)的程序計(jì)數(shù)器為空。
虛擬機(jī)棧Stack
Java虛擬機(jī)棧也是線程私有的。一條線程啟動(dòng)就會(huì)為它建立一個(gè)虛擬機(jī)棧。
在線程中每有一個(gè)Java方法被調(diào)用就會(huì)創(chuàng)建一個(gè) “棧幀” 。每個(gè) “棧幀” 會(huì)保存執(zhí)行該方法所需的局部變量表(一般Java程序員喜歡用這個(gè)部分來代表?xiàng)#?、操作?shù)棧、動(dòng)態(tài)鏈接以及方法出口等信息。
如果一個(gè)線程中有過多的 “棧幀” 要入到虛擬機(jī)棧中,即短時(shí)間內(nèi)調(diào)用了過多的方法,就會(huì)造成 -- 棧益處 -- ,即 StackOverflowError 錯(cuò)誤。
在這個(gè)內(nèi)存區(qū)域中,如果虛擬機(jī)需要擴(kuò)展內(nèi)存,但沒有申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出 OutOfMemoryError 錯(cuò)誤。
本地方法棧
和虛擬機(jī)棧有些類似,但它是為Native方法提供服務(wù)的。
Java堆Heap
Java的堆內(nèi)存是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊。它是所有線程所共享的,用于存放對(duì)象實(shí)例和數(shù)組,Java虛擬機(jī)的GC主要就發(fā)生在這個(gè)地方。因此這塊區(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ū)域和每個(gè)線程私有的內(nèi)存區(qū)域。
在Java堆區(qū)域中,如果沒有內(nèi)存分配給要?jiǎng)?chuàng)建的實(shí)例,并且堆也不能夠再擴(kuò)展,就會(huì)拋出OutOfMemoryError錯(cuò)誤。
回收算法
在Java8之后,Heap Segment真正意義上的是由Young Generiation和Old Generiation組成的。對(duì)象在其中是標(biāo)記復(fù)制算法來判定一個(gè)對(duì)象是否應(yīng)該被清理掉。
Heap Segment中發(fā)生的GC稱為Major GC,只會(huì)影響Heap Segment區(qū)。

Young Generiation中的GC變化 — 復(fù)制算法
這個(gè)區(qū)域發(fā)生的GC稱為Minor GC。
- 當(dāng)對(duì)象被創(chuàng)建后,首先會(huì)被加入eden區(qū)。當(dāng)eden區(qū)滿了之后,就會(huì)觸發(fā)一次GC,存活下來的對(duì)象會(huì)被復(fù)制到survivor區(qū)。
- 當(dāng)不為空的Survivor區(qū)滿了,同樣會(huì)觸發(fā)一次GC。
- 當(dāng)短時(shí)間內(nèi)有大量對(duì)象創(chuàng)建和釋放同樣會(huì)造成內(nèi)存抖動(dòng),會(huì)觸發(fā)CG。
- 如圖所示,survivor有兩個(gè)區(qū)域,其中一個(gè)總是保持為空。
- 現(xiàn)假設(shè)兩個(gè)Survivor區(qū)分別為S0,S1,并且首次GC時(shí),eden區(qū)中存活的對(duì)象被復(fù)制到S0中。當(dāng)再次發(fā)生GC時(shí),S0和eden中仍然存活的對(duì)象就會(huì)被復(fù)制到空的S1中,此時(shí)S0為空;再次發(fā)生GC時(shí),S1和eden中存活的對(duì)象將被復(fù)制到S0中,此時(shí)S1為空;再次發(fā)生GC...就是這樣進(jìn)行的。當(dāng)一個(gè)對(duì)象被來回復(fù)制轉(zhuǎn)移的次數(shù)達(dá)到閥值(默認(rèn)為15次,可以通過使用
-XX:MaxTenuringThreshold該命令來調(diào)整閥值)時(shí),這個(gè)對(duì)象將被復(fù)制到Old Generiation區(qū)中,此時(shí)該對(duì)象將會(huì)變的相對(duì)安全,因?yàn)?em>Old Segment區(qū)的GC頻率相對(duì)較低。
Old Segment中的GC變化
這個(gè)區(qū)域發(fā)送的GC成為Full GC。
- 該區(qū)域滿了之后會(huì)觸發(fā)一次GC,在該次GC中,一些年齡較大的對(duì)象會(huì)被清理掉。
- 若多次觸發(fā)GC后,該區(qū)域仍然處于滿的狀態(tài),則會(huì)拋出
OutOfMemoryError。 - 以兩種情況下,新建對(duì)象會(huì)被直接復(fù)制到該區(qū)域中:
- 當(dāng)新建對(duì)象所需要的內(nèi)存大于1/2的單個(gè)survivor區(qū)內(nèi)存時(shí)。比如一些很長(zhǎng)的對(duì)象;
- 當(dāng)新建對(duì)象被該區(qū)中的對(duì)象引用時(shí),或者引用了該區(qū)域中的對(duì)象。
方法區(qū)
Java的方法區(qū)和Java的堆內(nèi)存一樣是被線程所共有的。它主要存放虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯產(chǎn)生的代碼等。
一些地方會(huì)將方法區(qū)合并到Java堆中一起去說。把它作為“永久代”。這在Hot-Spot虛擬機(jī)而言成立,但是一般來說是不成立的。
Java的方法區(qū)如果內(nèi)存不夠分配的話,也是會(huì)拋出OutOfMemoryError錯(cuò)誤的。也就是如果加載過多類到方法區(qū)的話,可能會(huì)造成方法區(qū)內(nèi)存益處。
對(duì)象的可到達(dá)性
在GC檢查對(duì)象的是否可以回收時(shí),是根據(jù)對(duì)象是否可到達(dá)引用練頂端的GC Roots對(duì)象來判斷的。GC Roots對(duì)象一般是虛擬機(jī)棧中變量表中引用的對(duì)象、類靜態(tài)屬性引用的對(duì)象、常量對(duì)象、JNI傳到底層的對(duì)象。就是說,一個(gè)對(duì)象如果溯源不到這幾種類型的對(duì)象的話,就認(rèn)為它是無法到達(dá)的,那么它將會(huì)在GC時(shí)被回收。
新的引用類型
在JDK 1.2之后,Java擴(kuò)充了4種引用類型定義:
強(qiáng)應(yīng)用類型
即我們平時(shí)通過new關(guān)鍵字創(chuàng)建出來的的對(duì)象的引用,只要強(qiáng)引用還存在,那么這些對(duì)象就一定不回被回收,即使時(shí)拋出OutOfMemoryError。什么時(shí)候強(qiáng)引用會(huì)不存在呢?當(dāng)一個(gè)方法執(zhí)行完,棧幀中的變量表將會(huì)被清理,在該方法中創(chuàng)建使用的臨時(shí)強(qiáng)引用就會(huì)被清理掉,之后,原本它指向的對(duì)象就被變的不可到達(dá)。
軟引用類型
用來描述一些有用但不是必須的對(duì)象,即通過SoftReference創(chuàng)建的對(duì)象,它們將會(huì)在原本確定要發(fā)生內(nèi)存溢出前的一次GC中被回收,如果回收完內(nèi)存還是不夠,Java堆就會(huì)拋出OutOfMemoryError錯(cuò)誤。就是說,在觸發(fā)內(nèi)存溢出發(fā)生前,這些對(duì)象是和強(qiáng)引用一樣,只要引用還在,就不會(huì)被回收。
弱引用類型
用來描述一些不必須的對(duì)象,即通過WeakReference創(chuàng)建的對(duì)象。弱引用對(duì)象的生命周期只有一次GC。
虛引用類型
一個(gè)對(duì)象的存在與否完全不受虛引用的影響,它唯一的用處就是可以用來監(jiān)測(cè)一個(gè)對(duì)象是否被回收。
方法區(qū)中的-運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池主要存放類中編譯時(shí)期生成的常量,當(dāng)然也可以動(dòng)態(tài)的往里面添加。
比如:
"abc".intern();
這個(gè)方法首先會(huì)檢查運(yùn)行時(shí)常量池中是否有這個(gè)字符串,有的話取出來用,沒有的話生成一個(gè)并存到常量池中。
再比如,運(yùn)行過程中生成通過static修飾的String時(shí),也會(huì)加入到常量池中。對(duì)于String而言,常量 + 常量 生成的也是常量,但是常量 + 變量 生成的就是變量了。
關(guān)于Dalvik虛擬機(jī)
Dalvik虛擬機(jī)是Google按照J(rèn)VM虛擬機(jī)規(guī)范定制的虛擬機(jī),它更符合移動(dòng)設(shè)備的環(huán)境要求。與標(biāo)準(zhǔn)虛擬機(jī)不同:
- Dalvik編譯生成的是
.dex文件,這種格式的文件體積更小。而JVM規(guī)范的是.class文件。 - Dalvik虛擬機(jī)是基于寄存器的,而JVM規(guī)范是基于棧的,所以速度方面會(huì)有優(yōu)勢(shì)。比如上面的說的標(biāo)準(zhǔn)Java虛擬機(jī)中,它的虛擬機(jī)棧就為線程的運(yùn)行提供了服務(wù)。而Dalvik虛擬機(jī)中,使用寄存器去儲(chǔ)存運(yùn)行指令,同時(shí)寄存器也提供了程序計(jì)數(shù)器。寄存器是處理器的一部分哦!
- Dalvik虛擬機(jī)允許在內(nèi)存中創(chuàng)建都個(gè)實(shí)例,以隔離不同的應(yīng)用程序。這樣,當(dāng)一個(gè)應(yīng)用程序在自己的進(jìn)程中崩潰后,不會(huì)影響其它進(jìn)程的運(yùn)行。
關(guān)于ART虛擬機(jī)
ART虛擬機(jī)在Android 5.0以后是被默認(rèn)開啟的,此時(shí)Dalvik已經(jīng)被Google放棄維護(hù)了。它與Dalvik虛擬機(jī)的不同:
- ART虛擬機(jī)在應(yīng)用程序安裝時(shí)就會(huì)把字節(jié)碼通過dex2oat工具直接轉(zhuǎn)成機(jī)器碼儲(chǔ)存,這個(gè)過程叫做AOT(Ahead-Of-Time)。而Dalvik是在每次啟動(dòng)應(yīng)用程序時(shí),通過傳統(tǒng)的JIT(JUST IN TIME)模式將字節(jié)碼轉(zhuǎn)成機(jī)器碼。顯然,這樣速度會(huì)慢不少。當(dāng)然,ART虛擬機(jī)的占用內(nèi)存也會(huì)更大些。
- ART虛擬機(jī)在進(jìn)行GC時(shí)采用了并法的模式。
- 在傳統(tǒng)的GC模式下,當(dāng)虛擬機(jī)觸發(fā)一次GC,會(huì)先暫停所有線程,然后檢查所有對(duì)象,將符合回收條件的對(duì)象進(jìn)行標(biāo)記,然后進(jìn)行回收,最后再恢復(fù)線程,這樣的話gc速度會(huì)快些,但是遇到內(nèi)存抖動(dòng),就會(huì)卡頓了。同時(shí),傳統(tǒng)的GC算法導(dǎo)致了【內(nèi)存碎片化】嚴(yán)重,在一次回收后,很多內(nèi)存塊都會(huì)出現(xiàn)不連續(xù)的情況,這樣會(huì)導(dǎo)致尋址變得困難,從而拖慢程序運(yùn)行速度。
- 而ART虛擬的垃圾回收算法允許GC時(shí)對(duì)對(duì)象的標(biāo)記和一些對(duì)象的清理工作并發(fā)進(jìn)行。同時(shí),ART引入了【移動(dòng)垃圾回收器】技術(shù),使得碎片化內(nèi)存能夠被對(duì)齊,從而能稍微節(jié)約一些內(nèi)存空間。
總結(jié)
- Heap Segment被劃分為兩塊:Young Generiation和Old Generiation。
- Young Genertiation中又被劃分為Eden區(qū)和兩個(gè)Survivor區(qū),對(duì)象在其中采用標(biāo)記復(fù)制算法來判定一個(gè)對(duì)象是應(yīng)該清理還是移到Old Generiation中。該內(nèi)存區(qū)域發(fā)生GC的頻率較高。
- Old Generiation發(fā)生GC的頻率相對(duì)較低。當(dāng)有大對(duì)象被創(chuàng)建,或者和該區(qū)域有關(guān)的對(duì)象被創(chuàng)建時(shí),它將會(huì)被直接移動(dòng)到該區(qū)域中。
看到這里的童鞋快獎(jiǎng)勵(lì)自己一口辣條吧!
想要看CoorChice更多的文章,可以加個(gè)關(guān)注哦!
