Javac 原始碼除錯教程

挖坑的張師傅發表於2019-12-29

為什麼寫這這篇文章

一直有讀者問我 javac 原始碼怎麼除錯,自己也在寫 JVM 掘金小冊的過程中閱讀了大量的 javac 的原始碼,網上這方面的文章也比較少,那就來寫一篇 javac 原始碼除錯的文章吧,作為 javac 系列文章的開篇。

javac 原始碼除錯的過程是比較簡單的,它本身就是一個用 Java 語言寫的,對我們理解內部邏輯比較友好。

環境搭建過程

環境備註:Intellij、JDK8

1、第一步下載匯入 javac 的原始碼

如果不想從 openjdk 下載折騰,可以跳過第 1 步直接從我的 github 下載:github.com/arthur-zhan…

OpenJDK 的下載方式為: 開啟 hg.openjdk.java.net/jdk8/jdk8/l… ,點選左側的 zip 或者 gz 進行下載。

在 Intellij 中新建一個 javac-source-code-reading 專案,把原始碼目錄的 src/share/classes/com 目錄整個拷貝到專案 src 目錄下,刪掉沒用的 javadoc 目錄。

2、找到 javac 主函式入口

程式碼在src/com/sun/tools/javac/Main.java

Javac 原始碼除錯教程

執行這個 main 函式,因為沒有加需要編譯的原始碼路徑,不出意外應該會在控制檯會輸出下面的內容

Javac 原始碼除錯教程

新建一個HelloWorld.java檔案,內容隨緣,在啟動配置的Program arguments里加入 HelloWorld.java 的絕對路徑。

Javac 原始碼除錯教程

再次執行 Main.java,會在 HelloWorld.java 的同級目錄生成 HelloWorld.class 檔案。

3、加斷點

在 Main.java 中打上斷點,開始除錯以後會發現不管怎麼設定,除錯都會進入tool.jar,沒有走剛剛匯入的原始碼。

Javac 原始碼除錯教程

Intellij 中顯示的是反編譯 tools.jar 得到的原始碼,可讀性沒有原始碼那麼好。

開啟 Project Structure 頁面(File->Project Structure), 選中圖中 Dependencies 選項卡,把 <Moudle source> 順序調整到專案 JDK 的上面:

Javac 原始碼除錯教程

再次除錯就已經可以進入到專案原始碼中的斷點處了。

javac 看位元組碼案例一:tableswitch 和 lookupswitch 選擇的策略

讀者提問,下面的程式碼編譯出的 switch-case 語句為什麼採用了 lookupswitch,而不是 tableswitch,不是說「如果 case 的值比較緊湊,中間有少量斷層或者沒有斷層,會採用 tableswitch 來實現 switch-case」嗎?

public static void foo() {
    int a = 0;
    switch (a) {
        case 0:
            System.out.println("#0");
            break;
        case 1:
            System.out.println("#1");
            break;
        default:
        System.out.println("default");
            break;
    }
}
複製程式碼

對應位元組碼

public static void foo();
 0: iconst_0
 1: istore_0
 2: iload_0
 3: lookupswitch  { // 2
               0: 28
               1: 39
         default: 50
    }
複製程式碼

這個問題比較有意思,主要是 tableswitch 和 lookupswitch 代價的估算,程式碼在 src/com/sun/tools/javac/jvm/Gen.java

在 case 值只有 0 和 1 兩個值的情況下

hi=1
lo=0
nlabels = 2

// table_space_cost = 4 + (1 - 0 + 1) = 6
long table_space_cost = 4 + ((long) hi - lo + 1); // words 

// table_time_cost = 3
long table_time_cost = 3; // comparisons

// lookup_space_cost = 3 + 2 * 2 = 7
long lookup_space_cost = 3 + 2 * (long) nlabels;

// lookup_time_cost = 2
long lookup_time_cost = nlabels;

// table_space_cost + 3 * table_time_cost  = 6 + 3 * 3 = 15
// lookup_space_cost + 3 * lookup_time_cost = 7 + 3 * 2 = 13
// opcode = 15 &lt;= 13 ? tableswitch : lookupswich

int opcode = nlabels &gt; 0 &&

table_space_cost + 3 * table_time_cost &lt;=
lookup_space_cost + 3 * lookup_time_cost

? tableswitch : lookupswitch;
複製程式碼

所以在 case 值只有 0, 1 兩個的情況下,代價的計算是 table_space_cost + 3 * table_time_cost > lookup_space_cost + 3 * lookup_time_cost,lookupswich代價更小選 lookupswich

如果有 0, 1,2 三個呢?

hi=2
lo=0
nlabels = 3

// table_space_cost = 4 + (2 - 0 + 1) = 7
long table_space_cost = 4 + ((long) hi - lo + 1); // words 

// table_time_cost = 3
long table_time_cost = 3; // comparisons

// lookup_space_cost = 3 + 2 * 3 = 9
long lookup_space_cost = 3 + 2 * (long) nlabels;

// lookup_time_cost = 3
long lookup_time_cost = nlabels;

// table_space_cost + 3 * table_time_cost  = 7 + 3 * 3 = 16
// lookup_space_cost + 3 * lookup_time_cost = 9 + 3 * 3 = 18
// opcode = 16 &lt;= 18 ? tableswitch : lookupswich

int opcode = nlabels &gt; 0 &&

table_space_cost + 3 * table_time_cost &lt;=
lookup_space_cost + 3 * lookup_time_cost

? tableswitch : lookupswitch;
複製程式碼

所以在 case 值只有 0, 1,2 三個的情況下,代價的計算是 table_space_cost + 3 * table_time_cost < lookup_space_cost + 3 * lookup_time_cost,tableswitch 代價更小選 tableswitch

其實在數量極少的情況下,兩個的差別不大,只是 javac 這裡的演算法導致選擇了 lookupswitch

javac 看位元組碼案例二:載入整數到棧上的位元組碼指令選擇

我們知道有很多指令可以把整數載入到棧上,比如iconst_0bipushsipushldc,那它們是如何選擇的呢?

public static void foo() {
        int a = 0;
        int b = 6;
        int c = 130;
        int d = 33000;
}

對應部分位元組碼
 0: iconst_0
 1: istore_0
 2: bipush        6
 4: istore_1
 5: sipush        130
 8: istore_2
 9: ldc           #2                  // int 33000
11: istore_3
複製程式碼

com/sun/tools/javac/jvm/Items.java 的 load() 函式加上斷點

Javac 原始碼除錯教程

可以看到選擇的策略依次往下:

  • -1~5 之間選擇 iconst_n 的方式
  • -128~127 之間選擇 bipush
  • -32768~32767 之間的選擇 sipush
  • 其它大整數選擇 ldc

這與 java 虛擬機器規範中位元組碼指令文件一致。

後記

用 javac 發掘很多有意思的東西,希望你能留言發現更好好玩的東東。

相關文章