曾經小小少年,到如今風度翩翩!曾幾何時,每次想了解Java中volatile關鍵字的實現原理時,小編都會去百度找部落格看,翻遍了許許多多的部落格,有講的深入的,有講的淺顯的,反正小編腦子是有點亂了。其中很多部落格講到其底層是給變數加了一條lock
指令,真的是這樣的嗎?確實是。
下面我們就來驗證下到底這個lock
指令是如何得出來的,以及介紹下檢視windows位元組碼的相關工具的使用,由於小編看得懂的位元組碼指令寥寥無幾,因此,暫時還不能每條指令具體分析,留到下篇部落格介紹。
一、Java位元組碼及class檔案反彙編
我們都知道,Java原始碼檔案想要執行,會被編譯器(javac
)編譯為.class
檔案,Java位元組碼檔案具備了相應的格式,而且非常嚴格(具體的class檔案格式,可以查閱《Java虛擬機器規範》)。由於.class
檔案為二進位制檔案,因此我們無法直接使用文字檔案開啟檢視,如果要開啟,我們可以使用諸如Java Decompiler
這類的工具來反編譯.class
檔案。Java虛擬機器與傳統組合語言不同,它不直接使用底層的暫存器,而是設計成一臺基於棧的虛擬機器,在Java方法中,前面指令的執行結果先push進運算元棧,後面的指令如果需要使用到先前的結果,則從運算元棧中將值pop出來。而這些操作,底層Java虛擬器在讀取
、儲存
、出棧
、入棧
等方面擁有許許多多的位元組碼指令支援,我們可以這樣子理解,如果說彙編指令屬於底層作業系統指令,那麼Java位元組碼指令屬於Java虛擬機器的指令,要想檢視.calss
檔案中的位元組碼指令,我們可以使用JDK提供的工具javap
進行反彙編。
Javap
反彙編示例:
Java原始碼
/**
* The class Hello.
*
* Description:反彙編、彙編測試用例
*
* @author: huangjiawei
* @since: 2018年8月14日
* @version: $Revision$ $Date$ $LastChangedBy$
*
*/
public class Hello {
private static volatile String name;
public Hello() {}
public static void say() {
for (int i = 0; i <= 1000; i++) {
System.out.println(i);
name = "huangjiawei";
System.out.println(name);
}
}
public static void main(String[] args) {
for (int i = 0; i <= 100; i++) {
say();
}
}
}
複製程式碼
執行完javac Hello.java javap -c Hello.class
之後得到下面位元組碼指令:
Compiled from "Hello.java"
public class Hello {
public Hello();// 構造方法位元組碼指令
Code:// 在x86架構中,組合語言.code標識代表指令程式碼區,.data表示資料區
0: aload_0 // 這裡指令aload_0表示將this引用壓入棧中
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void say();// say方法
Code:
0: iconst_0
1: istore_0
2: iload_0
3: sipush 1000
6: if_icmpgt 36
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
12: iload_0
13: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
16: ldc #4 // String huang
18: putstatic #5 // Field name:Ljava/lang/String;
21: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
24: getstatic #5 // Field name:Ljava/lang/String;
27: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: iinc 0, 1
33: goto 2
36: return
// main 方法指令區
public static void main(java.lang.String[]);
Code:
0: iconst_0 // int 常量i
1: istore_1
2: iload_1
3: bipush 100
5: if_icmpgt 17
8: invokestatic #7 // Method say:()V
11: iinc 1, 1
14: goto 2
17: return
}
複製程式碼
安裝HSDIS
深入學習這些位元組碼指令,對於JVM調優非常有幫助,比如說判斷槽位是否複用、逃逸分析棧上分配等等。具體的位元組碼指令學習,可以查閱《Java虛擬機器規範》一書,英語好的話,建議線上官方地址:點我就好啦!
那麼,我們瞭解了Java的位元組碼指令,我們想一想,我們開發的Java應用程式最後還不是在Linux或者Windows上面進行執行,而我們又知道,和底層硬體最接近的就是組合語言了,那麼,我們能不能將class檔案轉換成特定平臺的彙編程式碼呢?答案是肯定的。下面我將介紹幾種檢視彙編程式碼的工具及其使用。
二、Hsdis 結合 JITWatch 檢視機器彙編程式碼
HSDIS
是由Project Kenai(kenai.com/projects/ba… VM JIT編譯程式碼的反彙編外掛,作用是讓HotSpot的-XX:+PrintAssembly
指令呼叫它來把動態生成的原生程式碼還原為彙編程式碼輸出,同時還生成了大量非常有價值的註釋,這樣我們就可以通過輸出的程式碼來分析問題。
windows上進行反彙編需要hsdis-amd64.dll
這個外掛,因此我們需要生成這個外掛,然後將該外掛放置到我們的jreDir/bin/server
目錄下,然後使用-XX:+PrintAssembly
即可輸出彙編程式碼。這裡有個官方標準教程,由於是英文的,在這裡我將其中的步驟做一個簡單的總結:
-
1、安裝
Cygwin
unix模擬環境安裝的過程中記得在
select package
視窗將下面的幾個包給加上:gcc-core
mingw64-i686-gcc-core
mingw64-x86_64-gcc-core
patch
make
-
2、下載
GNU binutils 2.28
,注意官方推薦是2.30版本,但是2.30版本後期make
會有問題 -
3、下載OpenJDK,詳細見官網。
-
4,5,6步見官網描述吧!沒有坑,哈哈!
為了防止官網後面訪問不了,小編將html檔案下載儲存在github上了,詳見How to build hsdis-amd64.dll and hsdis-i386.dll on Windows
當你在命令列執行java -XX:+PrintAssembly -XX:+UnlockDiagnosticVMOptions Hello >> code.txt
就會輸出大量彙編程式碼,如下圖:
但是,有沒有這樣一種更加直觀的方式,能夠具體檢視某個方法的彙編程式碼呢?答案是肯定的,下面出場的是jitwatch
,它是一個開源專案,其github地址為:jitwatch
相對來說,jitwatch
的安裝相對比較簡單,我們可以直接克隆專案,該專案支援三種編譯方式:
- 如果使用ant編譯,請使用
ant clean compile run
執行 - 如果使用gradle編譯構建,請使用
gradlew clean build run
- 如果使用maven構建,請使用
mvn clean compile exec:java
啟動完專案之後大概就是這麼一個介面:
我們大概有兩種方式檢視我們的彙編程式碼:
- 1、使用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=code.log
指令執行,然後點選介面的Open Log
按鈕將日誌檔案匯入,再start
- 2、點選介面的
Sandbox
,配置相關引數,然後start
Java位元組碼檔案中有一個叫做行號表
的屬性,存在於Code屬性中, 它建立了位元組碼偏移量到原始碼行號之間的聯絡。我們可以點選LNT
按鈕,進行除錯:
最後,如果您正在使用JDK8,那麼您需要確保你寫的Java方法被呼叫的次數足夠多,以觸發C1(客戶端)編譯,並大約10000次觸發C2(伺服器)編譯器並開啟高階優化。換句話說,你要像檢視彙編程式碼,你寫的Java原始碼檔案不能太過於簡單,要足夠複雜,但我們第一節的Hello.java
已經足夠了,同時jitwatch本身也提供了很多學習樣例,可以在JITWatchDir\sandbox\sources
中獲得。
還記得最開始我們討論的volatile底層彙編程式碼lock指令嗎?檢視我們的彙編程式碼可以發現有這麼一行程式碼:
是吧!我們終於自己將lock指令找出來了,至於為什麼lock指令能夠保證記憶體一致性,我們首先需要從組合語言層面對lock指令的功能進行一番瞭解。在所有的 X86 CPU 上都具有鎖定一個特定記憶體地址的能力,當這個特定記憶體地址被鎖定後,它就可以阻止其他的系統匯流排讀取
或修改
這個記憶體地址。這種能力是通過 LOCK 指令字首再加上下面的彙編指令來實現的。當使用 LOCK 指令字首時,它會使 CPU 宣告一個 LOCK# 訊號,這樣就能確保在多處理器系統或多執行緒競爭的環境下互斥地使用這個記憶體地址。當指令執行完畢,這個鎖定動作也就會消失。注意由於是記憶體互斥的,因此這個臨界區除了當前lock的執行緒擁有,其他執行緒都不能進入該臨界區。
2019年8月23日更新
匯流排加鎖lock是早起CPU的一種實現方式,最新的實現採用EMSI快取一致性協議來實現VOLITALE
。
大概流程就是多個執行緒都去嗅探(監聽)匯流排上的某個變數,一旦有執行緒將變數協會主記憶體時,其他執行緒將會第一時間監聽到變數的改變,效能比lock匯流排加鎖高很多。