Java虛擬機器:JVM架構與垃圾回收

a745233700發表於2019-04-11

 

一、JVM架構圖分析:

JVM被分為三個主要的子系統:

(1)類載入器子系統(2)執行時資料區(3)執行引擎。

1. 類載入器子系統

Java的動態類載入功能是由類載入器子系統處理。當它在執行時(不是編譯時)首次引用一個類時,它載入、連結並初始化該類檔案。

1.1 載入:

類由此元件載入。啟動類載入器 (BootStrap class Loader)、擴充套件類載入器(Extension class Loader)和應用程式類載入器(Application class Loader) 這三種類載入器幫助完成類的載入。

  • 啟動類載入器 – 負責從啟動類路徑中載入類,無非就是rt.jar。這個載入器會被賦予最高優先順序。
  • 擴充套件類載入器 – 負責載入ext 目錄(jre\lib)內的類.
  • 應用程式類載入器 – 負責載入應用程式級別類路徑,涉及到路徑的環境變數等etc.

上述的類載入器會遵循委託層次演算法(Delegation Hierarchy Algorithm)載入類檔案

1.2 連結:

  • 校驗 – 位元組碼校驗器會校驗生成的位元組碼是否正確,如果校驗失敗,我們會得到校驗錯誤。
  • 準備 – 分配記憶體並初始化預設值給所有的靜態變數。
  • 解析 – 所有符號記憶體引用被方法區(Method Area)的原始引用所替代。

1.3 初始化:

這是類載入的最後階段,這裡所有的靜態變數會被賦初始值, 並且靜態塊將被執行。

2. 執行時資料區(Runtime Data Area)

執行時資料區域被劃分為5個主要元件:

2.1 方法區(Method Area)

所有類級別資料將被儲存在這裡,包括靜態變數。每個JVM只有一個方法區,它是一個共享的資源。

2.2 堆區(Heap Area)

所有的物件和它們相應的例項變數以及陣列將被儲存在這裡。每個JVM同樣只有一個堆區。由於方法區堆區的記憶體由多個執行緒共享,所以儲存的資料不是執行緒安全的

2.3 棧區(Stack Area)

對每個執行緒會單獨建立一個執行時棧。對每個函式呼叫會在棧記憶體生成一個棧幀(Stack Frame)。所有的區域性變數將在棧記憶體中建立。棧區是執行緒安全的,因為它不是一個共享資源。棧幀被分為三個子實體:

  • a 區域性變數陣列 – 包含多少個與方法相關的區域性變數並且相應的值將被儲存在這裡。
  • b 運算元棧 – 如果需要執行任何中間操作,運算元棧作為執行時工作區去執行指令。
  • c 幀資料 – 方法的所有符號都儲存在這裡。在任意異常的情況下,catch塊的資訊將會被儲存在幀資料裡面。

2.4 PC暫存器

每個執行緒都有一個單獨的PC暫存器來儲存當前執行指令的地址,一旦該指令被執行,pc暫存器會被更新至下條指令的地址。

2.5 本地方法棧

本地方法棧儲存本地方法資訊。對每一個執行緒,將建立一個單獨的本地方法棧。

3. 執行引擎

分配給執行時資料區的位元組碼將由執行引擎執行。執行引擎讀取位元組碼並逐段執行。

3.1  直譯器:

 直譯器能快速的解釋位元組碼,但執行卻很慢。 直譯器的缺點就是,當一個方法被呼叫多次,每次都需要重新解釋。

3.2 編譯器:

JIT編譯器消除了直譯器的缺點。執行引擎利用直譯器轉換位元組碼,但如果是重複的程式碼則使用JIT編譯器將全部位元組碼編譯成本機程式碼。本機程式碼將直接用於重複的方法呼叫,這提高了系統的效能。

3.3  垃圾回收器:

收集並刪除未引用的物件。可以通過呼叫"System.gc()"來觸發垃圾回收,但並不保證會確實進行垃圾回收。JVM的垃圾回收只收集哪些由new關鍵字建立的物件。所以,如果不是用new建立的物件,你可以使用finalize函式來執行清理。

3.4 Java本地介面 (JNI)JNI 會與本地方法庫進行互動並提供執行引擎所需的本地庫。

3.5 本地方法庫:它是一個執行引擎所需的本地庫的集合。

 

二、JVM三大核心區域:

1、通過一個小程式認識JVM:

