為什麼寫這這篇文章
一直有讀者問我 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
執行這個 main 函式,因為沒有加需要編譯的原始碼路徑,不出意外應該會在控制檯會輸出下面的內容
新建一個HelloWorld.java
檔案,內容隨緣,在啟動配置的Program arguments
里加入 HelloWorld.java 的絕對路徑。
再次執行 Main.java,會在 HelloWorld.java 的同級目錄生成 HelloWorld.class 檔案。
3、加斷點
在 Main.java 中打上斷點,開始除錯以後會發現不管怎麼設定,除錯都會進入tool.jar
,沒有走剛剛匯入的原始碼。
Intellij 中顯示的是反編譯 tools.jar 得到的原始碼,可讀性沒有原始碼那麼好。
開啟 Project Structure 頁面(File->Project Structure), 選中圖中 Dependencies 選項卡,把 <Moudle source>
順序調整到專案 JDK 的上面:
再次除錯就已經可以進入到專案原始碼中的斷點處了。
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 <= 13 ? tableswitch : lookupswich
int opcode = nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
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 <= 18 ? tableswitch : lookupswich
int opcode = nlabels > 0 &&
table_space_cost + 3 * table_time_cost <=
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_0
、bipush
、sipush
、ldc
,那它們是如何選擇的呢?
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() 函式加上斷點
可以看到選擇的策略依次往下:
- -1~5 之間選擇 iconst_n 的方式
- -128~127 之間選擇 bipush
- -32768~32767 之間的選擇 sipush
- 其它大整數選擇 ldc
這與 java 虛擬機器規範中位元組碼指令文件一致。
後記
用 javac 發掘很多有意思的東西,希望你能留言發現更好好玩的東東。