一直有讀者問我 javac 源碼怎麼調試,本身也在寫 JVM 掘金小冊的過程當中閱讀了大量的 javac 的源碼,網上這方面的文章也比較少,那就來寫一篇 javac 源碼調試的文章吧,做爲 javac 系列文章的開篇。java
javac 源碼調試的過程是比較簡單的,它自己就是一個用 Java 語言寫的,對咱們理解內部邏輯比較友好。git
環境備註:Intellij、JDK8github
一、第一步下載導入 javac 的源碼算法
若是不想從 openjdk 下載折騰,能夠跳過第 1 步直接從個人 github 下載:github.com/arthur-zhan…bash
OpenJDK 的下載方式爲: 打開 hg.openjdk.java.net/jdk8/jdk8/l… ,點擊左側的 zip 或者 gz 進行下載。jvm
在 Intellij 中新建一個 javac-source-code-reading 項目,把源碼目錄的 src/share/classes/com 目錄整個拷貝到項目 src 目錄下,刪掉沒用的 javadoc 目錄。函數
二、找到 javac 主函數入口spa
代碼在src/com/sun/tools/javac/Main.java
.net
運行這個 main 函數,由於沒有加須要編譯的源代碼路徑,不出意外應該會在控制檯會輸出下面的內容調試
新建一個HelloWorld.java
文件,內容隨緣,在啓動配置的Program arguments
里加入 HelloWorld.java 的絕對路徑。
再次運行 Main.java,會在 HelloWorld.java 的同級目錄生成 HelloWorld.class 文件。
三、加斷點
在 Main.java 中打上斷點,開始調試之後會發現無論怎麼設置,調試都會進入tool.jar
,沒有走剛剛導入的源碼。
Intellij 中顯示的是反編譯 tools.jar 獲得的源碼,可讀性沒有源碼那麼好。
打開 Project Structure 頁面(File->Project Structure), 選中圖中 Dependencies 選項卡,把 <Moudle source>
順序調整到項目 JDK 的上面:
再次調試就已經能夠進入到項目源碼中的斷點處了。
讀者提問,下面的代碼編譯出的 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
咱們知道有不少指令能夠把整數加載到棧上,好比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() 函數加上斷點
能夠看到選擇的策略依次往下:
這與 java 虛擬機規範中字節碼指令文檔一致。
用 javac 發掘不少有意思的東西,但願你能留言發現更好好玩的東東。