package com.spark.jvm;
/**
 * 從JVM呼叫的角度分析java程式堆記憶體空間的使用:
 * 當JVM程式啟動的時候,會從類載入路徑中找到包含main方法的入口類HelloJVM
 * 找到HelloJVM會直接讀取該檔案中的二進位制資料,並且把該類的資訊放到執行時的Method記憶體區域中。
 * 然後會定位到HelloJVM中的main方法的位元組碼中,並開始執行Main方法中的指令
 * 此時會建立Student例項物件,並且使用student來引用該物件(或者說給該物件命名),其內幕如下:
 * 第一步:JVM會直接到Method區域中去查詢Student類的資訊,此時發現沒有Student類,就通過類載入器載入該Student類檔案;
 * 第二步:在JVM的Method區域中載入並找到了Student類之後會在Heap區域中為Student例項物件分配記憶體,
 * 並且在Student的例項物件中持有指向方法區域中的Student類的引用(記憶體地址);
 * 第三步:JVM例項化完成後會在當前執行緒中為Stack中的reference建立實際的應用關係,此時會賦值給student
 * 接下來就是呼叫方法
 * 在JVM中方法的呼叫一定是屬於執行緒的行為,也就是說方法呼叫本身會發生線上程的方法呼叫棧:
 * 執行緒的方法呼叫棧(Method Stack Frames),每一個方法的呼叫就是方法呼叫棧中的一個Frame,
 * 該Frame包含了方法的引數,區域性變數,臨時資料等 student.sayHello();
 */
public class HelloJVM {
	//在JVM執行的時候會通過反射的方式到Method區域找到入口方法main
	public static void main(String[] args) {//main方法也是放在Method方法區域中的
		/**
		 * student(小寫的)是放在主執行緒中的Stack區域中的
		 * Student物件例項是放在所有執行緒共享的Heap區域中的
		 */
		Student student = new Student("spark");
		/**
		 * 首先會通過student指標(或控制程式碼)(指標就直接指向堆中的物件,控制程式碼表明有一箇中間的,student指向控制程式碼,控制程式碼指向物件)
		 * 找Student物件,當找到該物件後會通過物件內部指向方法區域中的指標來呼叫具體的方法去執行任務
		 */
		student.sayHello();
	}
}
 
class Student {
	// name本身作為成員是放在stack區域的但是name指向的String物件是放在Heap中
	private String name;
	public Student(String name) {
		this.name = name;
	}
	//sayHello這個方法是放在方法區中的
	public void sayHello() {
	System.out.println("Hello, this is " + this.name);
	}
}

2、JVM三大效能調優引數:-Xms –Xmx –Xss

-Xms –Xmx是對堆的效能調優引數,一般兩個設定是一樣的,如果不一樣,當Heap不夠用,會發生記憶體抖動。一般都調大這兩個引數,並且兩個大小一樣。

-Xss是對每一個執行緒棧的效能調優引數,影響堆疊呼叫的深度

實戰演示從OOM推匯出JVM GC時候基於的記憶體結構:Young Generation(Eden、From、To)、OldGeneration、Permanent Generation

JVMHeap區域(年輕代、老年代)和方法區(永久代)結構圖:

