1. 程式人生 > >JVM類載入與執行時優化

JVM類載入與執行時優化

  1. 類載入生命週期

這裡寫圖片描述

	a. 裝載(load)
		i. 開始時機:
			1) new例項化物件時,若類沒有載入
			2) 讀取或設定一個類static欄位,若類沒有被載入。final除外,因為final欄位的值已經在編譯期放到了常量池中
			3) 呼叫類的static方法
			4) 反射呼叫類
			5) 初始化一個類時,若父類沒有被載入,會先載入父類
		ii. 不會載入類的情況
			1) 通過子類去引用父類的static欄位,不會導致子類載入
			2) 陣列定義引用類,不會導致類載入。 如 Student[] stu = new Student[10], student類不會被載入
			3) 讀取類的final欄位,不會導致類載入。
		iii. 流程
			1) 通過類的全限定名獲取定義此類的二進位制流。可以從class檔案,網路,或者執行時計算(如動態代理)出這個二進位制流
			2) 將位元組流代表的static儲存結構轉化為方法區的執行時資料結構,也就是儲存到方法區中
			3) 記憶體中生成一個代表此類的class物件
	b. 連結
		i. 驗證
			1) 目的:防止載入的class檔案危害虛擬機器本身安全
			2) 流程:
				a) 檔案格式驗證,如magic是否為0xCAFEBABE,主次版本號是否在當前VM能處理範圍內
				b) 元資料驗證,主要驗證描述資訊是否符合Java語言規範
				c) 位元組碼驗證,最複雜,通過資料流和控制流分析,確定程式語義是合法的,符合邏輯的
				d) 符號引用驗證,如通過全限定名能否找到類,欄位方法的可訪問性等。
		ii. 準備
			1) 目的:為static變數分配記憶體,並將它們統一初始化為0. static final除外
		iii. 解析
			1) 目的:將常量池中的符號引用替換為直接引用
				a) 符號引用:字面量,如類名,方法名等
				b) 直接引用:類或方法存放在記憶體中的地址
	c. 初始化
		i. 初始化static變數為實際的值。通過執行類構造器<clint>方法來完成。這個方法是編譯器自動生成在位元組碼中的
		ii. clinit:
			1) static變數的賦值 + static{}語句塊。 static{}語句塊只能訪問到定義在它之前的變數。
			2) clinit和例項構造器init不同,不需要顯式呼叫父類構造器。JVM保證子類的clinit執行前,父類的clinit肯定被執行過
			3) 父類的clinit先執行,故父類中的static{}語句塊在子類的static變數賦值前被呼叫
			4) 介面中不能使用static{}語句塊,但仍然可以為變數賦值
			5) JVM可以保證clinit方法是執行緒安全的。多個執行緒同時去初始化一個類,只有一個執行緒會執行clinit方法,其他都會阻塞等待。
  1. 編譯期優化(早期優化)
  1. 為了保證JRuby,Groovy等語言編譯的位元組碼也能得到效能優化,JVM將效能優化放在了後期的執行時優化,即JIT執行時編譯優化中

  2. 編譯期優化主要為語法糖,用來實現Java的各種新的語法特性,比如泛型,變長引數,自動裝箱/拆箱

  3. Java語法糖:與位元組碼無關,編譯後會去掉它們。作用僅僅為方便碼農寫程式碼,以及將執行時異常在編譯期及早發現(如泛型的使用)

    1) 泛型與型別擦除

    Java泛型只在編譯期存在,編譯完成後的位元組碼中會替換為原生型別。故稱Java泛型為偽泛型。C#的泛型在執行期仍然存在。
    

    2) 條件編譯

    if語句中使用常量。比如if(false) {}, 這個語句塊不會被編譯到位元組碼中.這個過程在編譯時的控制流分析中完成。
    
  4. 執行時優化(晚期優化)

1). 解釋執行和編譯執行的對比

	a. 解釋執行需要JVM直譯器,速度慢。編譯執行生成了本地機器語言,速度快。
	b. 解釋執行利用位元組碼,比機器碼緊湊,故需要記憶體比編譯執行小。
	c. 解釋執行可以直接在位元組碼上執行,而編譯執行需要執行時將位元組碼先轉化為機器碼。故編譯執行啟動時間較長。
	d. 編譯器可以將一些執行頻繁的熱點位元組碼,優化為機器碼,從而加快執行速度。直譯器也可以將一些優化得比較激進的機器程式碼,逆優化為位元組碼,進行解釋執行。 

2)不同JVM的執行時優化策略

	a. Hotspot採用直譯器與編譯器並存的構架。
		i. 第0層,解釋執行,不開啟效能監控器,可觸發第一層編譯
		ii. 第1層,將位元組碼編譯為機器碼,進行簡單可靠的優化,可以開啟效能監控
		iii. 第2層,將位元組碼編譯為機器碼,會開啟一些編譯耗時的優化和一些不可靠的激進優化
	b. 早期JVM很多隻有直譯器
	c. JRockit虛擬機器只有編譯器,沒有直譯器。因為它是面向伺服器應用的。

3)編譯物件和觸發條件

	a. 熱點程式碼
		i. 被多次呼叫的方法
		ii. 被多次執行的迴圈體
	b. 熱點探測方式
		i. 基於取樣的熱點探測:
			1) 週期性檢查各個執行緒的棧頂,發現某個方法經常位於棧頂,則被認為是熱點方法
			2) 簡單,高效,並且可以得到方法呼叫鏈。但很難精確確認方法熱度,因為執行緒可能阻塞等
		ii. 基於計數器
			1) 為每個方法建立計數器,統計它的執行次數
			2) 麻煩,但很精確。HotSpot中採用的就是這種方法
	c. 流程:
		i. 檢查是否存在被JIT編譯過的方法版本
		ii. 不存在,則計數器加1
		iii. 判斷計數是否超過閾值。超過,則提交即時編譯的申請
		iv. 執行引擎不會同步等待編譯完成,而是繼續使用直譯器執行。
		v. JIT編譯完成後,方法的呼叫入口會被自動更新。這樣,下一次呼叫的時候,就是新的入口,也就是JIT編譯之後的了。

4)編譯過程

	a. 位元組碼 -> HIR,基礎化的優化,如方法內聯
	b. HIR -> LIR, 從HIR中產生低階中間程式碼表示
	c. LIR -> 機器碼,使用線性掃描演算法,在LIR上分配暫存器,產生機器程式碼

5)Java即時編譯與C/C++編譯對比

	a. 劣勢
		i. Java即時編譯是在執行時,故會佔用使用者程式的執行時間。而C/C++是靜態編譯為機器碼的,完全不佔用執行時間。
		ii. Java執行時不能進行一些比較耗時的優化,故能做的優化也沒有C那麼多
		iii. 多型選擇頻率遠高於C,需要建立虛方法表。也正是多型的存在,使得編譯優化難度遠高於C。因為多型較難預測程式碼跳轉分支。
		iv. Java執行時可以載入新的類,如網路中的二進位制流。這使得編譯器無法看清程式全貌,全域性優化很難進行。
		v. Java物件都是在堆上分配(除了class物件在方法區),而C/C++既可以在堆上,又可以在棧上。棧上分配可以減輕垃圾回收壓力,且速度遠快於堆。
	b. 優勢
		i. 可以進行效能監控,熱點探測,分支頻率預測,呼叫頻率預測,從而有選擇性的優化程式碼
		
一句話,Java效能上的劣勢,是為了換回碼農開發效率上的優勢。Java用心良苦,碼農們不要再吐槽它的效能了!