JVM筆記
JDK:Java、JVM、Java API類(lèi)庫(kù),是支持java程序開(kāi)發(fā)的最小環(huán)境。
JRE:Java API類(lèi)庫(kù)中Java SE API子集和JVM,是支持java程序運(yùn)行的標(biāo)準(zhǔn)環(huán)境。
Fork/Join模式:處理多核并行編程的一種經(jīng)典方法,能夠輕松利用多個(gè)CPU核心提供的計(jì)算資源來(lái)協(xié)作完成一個(gè)復(fù)雜的計(jì)算任務(wù)。
JVM內(nèi)存管理
JVM內(nèi)存管理即是管理運(yùn)行時(shí)數(shù)據(jù)區(qū)
- 運(yùn)行時(shí)數(shù)據(jù)區(qū)分類(lèi):
- 所有線程共享:
- 方法區(qū)
- 堆
- 線程隔離:
- 虛擬機(jī)棧
- 本地方法棧
- 程序計(jì)數(shù)器
- 所有線程共享:
線程共享
堆
堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊,是被所有線程共享的一塊內(nèi)存區(qū)域。在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,唯一目的是用于存放對(duì)象實(shí)例。
- 所有的對(duì)象實(shí)例以及數(shù)組都分配在堆上。
- 堆是GC管理的主要區(qū)域,因此從垃圾回收的角度考慮,目前多采用分代收集算法,Java堆還可以細(xì)分為新生代和老生代。
- 在JVM規(guī)范中,堆可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上連續(xù)即可。在JVM配置中使用Xmx和Xms配置。
- Xmx:設(shè)置JVM最大堆內(nèi)存空間。
- Xms:設(shè)置JVM初始堆內(nèi)存空間。(可與Xmx相同,這樣堆不可擴(kuò)展,避免GC回收后進(jìn)行調(diào)整)
- 堆無(wú)法繼續(xù)擴(kuò)展時(shí),會(huì)拋出OutOfMemmoryError異常。
方法區(qū)
方法區(qū)是各個(gè)線程共享的內(nèi)存區(qū)域,它們用于存儲(chǔ)已被虛擬機(jī)加載的Class的相關(guān)信息:類(lèi)信息(類(lèi)名、訪問(wèn)修飾符、常量池等)、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
- GC在方法區(qū)的回收目標(biāo)主要是針對(duì)常量池的回收和針對(duì)類(lèi)型的卸載。
- 當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求時(shí),將拋出OutOfMemmoryErrori 異常。
- 在JVM中使用MaxPermSize設(shè)置方法區(qū)大小。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分,常量池主要存放Class文件中編譯期生成的各種字面量和符號(hào)引用。這部分內(nèi)容在類(lèi)加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
- 運(yùn)行時(shí)常量池具有動(dòng)態(tài)性,Java語(yǔ)言不要求常量一定只能在編譯期產(chǎn)生,運(yùn)行期間也可能將新的常量放入池中,例如String的intern()方法。
- 常量池?zé)o法申請(qǐng)到內(nèi)存時(shí)拋出OutOfMemmoryError異常。
線程隔離
程序計(jì)數(shù)器
是一塊較小的內(nèi)存空間。作用是用于標(biāo)識(shí)當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。字節(jié)碼解釋器工作時(shí)就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)獲取下一條需要執(zhí)行的字節(jié)碼指令。分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)都是依賴這個(gè)計(jì)數(shù)器完成。
- JVM中多線程通過(guò)時(shí)間片輪轉(zhuǎn)的方式輪流切換線程。因此在任意確定時(shí)刻,一個(gè)處理器只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器。各條線程之間的計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ)。
- 如果線程正在執(zhí)行一個(gè)Java方法,則這個(gè)計(jì)數(shù)器的值是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果是一個(gè)Native方法,則這個(gè)計(jì)數(shù)器值為空。
- 程序計(jì)數(shù)器是JVM中唯一一個(gè)沒(méi)有規(guī)定任何OutOfMemmoryError情況的區(qū)域。
Java虛擬機(jī)棧
Java虛擬機(jī)棧也是線程私有的,它的生命周期與線程相同。描述了Java方法執(zhí)行的內(nèi)存模型:每一個(gè)方法執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀,棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法返回地址等信息。每一個(gè)方法從調(diào)用到執(zhí)行完成對(duì)應(yīng)著虛擬機(jī)棧中棧幀入棧出棧的過(guò)程。

