Java 反彙編、反編譯、volitale解讀

曾經小小少年,到現在風度翩翩!曾幾什麼時候,每次想了解Java中volatile關鍵字的實現原理時,小編都會去百度找博客看,翻遍了許許多多的博客,有講的深刻的,有講的淺顯的,反正小編腦子是有點亂了。其中不少博客講到其底層是給變量加了一條lock指令,真的是這樣的嗎?確實是。html

下面咱們就來驗證下到底這個lock指令是如何得出來的,以及介紹下查看windows字節碼的相關工具的使用,因爲小編看得懂的字節碼指令寥寥無幾,所以,暫時還不能每條指令具體分析,留到下篇博客介紹。java

1、Java字節碼及class文件反彙編

咱們都知道,Java源代碼文件想要執行,會被編譯器(javac)編譯爲.class文件,Java字節碼文件具有了相應的格式,並且很是嚴格(具體的class文件格式,能夠查閱《Java虛擬機規範》)。因爲.class文件爲二進制文件,所以咱們沒法直接使用文本文件打開查看,若是要打開,咱們可使用諸如Java Decompiler這類的工具來反編譯.class文件。Java虛擬機與傳統彙編語言不一樣,它不直接使用底層的寄存器,而是設計成一臺基於棧的虛擬機,在Java方法中,前面指令的執行結果先push進操做數棧,後面的指令若是須要使用到先前的結果,則從操做數棧中將值pop出來。而這些操做,底層Java虛擬器在讀取存儲出棧入棧等方面擁有許許多多的字節碼指令支持,咱們能夠這樣子理解,若是說彙編指令屬於底層操做系統指令,那麼Java字節碼指令屬於Java虛擬機的指令,要想查看.calss文件中的字節碼指令,咱們可使用JDK提供的工具javap進行反彙編。nginx

Javap反彙編示例:git

Java源代碼github

/**
 * 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以後獲得下面字節碼指令:windows

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文件轉換成特定平臺的彙編代碼呢?答案是確定的。下面我將介紹幾種查看彙編代碼的工具及其使用。bash

2、Hsdis 結合 JITWatch 查看機器彙編代碼

HSDIS是由Project Kenai(kenai.com/projects/ba… VM JIT編譯代碼的反彙編插件,做用是讓HotSpot的-XX:+PrintAssembly指令調用它來把動態生成的本地代碼還原爲彙編代碼輸出,同時還生成了大量很是有價值的註釋,這樣咱們就能夠經過輸出的代碼來分析問題。服務器

windows上進行反彙編須要hsdis-amd64.dll這個插件,所以咱們須要生成這個插件,而後將該插件放置到咱們的jreDir/bin/server目錄下,而後使用-XX:+PrintAssembly便可輸出彙編代碼。這裏有個官方標準教程,因爲是英文的,在這裏我將其中的步驟作一個簡單的總結:多線程

  • 一、安裝Cygwinunix模擬環境

    安裝的過程當中記得在select package窗口將下面的幾個包給加上:

    • gcc-core
    • mingw64-i686-gcc-core
    • mingw64-x86_64-gcc-core
    • patch
    • make
  • 二、下載GNU binutils 2.28,注意官方推薦是2.30版本,可是2.30版本後期make會有問題

  • 三、下載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

啓動完項目以後大概就是這麼一個界面:

咱們大概有兩種方式查看咱們的彙編代碼:

  • 一、使用-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation -XX:LogFile=code.log指令執行,而後點擊界面的Open Log按鈕將日誌文件導入,再start
  • 二、點擊界面的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總線加鎖高不少。

相關文章
相關標籤/搜索