從Java GC的角度解讀程式碼:程式20行new的Person物件會首先會進入年輕代的Eden中(如果物件太大可能直接進入年老代)。在GC之前物件是存在Eden和from中的,進行GC的時候,Eden中的物件被拷貝到To這樣一個survive空間(survive空間:包括from和to,他們的空間大小是一樣的,又叫s1和s2)中,From中的物件的年齡(GC倖存的次數)到一定閾值(預設15)就會放到OldGeneration。(但是實際情況比較複雜,有可能沒有到閾值就從Survive區域直接到Old Generation區域:在進行GC的時候會對Survive中的物件進行判斷,Survive空間中年齡相同的物件的總和大於等於Survive空間一半的話,這組物件就會被複制到OldGeneration,如果沒到次數的From中的物件會被複制到To中,複製完成後To中儲存的是有效的物件,Eden和From中剩下的都是無效的物件,這個時候就把Eden和From中所有的物件清空。在複製的時候Eden中的物件進入To中,To可能已經滿了,這個時候Eden中的物件就會被直接複製到Old Generation中,From中的物件也會直接進入Old Generation中。複製完成後,To和From的名字會對調一下,因為Eden和From都是空的,對調後Eden和To都是空的,下次分配就會分配到Eden。一直迴圈這個流程。好處:使用物件最多和效率最高的就是在Young Generation中,通過From to就避免過於頻繁的產生FullGC(Old Generation滿了一般都會產生FullGC)

虛擬機器在進行MinorGC(新生代的GC)的時候,會判斷要進入OldGeneration區域物件的大小,是否大於Old Generation剩餘空間大小,如果大於就會發生Full GC。

剛分配物件在Eden中,如果空間不足嘗試進行GC,回收空間,如果進行了MinorGC空間依舊不夠就放入Old Generation,如果OldGeneration空間還不夠就OOM了。

比較大的物件,陣列等,大於某值(可配置)就直接分配到老年代,(避免頻繁記憶體拷貝)

3、年輕代和年老代屬於Heap空間的,Permanent Generation(永久代)可以理解成方法區,也有可能發生GC,例如類的例項物件全部被GC了,同時它的類載入器也被GC掉了,這個時候就會觸發永久代中物件的GC。

如果OldGeneration滿了就會產生FullGC,老年代滿了的原因有:(1)from survive中物件的生命週期到一定閾值;(2)分配的物件直接是大物件;(3)由於To 空間不夠,進行GC直接把物件拷貝到年老代(年老代GC時候採用不同的演算法)

如果Young Generation大小分配不合理或空間比較小,這個時候導致物件很容易進入Old Generation中,而Old Generation中回收具體物件的時候速度是遠遠低於Young Generation回收速度。因此實際分配要考慮年老代和新生代的比例,考慮Eden和survives的比例。

Permanent Generation中發生GC的時候也對效能影響非常大,也是Full GC

4、JVM GC時候核心引數:

-XX:NewRatio  –XX:SurvivorRatio  –XX:NewSize  –XX:MaxNewSize

–XX:NewSize–XX:MaxNewSize指定新生代初始大小和最大大小。

(1)-XX:NewRatio    是年老代 新生代相對的比例,比如NewRatio=2,表明年老代是新生代的2倍。老年代佔了heap的2/3,新生代佔了1/3

(2)-XX:SurvivorRatio 配置的是在新生代裡面Eden和一個Servive的比例

如果指定NewRatio還可以指定NewSizeMaxNewSize,如果同時指定了會如何?

NewRatio=2,這個時候新生代會嘗試分配整個Heap大小的1/3的大小,但是分配的空間不會小於-XX:NewSize也不會大於 –XX:MaxNewSize

(3)-XX:NewSize  –XX:MaxNewSize

實際設定比例還是設定固定大小,固定大小理論上速度更高。

-XX:NewSize –XX:MaxNewSize理論越大越好,但是整個Heap大小是有限的,一般年輕代的設定大小不要超過年老代。

-XX:SurvivorRatio新生代裡面Eden和一個Servive的比例,如果SurvivorRatio是5的話,也就是Eden區域是SurviveTo區域的5倍。Survive由From和To構成。結果就是整個Eden佔用了新生代5/7,From和To分別佔用了1/7,如果分配不合理,Eden太大,這樣產生物件很順利,但是進行GC有一部分物件倖存下來,拷貝到To,空間小,就沒有足夠的空間,物件會被放在old Generation中。如果Survive空間大,會有足夠的空間容納GC後存活的物件,但是Eden區域小,會被很快消耗完,這就增加了GC的次數。

5、JVM的GC日誌解讀:

(1)JVM YoungGeneration下MinorGC日誌詳解

[GC (Allocation Failure) [PSYoungGen:2336K->288K(2560K)] 8274K->6418K(9728K), 0.0112926 secs] [Times:user=0.06 sys=0.00, real=0.01 secs]

PSYoungGen(是新生代型別,新生代日誌收集器),2336K表示使用新生代GC前,佔用的記憶體,->288K表示GC後佔用的記憶體,(2560K)代表整個新生代總共大小

8274K(GC前整個JVM Heap對記憶體的佔用)->6418K(MinorGC後記憶體佔用總量)(9728K)(整個堆的大小)0.0112926 secs(Minor GC消耗的時間)] [Times: user=0.06 sys=0.00, real=0.01 secs] 使用者空間,核心空間時間的消耗,real整個的消耗

(2)JVM的GC日誌Full GC日誌每個欄位徹底詳解

[Full GC (Ergonomics) [PSYoungGen: 984K->425K(2048K)] [ParOldGen:7129K->7129K(7168K)] 8114K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1022588 secs] [Times: user=0.56 sys=0.02,real=0.10 secs]

[Full GC (Allocation Failure) [PSYoungGen: 425K->425K(2048K)][ParOldGen: 7129K->7129K(7168K)] 7555K->7555K(9216K), [Metaspace:2613K->2613K(1056768K)], 0.1003696 secs] [Times: user=0.64 sys=0.03,real=0.10 secs]

[Full GC(表明是Full GC) (Ergonomics) [PSYoungGen:FullGC會導致新生代Minor GC產生]984K->425K(2048K)][ParOldGen:(老年代GC)7129K(GC前多大)->7129K(GC後,並沒有降低記憶體佔用,因為寫的程式不斷迴圈一直有引用)(7168K) (老年代總容量)] 8114K(GC前佔用整個Heap空間大小)->7555K (GC後佔用整個Heap空間大小) (9216K) (整個Heap大小,JVM堆的大小), [Metaspace: (java6 7是permanentspace,java8改成Metaspace,類相關的一些資訊) 2613K->2613K(1056768K) (GC前後基本沒變,空間很大)], 0.1022588 secs(GC的耗時,秒為單位)] [Times: user=0.56 sys=0.02, real=0.10 secs](使用者空間耗時,核心空間耗時,真正的耗時時間)

(3)Java8中的JVM的MetaSpace

Metaspace的使用C語言實現的,使用的是OS的空間,Native Memory Space可動態的伸縮,可以根據類載入的資訊的情況,在進行GC的時候進行調整自身的大小,來延緩下一次GC的到來。

可以設定Metaspace的大小,如果超過最大大小就會OOM,不設定如果把整個作業系統的記憶體耗盡了出現OOM,一般會設定一個足夠大的初始值,安全其間會設定最大值。

永久代發生GC有兩種情況,類的所有的例項被GC掉,且class load不存。

對於後設資料空間 簡化了GC, class load不存在了就需要進行GC。

 

 

本文轉自部落格:https://blog.csdn.net/aijiudu/article/details/72991993

相關文章