- 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類(lèi)型、對(duì)象引用和返回地址類(lèi)型。其中64位的long和double會(huì)占用2個(gè)局部變量空間,其余的數(shù)據(jù)類(lèi)型占用1個(gè)空間。局部變量表所需的內(nèi)存空間大小在編譯期完成確定和分配,在方法運(yùn)行期間,不會(huì)改變局部變量表的大小。
- 方法存儲(chǔ)在運(yùn)行時(shí)常量池中,每一個(gè)棧幀都有一個(gè)動(dòng)態(tài)鏈接指向該棧幀所屬方法在運(yùn)行時(shí)常量池中的地址。
- 在JVM中,虛擬機(jī)棧有兩個(gè)異常:
- 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常。
- 如果虛擬機(jī)棧在動(dòng)態(tài)擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)從會(huì)拋出OutOfMemmoryError異常。
- 在JVM中使用Xss配置虛擬機(jī)棧大小。
本地方法棧
本地方法棧存儲(chǔ)Native方法的內(nèi)存模型,基本與Java虛擬機(jī)棧類(lèi)似,也會(huì)拋出StackOverflowError異常和OutOfMemmoryError異常。
總結(jié)
- 除了程序計(jì)數(shù)器以外,堆、方法區(qū)、棧、本地方法區(qū)都會(huì)產(chǎn)生OutOfMemmory異常(OOM)。
- 堆的OOM:堆用于存儲(chǔ)對(duì)象實(shí)例,產(chǎn)生OOM,只需要不斷創(chuàng)建對(duì)象,并保證GC Root到對(duì)象之間具有可達(dá)路徑。
內(nèi)存中對(duì)象訪問(wèn)
最簡(jiǎn)單的對(duì)象訪問(wèn),也涉及棧、堆、方法區(qū)這三個(gè)最重要內(nèi)存區(qū)域之間的關(guān)聯(lián)。
Object obj =new Object();
分析:
- Object obj這部分的語(yǔ)義會(huì)反映到Java棧的本地變量表中,作為一個(gè)reference類(lèi)型數(shù)據(jù)出現(xiàn)。
- new Object()這部分語(yǔ)義會(huì)反映在Java堆中,形成一塊存儲(chǔ)了Object類(lèi)型所有實(shí)例數(shù)據(jù)值(對(duì)象中各個(gè)實(shí)例字段的數(shù)據(jù))的結(jié)構(gòu)化內(nèi)存。此外在Java堆中還包含能查找到此對(duì)象類(lèi)型數(shù)據(jù)(對(duì)象類(lèi)型、父類(lèi)、實(shí)現(xiàn)的接口)的地址信息,這些都存儲(chǔ)在方法區(qū)中。
reference類(lèi)型:一個(gè)指向?qū)ο蟮囊?,并沒(méi)有定義這個(gè)引用應(yīng)該通過(guò)哪種方式去定位,以及訪問(wèn)Java堆中的對(duì)象的具體位置。目前主流的訪問(wèn)方式有兩種:句柄和直接指針。
- 句柄:Java堆中會(huì)劃分一塊內(nèi)存作為句柄池,reference中存儲(chǔ)的就是對(duì)象在句柄池中的句柄地址,而該句柄地址對(duì)應(yīng)的句柄中包含了對(duì)象實(shí)例數(shù)據(jù)和類(lèi)型數(shù)據(jù)各自的具體地址信息。
- 直接指針:reference中直接存儲(chǔ)的就是對(duì)象在Java堆中的地址。但采用這種方式,必須要考慮如何放置訪問(wèn)類(lèi)型數(shù)據(jù)的相關(guān)信息,因此在對(duì)象的結(jié)構(gòu)化內(nèi)存中,還需要包含一個(gè)指向該實(shí)例對(duì)象類(lèi)型數(shù)據(jù)的指針引用。
- 兩種方式比較:句柄訪問(wèn)方式的最大好處是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象移動(dòng)(GC回收)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要改變;使用直接訪問(wèn)的最大好處是速度快,節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo)。
GC和內(nèi)存分配策略
程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧三個(gè)區(qū)與線程的生命周期相同。這幾個(gè)區(qū)域的內(nèi)存分配都是編譯期可知的, 因此具有確定性,故不需要在這幾個(gè)區(qū)域考慮回收的問(wèn)題。因?yàn)樵诜椒ńY(jié)束或者線程結(jié)束時(shí),內(nèi)存自然也就跟著回收了。
Java堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類(lèi)需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,只有在程序運(yùn)行期間才能知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的。因此這兩個(gè)區(qū)域是GC的主要回收區(qū)域。
GC需要完成的三件事情:
- 哪些內(nèi)存需要回收
- 什么時(shí)候回收
- 如何回收
確定回收對(duì)象
- 引用計(jì)數(shù)算法
- 根搜索算法
引用計(jì)數(shù)算法
描述:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),該對(duì)象的引用計(jì)數(shù)器就加1;當(dāng)引用失效時(shí),計(jì)數(shù)器值減1.任何時(shí)候,計(jì)數(shù)器都為0的對(duì)象表明不可能再被使用。
特點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,判定高效,但很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。
public class Father{
public Object instance = null;
public static void testGC(){
Father objA=new Father();
Father objB=new Father();
objA.instance=objB;
objB.instance=objA;
objA=null;
objB=null;
System.gc();
}
}
在該例子中,objA的引用指向一個(gè)Fahter實(shí)例A,objB的引用指向一個(gè)Father實(shí)例B(這兩個(gè)實(shí)例完全不一樣)。之后,令objA的實(shí)例A中的instance引用指向objB,objB實(shí)例B中的instance引用指向objA。這樣,objA和objB對(duì)象實(shí)例的引用計(jì)數(shù)器的值分別為1.此時(shí),令objA和objB這兩個(gè)引用變量指向null,表示原先的實(shí)例A和實(shí)例B都沒(méi)有被任何引用變量指向,實(shí)例A和B已經(jīng)是可回收,但由于實(shí)例A和B互相循環(huán)引用,引用計(jì)數(shù)器都為1,因此這種情況下,GC無(wú)法對(duì)實(shí)例A、B進(jìn)行回收。
根搜索算法
描述:通過(guò)一系列的GC Roots的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的,可以被GC回收。
GC Roots對(duì)象包括以下幾種:
- 虛擬機(jī)棧中的引用的對(duì)象。
- 方法區(qū)中的類(lèi)靜態(tài)屬性引用的對(duì)象。
- 方法區(qū)中的常量引用的對(duì)象。
- 本地方法棧中的引用的對(duì)象。
引用
判定對(duì)象是否存活都與“引用”相關(guān)。
引用原始定義:如果reference類(lèi)型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)從的起始地址,就稱這塊內(nèi)存代表著一個(gè)引用。
引用的擴(kuò)充定義:強(qiáng)引用、軟引用、弱引用、虛引用。
- 強(qiáng)引用:在程序中普遍存在的,類(lèi)似于Object obj=new Object()這類(lèi)的引用。
- 軟引用:關(guān)聯(lián)非必需的對(duì)象。對(duì)于軟引用對(duì)象,系統(tǒng)會(huì)在內(nèi)存溢出異常之前,會(huì)將此類(lèi)引用對(duì)象列入回收范圍進(jìn)行回收,如果回收后還是內(nèi)存不足,才會(huì)拋出內(nèi)存溢出異常。
- 弱引用:關(guān)聯(lián)非必需對(duì)象,強(qiáng)度比軟引用更弱。這類(lèi)引用只能生存到下一次垃圾回收之前。在下一次GC工作時(shí),無(wú)論內(nèi)存是否足夠,都會(huì)回收掉弱引用對(duì)象。
- 虛引用:設(shè)置虛引用僅僅是希望在這個(gè)對(duì)象被回收時(shí)收到一個(gè)系統(tǒng)通知。
回收標(biāo)記過(guò)程
在GC回收過(guò)程中,并不一定會(huì)回收對(duì)象。在這個(gè)過(guò)程中,會(huì)有兩次標(biāo)記,對(duì)象可以有機(jī)會(huì)離開(kāi)回收隊(duì)列。
在根搜索算法中,回收一個(gè)對(duì)象,至少需要兩次標(biāo)記過(guò)程。(在一次根搜索之后,對(duì)于回收對(duì)象)
- 回收對(duì)象第一次標(biāo)記,并進(jìn)行篩選。篩選條件是此對(duì)象是否有必要執(zhí)行finalize()方法。對(duì)于以下兩種方式,JVM認(rèn)為沒(méi)有必要執(zhí)行finalize()方法。
- 對(duì)象沒(méi)有覆蓋finalize()方法。
- finalize()方法已經(jīng)被執(zhí)行過(guò)。
- 若標(biāo)記為有必要執(zhí)行finalize()方法。
- 對(duì)象放置在F-Queue隊(duì)列中,并在稍后由JVM自動(dòng)建立、低優(yōu)先級(jí)的Finalizer線程區(qū)執(zhí)行。(JVM會(huì)觸發(fā)這個(gè)方法,但不保證會(huì)等待它運(yùn)行結(jié)束)
- 稍后,在執(zhí)行finalize()方法期間,GC對(duì)F-Queue隊(duì)列中的對(duì)象進(jìn)行第二次標(biāo)記。如果對(duì)象在finalize()方法中成功拯救自己—--只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可。此時(shí)在第二次標(biāo)記中,它會(huì)被移出F-Queue隊(duì)列,不會(huì)回收。
- 之后,還在F-Queue中的對(duì)象將被回收。
任何一個(gè)對(duì)象的finalize()方法都會(huì)被系統(tǒng)自動(dòng)調(diào)用一次。
執(zhí)行回收算法
- 標(biāo)記-清除
- 標(biāo)記-復(fù)制
- 標(biāo)記-整理
- 分代收集
標(biāo)記-清除
算法分為標(biāo)記和清除兩個(gè)階段。
- 首先標(biāo)記出所有需要回收的對(duì)象。
- 標(biāo)記完成后同意回收掉所有被標(biāo)記的對(duì)象。
主要的缺點(diǎn):
- 效率問(wèn)題,標(biāo)記和清除過(guò)程的效率不高。
-
空間問(wèn)題,標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能導(dǎo)致,當(dāng)程序在以后的運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí)無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾回收動(dòng)作。
標(biāo)記-清除
標(biāo)記-復(fù)制(新生代)
為了解決效率問(wèn)題,改進(jìn)標(biāo)記-清除算法為標(biāo)記-復(fù)制算法。
- 將可用內(nèi)存按容量大小劃分成相等的兩塊,每次只使用其中的一塊。
- 當(dāng)一塊內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另一塊上面,然后清除已使用過(guò)的這塊。
- 這樣使得每次都是對(duì)其中的一塊進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí),也不用考慮內(nèi)存碎片的問(wèn)題。只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可。
主要的缺點(diǎn):
將原先可用內(nèi)存容量縮小為原來(lái)的一半,每次都是使用一半作為當(dāng)前可用的內(nèi)存進(jìn)行分配??臻g代價(jià)較高。

