一、JVM整體架構 1、JVM(Java虛擬機):指以軟體的方式模擬具有完整硬體系統功能、運行在一個完全隔離環境中的完整電腦系統,是物理機的軟體實現。常用的虛擬機有VMWare、Virtual Box、Java Virtual Machine。 2、JVM由三個主要的子系統構成 類載入子系統 (即 ...
一、JVM整體架構
1、JVM(Java虛擬機):指以軟體的方式模擬具有完整硬體系統功能、運行在一個完全隔離環境中的完整電腦系統,是物理機的軟體實現。常用的虛擬機有VMWare、Virtual Box、Java Virtual Machine。
2、JVM由三個主要的子系統構成
- 類載入子系統 (即類載入器)
- 運行時數據區(即記憶體結構 / 記憶體模型 / JMM)
- 執行引擎(包含垃圾收集器)
通過類載入器將.class文件載入進記憶體中,再由執行引擎去運行。
堆和方法區是所有線程都共用的。
Java棧、本地方法棧和程式計數器是非線程共用的。
二、JVM記憶體結構
下麵會用到的例子:定義一個Math類。
1、本地方法棧(線程私有):登記native本地方法(即native方法是存到本地方法棧),在執行引擎執行時載入本地方法庫(C語言實現的庫)。
舉例:Thread類中的 start0() 方法底層實現是用C語言寫的。
2、程式計數器(線程私有):就是一個指針,指向方法區中的方法位元組碼(用來存儲指向下一條指令的地址,也即指向將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不計。
舉例:對.class文件進行 javap -c 反編譯,下圖為 math() 方法反編譯後的結果:(iconst_1和istore_1兩條指令代表 int a=1)
3、方法區(線程共用):類的所有欄位和方法位元組碼,以及一些特殊方法如構造函數、介面代碼也定義在此(類載入器會將位元組碼文件載入到方法區中,分解成多個部分)。簡單說,所有定義的方法的信息都保存在該區域,靜態變數+常量+類信息(構造方法/介面定義)+運行時常量池都存在方法區中,雖然JVM規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應該是與Java堆區分開來。
4、Java棧(線程私有):Java線程執行方法的記憶體模型,一個線程對應一個棧,每個方法在執行的同時都會創建一個棧幀(用於存儲局部變數表、操作數棧、動態鏈接、方法出口等信息),不存在垃圾回收問題,只要線程一結束該棧就釋放,生命周期和線程一致。
JVM對該區域規範了兩種異常:
① 線程請求的棧深度大於虛擬機棧所允許的深度時,將拋出StackOverFlowError異常。
② 若虛擬機棧可動態擴展,當無法申請到足夠記憶體空間時將拋出OutOfMemoryError,通過JVM參數 -> Xss指定棧空間,空間大小決定函數調用的深度。
下圖左邊為Java棧的記憶體結構圖:
動態鏈接:如Map map = new HashMap(),程式運行時map變數需要去找到運行時的HashMap實例對象(多態)的過程,就叫動態鏈接。
方法出口:如main()方法中的math()方法運行結束return返回的值用於main()方法中,return的這個過程就叫方法出口。
5、堆(線程共用): 虛擬機啟動時創建,用於存放對象實例,幾乎所有的對象(包含常量池)都放在堆上分配記憶體,當對象無法在該空間申請到記憶體時將拋出OutOfMemoryError異常。同時也是垃圾收集器管理的主要區域。可通過 -Xmx -Xms 參數來分別指定最大堆和最小堆。
下圖為堆的記憶體結構圖:
JVM調優的根本目的:儘量減少Full GC的次數,並且縮短每次Full GC的時間長度。(Full GC性能非常低,執行這個過程時會停止整個JVM,即STW(Stop The World),包括程式運行的那些線程,然後專門去做垃圾收集。現在新的GC會儘量縮短STW時間長度,儘量讓垃圾收集和業務線程併發執行)
垃圾收集器(GC)存在的意義:回收堆中無引用的對象,釋放空間。
堆中新生代(Young Generation)占1/3的堆空間,新生代包括伊甸園區(Eden Space)和幸存者區(Survivor Space);老年代(Old Generation)占2/3的堆空間。
JDK1.8以前是沒有元數據區(MetaData Space)的,而是永久代,從1.8開始元空間取代了永久代,本質和永久代類似,都是對JVM規範中方法區的實現,區別在於元數據區並不在虛擬機中,不屬於堆中的記憶體結構,而是直接使用本機物理記憶體,永久代在虛擬機中,邏輯結構上屬於堆,但是物理上不屬於堆,堆大小=新生代+老年代。元數據區也有可能發生OutOfMemory異常。
- JDK1.6及之前:有永久代,常量池在方法區
- JDK1.7:有永久代,但已逐步“去永久代”,常量池在堆
- JDK1.8及之後:無永久代,常量池在元空間
提問:為什麼JDK1.8用元數據區取代了永久代?
官方解釋:移除永久代是為融合HotSpot JVM與JRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代。
(JDK 8實際上是這兩種虛擬機合併之後的產物,HotSpot JVM(Sun公司開發),JRockit VM(BEA公司開發)。猜測:HotSpot JVM輸了,沒錯,開發出Java語言的Sun公司最後輸了,就聽JRockit VM的)
① new出來的對象會放在Eden區中,當Eden區占滿時會做一次Minor GC(即輕GC),回收Eden區中無引用的對象,剩下的存活對象則會放到From區中。
② 後面又有新的new出來的對象不斷存放在Eden區中,當Eden區占滿時再做Minor GC,又有對象存放在From區中,當From區占滿時也會做Minor GC,GC會回收Eden區和From區中無引用的對象,剩下的存活對象則會放到To區中,此時角色轉變,From區變成To區,To區變成From區。
③ 接著,還會有新的對象存放到Eden區中,當Eden區占滿時再做Minor GC,此時存活的對象放到新的From區中,當新的From區放滿時又做Minor GC,把存活對象放入新的To區,依次輪循。
④ 如果一直有新的對象過來,不斷的做Minor GC,當幸存者區的From區和To區輪循大概15次時,如果To區還有存活的對象,再做一次Minor GC則會把存活的對象移入老年代。
⑤ 如果有一天老年代也占滿了,則會觸發Full GC再次回收無引用對象(有的GC會回收所有區,有的只會回收老年代,得看使用了哪種GC,後面會講)。
三、JVM執行引擎
JVM執行引擎:讀取運行時數據區的Java位元組碼並逐個執行。
解釋執行位元組碼的方式:① JIT編譯器;② 位元組碼解釋器;③ mixed mode(前兩種的混合模式,JDK 8是採用這種,之前的我不知道)
JIT編譯器(Just In Time,即時編譯):一次性解釋所有指令,第一次執行慢,後面執行快。(會緩存)
位元組碼解釋器:逐行解釋指令,每次執行需要重新解釋,前幾次可能比JIT編譯器快,後面比它慢。(不緩存)