個人部落格網:https://wushaopei.github.io/ (你想要這裡多有)
一、JVM體系結構概述
1、JVM 與系統、硬體

JVM是運作在作業系統之上的,它與硬體沒有直接的互動
2、JVM 體系結構概覽
3、類裝載器ClassLoader 執行原理
負責加載class檔案,class檔案在檔案開頭有特定的檔案标示,并且ClassLoader隻負責class檔案的加載,至于它是否可以運作,則由Execution Engine決定
4、類裝載器ClassLoader裝載流程(雙親委派)
- 虛拟機自帶的加載器
- 啟動類加載器(Bootstrap)C++
- 擴充類加載器(Extension)Java
- 應用程式類加載器(AppClassLoader)Java 也叫系統類加載器,加載目前應用的classpath的所有類
- 使用者自定義加載器 Java.lang.ClassLoader的子類,使用者可以定制類的加載方式
注:Execution Engine執行引擎負責解釋指令,送出作業系統執行。
5、Native Interface本地接口
本地接口的作用是融合不同的程式設計語言為 Java 所用,它的初衷是融合 C/C++程式,Java 誕生的時候是 C/C++橫行的時候,要想立足,必須有調用 C/C++程式,于是就在記憶體中專門開辟了一塊區域處理标記為native的代碼,它的具體做法是 Native Method Stack中登記 native方法,在Execution Engine 執行時加載native libraies。
目前該方法使用的越來越少了,除非是與硬體有關的應用,比如通過Java程式驅動列印機或者Java系統管理生産裝置,在企業級應用中已經比較少見。因為現在的異構領域間的通信很發達,比如可以使用 Socket通信,也可以使用Web Service等等,不多做介紹。
6、Native Method Stack(本地方法棧)
它的具體做法是Native Method Stack中登記native方法,在Execution Engine 執行時加載本地方法庫。
7、PC寄存器
每個線程都有一個程式計數器,是線程私有的,就是一個指針,指向方法區中的方法位元組碼(用來存儲指向下一條指令的位址,也即将要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記。
這塊記憶體區域很小,它是目前線程所執行的位元組碼的行号訓示器,位元組碼解釋器通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。
如果執行的是一個Native方法,那這個計數器是空的。
用以完成分支、循環、跳轉、異常處理、線程恢複等基礎功能。不會發生記憶體溢出(OutOfMemory=OOM)錯誤
8、 Method Area 方法區
方法區是被所有線程共享,所有字段和方法位元組碼,以及一些特殊方法如構造函數,接口代碼也在此定義。簡單說,所有定義的方法的資訊都儲存在該區域,此區屬于共享區間。
靜态變量+常量+類資訊(構造方法/接口定義)+運作時常量池存在方法區中
But
執行個體變量存在堆記憶體中,和方法區無關
9、Stack 棧
棧也叫棧記憶體,主管Java程式的運作,是線上程建立時建立,它的生命期是跟随線程的生命期,線程結束棧記憶體也就釋放,對于棧來說不存在垃圾回收問題,隻要線程一結束該棧就Over,生命周期和線程一緻,是線程私有的。8種基本類型的變量+對象的引用變量+執行個體方法都是在函數的棧記憶體中配置設定。
9.1 棧存儲什麼?
棧幀中主要儲存3 類資料:
- 本地變量(Local Variables):輸入參數和輸出參數以及方法内的變量;
- 棧操作(Operand Stack):記錄出棧、入棧的操作;
- 棧幀資料(Frame Data):包括類檔案、方法等等。
9.2 棧運作原理
棧中的資料都是以棧幀(Stack Frame)的格式存在,棧幀是一個記憶體區塊,是一個資料集,是一個有關方法(Method)和運作期資料的資料集,當一個方法A被調用時就産生了一個棧幀 F1,并被壓入到棧中,
A方法又調用了 B方法,于是産生棧幀 F2 也被壓入棧,
B方法又調用了 C方法,于是産生棧幀 F3 也被壓入棧,
……
執行完畢後,先彈出F3棧幀,再彈出F2棧幀,再彈出F1棧幀……
遵循“先進後出”/“後進先出”原則。
每個方法執行的同時都會建立一個棧幀,用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊,每一個方法從調用直至執行完畢的過程,就對應着一個棧幀在虛拟機中入棧到出棧的過程。棧的大小和具體JVM的實作有關,通常在256K~756K之間。
圖示在一個棧中有兩個棧幀:
- 棧幀 2是最先被調用的方法,先入棧,
- 然後方法 2 又調用了方法1,棧幀 1處于棧頂的位置,
- 棧幀 2 處于棧底,執行完畢後,依次彈出棧幀 1和棧幀 2,
- 線程結束,棧釋放。
每執行一個方法都會産生一個棧幀,儲存到棧(後進先出)的頂部,頂部棧就是目前的方法,該方法執行完畢 後會自動将此棧幀出棧。
10、棧+堆+方法區的互動關系
HotSpot是使用指針的方式來通路對象:
Java堆中會存放通路類中繼資料的位址,reference存儲的就直接是對象的位址
三種JVM:
- Sun公司的HotSpot
- BEA公司的JRockit
- IBM公司的J9 VM
二、堆體系結構概述
1、Heap堆(Java7之前)
一個JVM執行個體隻存在一個堆記憶體,堆記憶體的大小是可以調節的。類加載器讀取了類檔案後,需要把類、方法、常變量放到堆記憶體中,儲存所有引用類型的真實資訊,以友善執行器執行。
堆記憶體邏輯上分為三部分:新生+養老+永久
新生區
新生區是類的誕生、成長、消亡的區域,一個類在這裡産生,應用,最後被垃圾回收器收集,結束生命。新生區又分為兩部分: 伊甸區(Eden space)和幸存者區(Survivor pace) ,所有的類都是在伊甸區被new出來的。幸存區有兩個: 0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程式又需要建立對象,JVM的垃圾回收器将對伊甸園區進行垃圾回收(Minor GC),将伊甸園區中的不再被其他對象所引用的對象進行銷毀。然後将伊甸園中的剩餘對象移動到幸存 0區。若幸存 0區也滿了,再對該區進行垃圾回收,然後移動到 1 區。那如果1 區也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候将産生MajorGC(FullGC),進行養老區的記憶體清理。若養老區執行了Full GC之後發現依然無法進行對象的儲存,就會産生OOM異常“OutOfMemoryError”。
如果出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛拟機的堆記憶體不夠。原因有二:
(1)Java虛拟機的堆記憶體設定不夠,可以通過參數-Xms、-Xmx來調整。
(2)代碼中建立了大量大對象,并且長時間不能被垃圾收集器收集(存在被引用)。
永久區
永久存儲區是一個常駐記憶體區域,用于存放JDK自身所攜帶的 Class,Interface 的中繼資料,也就是說它存儲的是運作環境必須的類資訊,被裝載進此區域的資料是不會被垃圾回收器回收掉的,關閉 JVM 才會釋放此區域所占用的記憶體。
如果出現java.lang.OutOfMemoryError: PermGen space,說明是Java虛拟機對永久代Perm記憶體設定不夠。一般出現這種情況,都是程式啟動需要加載大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。或者大量動态反射生成的類不斷被加載,最終導緻Perm區被占滿。
Jdk1.6及之前: 有永久代, 常量池1.6在方法區
Jdk1.7: 有永久代,但已經逐漸“去永久代”,常量池1.7在堆
Jdk1.8及之後: 無永久代,常量池1.8在元空間
了解三區結構後方可了解-JVM垃圾收集
實際而言,方法區(Method Area)和堆一樣,是各個線程共享的記憶體區域,它用于存儲虛拟機加載的:類資訊+普通常量+靜态常量+編譯器編譯後的代碼等等,雖然JVM規範将方法區描述為堆的一個邏輯部分,但它卻還有一個别名叫做Non-Heap(非堆),目的就是要和堆分開。
對于HotSpot虛拟機,很多開發者習慣将方法區稱之為“永久代(Parmanent Gen)” ,但嚴格本質上說兩者不同,或者說使用永久代來實作方法區而已,永久代是方法區(相當于是一個接口interface)的一個實作,jdk1.7的版本中,已經将原本放在永久代的字元串常量池移走。
三、堆參數微調優入門
JVM垃圾收集(Java Garbage Collection )
這裡均以JDK1.8+HotSpot為例
1、Java 7
2、Java 8
3、堆記憶體調優簡介01
4、堆記憶體調優簡介02
5、堆記憶體調優簡介03
此圖為java7,示範為8
6、堆記憶體調優簡介04
GC 記憶體回收機制
四、GC垃圾回收
1、GC是什麼
定義: 分代收集算法
- 次數上頻繁收集Young區
- 次數上較少收集Old區
- 基本不動Perm區
GC4大算法
2、GC算法總體概述
JVM在進行GC時,并非每次都對上面三個記憶體區域一起回收的,大部分時候回收的都是指新生代。
是以GC按照回收的區域又分了兩種類型,一種是普通GC(minor GC),一種是全局GC(major GC or Full GC)
Minor GC和Full GC的差別
普通GC(minor GC):隻針對新生代區域的GC,指發生在新生代的垃圾收集動作,因為大多數Java對象存活率都不高,是以Minor GC非常頻繁,一般回收速度也比較快。
全局GC(major GC or Full GC):指發生在老年代的垃圾收集動作,出現了Major GC,經常會伴随至少一次的Minor GC(但并不是絕對的)。Major GC的速度一般要比Minor GC慢上10倍以上
3 四種算法
3.1 引用計數法
Code示範:
package com.atguigu.jvm;
/**@Description:-verbose:gc*/
public class RefCountGC {
private byte[] bigSize = new byte[2 * 1024 * 1024];//這個成員屬性唯一的作用就是占用一點記憶體
Object instance = null;
public static void main(String[] args) {
RefCountGC objectA = new RefCountGC();
RefCountGC objectB = new RefCountGC();
objectA.instance = objectB;
objectB.instance = objectA;
objectA = null;
objectB = null;
System.gc();
}
}
3.2 複制算法(Copying)
(1)定義:年輕代中使用的是Minor GC,這種GC算法采用的是複制算法(Copying)
(2)原理:
Minor GC會把Eden中的所有活的對象都移到Survivor區域中,如果Survivor區中放不下,那麼剩下的活的對象就被移到Old generation中,也即一旦收集後,Eden是就變成空的了。
當對象在 Eden ( 包括一個 Survivor 區域,這裡假設是 from 區域 ) 出生後,在經過一次 Minor GC 後,如果對象還存活,并且能夠被另外一塊 Survivor 區域所容納( 上面已經假設為 from 區域,這裡應為 to 區域,即 to 區域有足夠的記憶體空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複制算法将這些仍然還存活的對象複制到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),并且将這些對象的年齡設定為1,以後對象在 Survivor 區每熬過一次 Minor GC,就将對象的年齡 + 1,當對象的年齡達到某個值時 ( 預設是 15 歲,通過-XX:MaxTenuringThreshold 來設定參數),這些對象就會成為老年代。
-XX:MaxTenuringThreshold — 設定對象在新生代中存活的次數
(3)動态示範:
在GC開始的時候,對象隻會存在于Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡門檻值,可以通過-XX:MaxTenuringThreshold來設定)的對象會被移動到年老代中,沒有達到門檻值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會将所有對象移動到年老代中。
因為Eden區對象一般存活率較低,一般的,使用兩塊10%的記憶體作為空閑和活動區間,而另外80%的記憶體,則是用來給建立對象配置設定記憶體的。一旦發生GC,将10%的from活動區間與另外80%中存活的eden對象轉移到10%的to空閑區間,接下來,将之前90%的記憶體全部釋放,以此類推。
(4)動态:
劣勢:
複制算法它的缺點也是相當明顯的。
- 它浪費了一半的記憶體,這太要命了。
- 如果對象的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要将所有對象都複制一遍,并将所有引用位址重置一遍。複制這一工作所花費的時間,在對象存活率達到一定程度時,将會變的不可忽視。 是以從以上描述不難看出,複制算法要想使用,最起碼對象的存活率要非常低才行,而且最重要的是,我們必須要克服50%記憶體的浪費。
3.3 标記清除(Mark-Sweep)
(1)老年代一般是由标記清除或者是标記清除與标記整理的混合實作
當堆中的有效記憶體空間(available memory)被耗盡的時候,就會停止整個程式(也被稱為stop the world),然後進行兩項工作,第一項則是标記,第二項則是清除。
标記:從引用根節點開始标記所有被引用的對象。标記的過程其實就是周遊所有的GC Roots,然後将所有GC Roots可達的對象 标記為存活的對象。
清除:周遊整個堆,把未标記的對象清除。
缺點:此算法需要暫停整個應用,會産生記憶體碎片
用通俗的話解釋一下标記/清除算法,就是當程式運作期間,若可以使用的記憶體被耗盡的時候,GC線程就會被觸發并将程式暫停,随後将依舊存活的對象标記一遍,最終再将堆中所有沒被标記的對象全部清除掉,接下來便讓程式恢複運作。
(3)動态示範
動圖:
- 回收時,對需要存活的對象進行标記
- 回收不是綠色的對象
(4)劣勢
- 首先,它的缺點就是效率比較低(遞歸與全堆對象周遊),而且在進行GC的時候,需要停止應用程式,這會導緻使用者體驗非常差勁
- 其次,主要的缺點則是這種方式清理出來的空閑記憶體是不連續的,這點不難了解,我們的死亡對象都是随即的出現在記憶體的各個角落的,現在把它們清除之後,記憶體的布局自然會亂七八糟。而為了應付這一點,JVM就不得不維持一個記憶體的空閑清單,這又是一種開銷。而且在配置設定數組對象的時候,尋找連續的記憶體空間會不太好找。
3.4 标記壓縮(Mark-Compact)
(1)定義:老年代一般是由标記清除或者是标記清除與标記整理的混合實作
在整理壓縮階段,不再對标記的對像做回收,而是通過所有存活對像都向一端移動,然後直接清除邊界以外的記憶體。
可以看到,标記的存活對象将會被整理,按照記憶體位址依次排列,而未被标記的記憶體會被清理掉。如此一來,當我們需要給新對象配置設定記憶體時,JVM隻需要持有一個記憶體的起始位址即可,這比維護一個空閑清單顯然少了許多開銷。
标記/整理算法不僅可以彌補标記/清除算法當中,記憶體區域分散的缺點,也消除了複制算法當中,記憶體減半的高額代價
(3)劣勢
标記/整理算法唯一的缺點就是效率也不高,不僅要标記所有存活對象,還要整理所有存活對象的引用位址。從效率上來說,标記/整理算法要低于複制算法。
3.5 标記清除壓縮(Mark-Sweep-Compact)
動态示範
4、總結
記憶體效率: 複制算法 > 标記清除算法 > 标記整理算法(此處的效率隻是簡單的對比時間複雜度,實際情況不一定如此)。
記憶體整齊度:複制算法 = 标記整理算法 > 标記清除算法。
記憶體使用率:标記整理算法 = 标記清除算法 > 複制算法。
可以看出,效率上來說,複制算法是當之無愧的老大,但是卻浪費了太多記憶體,而為了盡量兼顧上面所提到的三個名額,标記/整理算法相對來說更平滑一些,但效率上依然不盡如人意,它比複制算法多了一個标記的階段,又比标記/清除多了一個整理記憶體的過程
5、難道就沒有一種最優算法嗎? 猜猜看,下面還有
回答:無,沒有最好的算法,隻有最合适的算法。==========>分代收集算法。
年輕代(Young Gen)
年輕代特點是區域相對老年代較小,對像存活率低。
這種情況複制算法的回收整理,速度是最快的。複制算法的效率隻和目前存活對像大小有關,因而很适用于年輕代的回收。而複制算法記憶體使用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。
老年代(Tenure Gen)
老年代的特點是區域較大,對像存活率高。x
這種情況,存在大量存活率高的對像,複制算法明顯變得不合适。一般是由标記清除或者是标記清除與标記整理的混合實作。
Mark階段的開銷與存活對像的數量成正比,這點上說來,對于老年代,标記清除或者标記整理有一些不符,但可以通過多核/線程利用,對并發、并行的形式提标記效率。
Sweep階段的開銷與所管理區域的大小形正相關,但Sweep“就地處決”的特點,回收的過程沒有對像的移動。使其相對其它有對像移動步驟的回收算法,仍然是效率最好的。但是需要解決記憶體碎片問題。
Compact階段的開銷與存活對像的資料成開比,如上一條所描述,對于大量對像的移動是很大開銷的,做為老年代的第一選擇并不合适。
基于上面的考慮,老年代一般是由标記清除或者是标記清除與标記整理的混合實作。以hotspot中的CMS回收器為例,CMS是基于Mark-Sweep實作的,對于對像的回收效率很高,而對于碎片問題,CMS采用基于Mark-Compact算法的Serial Old回收器做為補償措施:當記憶體回收不佳(碎片導緻的Concurrent Mode Failure時),将采用Serial Old執行Full GC以達到對老年代記憶體的整理。
五、面試題
- JVM記憶體模型以及分區,需要詳細到每個區放什麼
- 堆裡面的分區:Eden,survival from to,老年代,各自的特點。
- GC的三種收集方法:标記清除、标記整理、複制算法的原理與特點,分别用在什麼地方
- Minor GC與Full GC分别在什麼時候發生