在分代回收中,多采用該算法回收新生代。
IBM研究表明:新生代中98%的對(duì)象都是朝生夕死的,所以不需要按照1:1比例劃分新生代內(nèi)存空間。而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間。每次使用Eden和其中一個(gè)Survivor空間。當(dāng)回收時(shí):
- 將Eden和Survivor中還存活的對(duì)象一次性的拷貝到另外一個(gè)Survivor空間中。
- 最后清理掉Eden和剛才用過(guò)的Survivor空間。
HotSpot虛擬機(jī)默認(rèn)Eden和Survivor比例是8:1,也就是每次新生代可用內(nèi)存空間是90%(80%+10%),只有10%的會(huì)被浪費(fèi)掉。
但也存在Survivor不夠用的情況,這樣的時(shí)候需要老年代進(jìn)行內(nèi)存擔(dān)保。
標(biāo)記-整理(老年代)
復(fù)制算法在對(duì)象存活率較高時(shí),需要執(zhí)行較多的復(fù)制操作,效率會(huì)變低。更需要考慮存活過(guò)多,Survivor空間不夠,需要額外空間擔(dān)保的問(wèn)題。對(duì)于內(nèi)存中存活100%的對(duì)象,復(fù)制算法并不合適。因此在對(duì)象存活率較高的老年代,一般選擇標(biāo)記-整理算法。
- 與標(biāo)記-清除算法中的標(biāo)記過(guò)程一樣,先進(jìn)行標(biāo)記。
-
讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。
標(biāo)記-整理
分代收集
這種算法沒(méi)有什么新的思想,只是根據(jù)對(duì)象的存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆劃分為新生代和老年代,這樣就可以根據(jù)每一個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>
- 在新生代,每次垃圾收集都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用標(biāo)記-復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。
- 在老年代,因?yàn)閷?duì)象存活率較高,沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用標(biāo)記-清除或者標(biāo)記-整理算法進(jìn)行回收。
內(nèi)存分配與回收策略
Java體系中的自動(dòng)內(nèi)存管理解決兩個(gè)問(wèn)題:
- 給對(duì)象分配內(nèi)存(本節(jié)內(nèi)容)
- 回收分配給對(duì)象的內(nèi)存(上一節(jié)以詳解)
對(duì)象內(nèi)存分配:堆上分配,對(duì)象主要分配在新生代的Eden區(qū)
優(yōu)先Eden分配
大多數(shù)情況下,對(duì)象在新生代Eden區(qū)中分配,當(dāng)Eden區(qū)沒(méi)有足夠的空間進(jìn)行分配時(shí),JVM觸發(fā)一次Minor GC。
- Minor GC和Full GC區(qū)別:
- 新生代GC(Minor GC):發(fā)生在新生代上的垃圾收集動(dòng)作。因?yàn)榇蠖鄶?shù)對(duì)象朝生夕滅,所以Minor GC非常頻繁,回收速度也很快。
- 老年代GC(Full/Major GC):發(fā)生在老年代的GC,出現(xiàn)了Full/Major GC。經(jīng)常會(huì)伴隨一次Minor GC,但非絕對(duì)的。Full GC的速度一般比Minor GC慢10倍以上。
大對(duì)象直接進(jìn)入老年代
所謂大對(duì)象:需要大量連續(xù)內(nèi)存空間的Java對(duì)象。比如:很長(zhǎng)的字符串及數(shù)組。經(jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間就會(huì)提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間分配下一個(gè)大對(duì)象。
長(zhǎng)期存活的對(duì)象進(jìn)入老年代
分代回收:JVM需要能夠識(shí)別哪些對(duì)象應(yīng)當(dāng)放在新生代,哪些對(duì)象可以放在老年代。
JVM給每個(gè)對(duì)象定義一個(gè)對(duì)象年齡計(jì)數(shù)器(Age)。
- 如果一個(gè)對(duì)象在Eden出生,并經(jīng)過(guò)第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動(dòng)到Survivor區(qū),并將Age=1;
- 對(duì)象在Survivor區(qū)每熬過(guò)一次Minor GC,Age+1,當(dāng)年齡增加到一定程度(默認(rèn)是15歲),就會(huì)被晉升到老年代中。
- 對(duì)象晉升老年代的Age閾值設(shè)置:-XX:MaxTenuringThreshold設(shè)置。
動(dòng)態(tài)對(duì)象年齡判定
為了適應(yīng)不同程序的內(nèi)存狀況,JVM不能總是要求年齡必須到達(dá)閾值才能晉升老年代。如果Survivor空間中相同年齡所有對(duì)象大小的綜合大于Survivor空間大小的一半,則年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無(wú)須等到閾值要求的年齡。
及時(shí)釋放Survivor空間,保證標(biāo)記-復(fù)制回收效率?
空間分配擔(dān)保
- 發(fā)生Minor GC時(shí),JVM會(huì)檢測(cè)之前每次晉升到老年代的平均大小是否大于老年代的剩余空間大?。?
- 如果大于,則改為直接進(jìn)行一次Full GC。
- 如果小于,則查看HandlePromotionFailure設(shè)置是否允許擔(dān)保失?。?
- 如果允許,只會(huì)進(jìn)行Minor GC。
- 如果不允許,則改為一次Full GC。

