首先了解一下理論知識:
字節碼:
Class文件是8位字節流,按字節對齊。之所以稱為字節碼,是因為每條指令都只占據一個字節,所有的操作碼和操作數都是按字節對齊的。如:0×03表示iconst_0
Class文件的頭4個字節稱為魔數(Magic Number),它的唯一作用是用於確認該文件是否是能被JVM接受的Class文件。魔數值為:0xCAFEBABE。
緊接着魔數的4個字節是Class文件的版本號:第5和第6字節是次版本號(Minor Version),第7和第8字節是主版本號(Major Version)。Java的版本號從45開始的,JDK6的版本號是50。
javap –verbose class文件,查看字節碼內容
全限定名:把類全名中的“.”替換成“/”最后加入一個“;”表示結束。如com/test/TestClass;
描述符:基本類型及void用大寫字符表示,對象類型用字符L加對象的全限定名表示。
標識字符 | 含義 |
B | 基本類型byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 對象類型,如Ljava/lang/Object; |
對於數組類型,每一維度將使用一個前置的“[”字符來描述,如定義一個“java.lang.String[][]”類型的二維數組,將被記錄為:“[[Ljava/lang/String;”,一個整型數組“int[]”將被記錄為“[I”.
用描述符來描述方法時,按照先參數列表,后返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號“( )”之內,如方法void inc()的描述符為“( )V”,方法java.lang.String toString() 的描述符合為“( )Ljava/lang/String;”,方法int indexOf(char[] source, int sourceOffset,int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符為“([CII[CIII)I”
類構造器“<clinit>”方法,實例構造器“<init>”
JVM中最基本的數據單元是字,字長必須足夠大,至少一個字長足以持有byte、short、int等的值,2個字長足以持有long、double的值,字長可以選擇32位或者64位。
字長是CPU的主要技術指標之一,指的是CPU一次能並行處理的二進制位數,字長總是8的整數倍,通常PC機的字長為16位(早期),32位,64位。
棧幀的2個部分:局部變量區和操作數棧,是按字來定義的。當把值放入局部變量區或者操作數棧時,它將占有1個或2個字單元。
每啟動一個新線程,JVM都為它分配一個Java棧,Java棧以幀為單位保存線程的運行狀態,JVM對Java棧執行2種操作:以幀為單位的壓棧和出棧。
每當線程調用一個Java方法時,JVM會在線程的Java棧中壓入一個新幀,而這個新幀也成了當前幀,當執行這個方法時,它使用這個幀來存儲參數、局部變量、中間運算結果等數據。
Java方法有2種方式完成,一種通過return返回,一種通過拋出異常中止,不管那種方式,JVM都將當前幀彈出Java棧然后釋放掉,這樣上一個方法的幀就成為當前幀了。
Java棧上的所有數據都是此線程私有的,任何線程都不能訪問另一個線程的棧數據,因此棧數據是線程安全的。
棧幀由3部分組成:局部變量區、操作數棧、幀數據區。
局部變量區和操作數棧是以字長(32位)為單位的數組。
局部變量區包含方法的參數和局部變量,編譯器首先按把這些參數放入局部變量數組。Java棧幀的局部變量區被組織為一個以字長為單位,從0開始計數 的數組。字節碼指令通過從0開始的索引來使用其中的數據,如iload_1(把局部變量區的第2個變量壓入棧頂),byte、short、int等的值在 數組中只占據1項,而long、double的值在數組中占據連續的2項。
操作數棧:和局部變量區一樣,操作數棧也是被組織成一個以字長為單位的數組,但是它不是通過索引來訪問,而是通過標志的棧操作:壓棧和出棧來訪問的。如iadd指令,從操作數棧中彈出2個整數,執行加法運算,然后將其結果壓回操作數棧。
iload_0 //將局部變量區的第1個變量壓入棧
iload_1 //將局部變量區的第2個變量壓入棧
iadd //棧中彈出2個整數,執行加法運算,然后將其結果壓回操作數棧
istore_2 //將棧頂的整數出棧,並存入局部變量區的第3個變量
一般讀取局部變量區的數據,需要把局部變量區的變量壓入棧,
把值寫到局部變量區,也需要先壓入棧,再寫到局部變量區
幀數據區:存放常量池(要訪問的類、字段、方法名等),異常表等數據。
LineNumberTable:字節碼偏移量與源代碼之間的映射關系。
常見指令:iload_1, istore_1, iconst_1, ldc, bipush, pop, dup, iadd, isub, imul, idiv, return, goto,invoke…, new, newarray, arraylength, instanceof,athrow,monitorenter, monitorexit
然后用字節碼了解一下JVM的語法糖:
語法糖:
泛型、自動裝箱、自動拆箱、循環遍歷、變長參數、條件編譯、內部類、枚舉類、斷言語句、對枚舉的switch
類型擦除:
public class TestCls3
{//編譯失敗,因為List<String>和List<Integer>的泛型被擦除,變成原生類型List
public static void method(List<String> list)
{
System.out.println("invoke method1");
}
public static void method(List<Integer> list)
{
System.out.println("invoke method2");
}
}
public class TestCls3
{//可以執行,因為在Class文件中,只有描述符不完全一致的兩個方法就可以共存
//也就是說兩個方法如果有相同的名稱和特征簽名,但返回值不同,也是可以共存在一個Class文件的
public static String method(List<String> list)
{
System.out.println("invoke method1");
return "";
}
public static int method(List<Integer> list)
{
System.out.println("invoke method2");
return 1;
}
public static void main(String[] args)
{
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
}
javap –verbose demo.TestCls3
Constant pool:
const #17 = Asciz (Ljava/util/List<Ljava/lang/String;>;)Ljava/lang/String;
;
const #39 = Asciz (Ljava/util/List<Ljava/lang/Integer;>;)I;
public static java.lang.String method(java.util.List); //方法名擦除為List
Signature: length = 0×2
00 11 //指到常量池中的第17
public static int method(java.util.List); //方法名擦除為List
Signature: length = 0×2
00 27 //指到常量池中的第39
類型擦除,僅僅對方法的Code屬性中的字節碼進行擦除,元數據Signature還是保留了泛型數據。(Method類的signature變量)
Java的條件編譯
只有條件為常量且只有if語句才能有這種效果
public class TestCls5
{
public static void main(String[] args)
{
if (true)
{
System.out.println(“true”);
}
else
{
System.out.println(“false”);
}
}
}
編譯后的字節碼只包含:System.out.println(“true”);
public class TestCls5
{
public static void main(String[] args)
{
System.out.println(“true”);
}
}
自增++操作的線程非安全:
public class TestCls5
{
private static volatile int count;
public static void main(String[] args)
{
count++;
}
}
對應字節碼(分為4個指令,在多線程下訪問可能出現臟數據):
Code:
0: getstatic #18; //Field count:I 獲取指定類的靜態域,並將其值壓入棧頂
3: iconst_1 將整型常量1壓入棧頂
4: iadd 將棧頂的2個值出棧並相加,然后將結果入棧頂
5: putstatic #18; //Field count:I 為指定的類的靜態域賦值
8: return 方法返回
字符串的+操作(javac編譯器會對String連接做自動優化):
public String constractStr(String str1, String str2, String str3)
{
return str1 + str2 + str3;
}
對應字節碼(JDK1.5之后轉換為調用StringBuilder.append方法):
Code:
0: new #24; //class java/lang/StringBuilder
3: dup
4: aload_1
5: invokestatic #26; //Method java/lang/String.valueOf:(Ljava/lang/Objec
t;)Ljava/lang/String;
8: invokespecial #32; //Method java/lang/StringBuilder.”<init>”:(Ljava/la
ng/String;)V
11: aload_2
12: invokevirtual #35; //Method java/lang/StringBuilder.append:(Ljava/lang
/String;)Ljava/lang/StringBuilder;
15: aload_3
16: invokevirtual #35; //Method java/lang/StringBuilder.append:(Ljava/lang
/String;)Ljava/lang/StringBuilder; ――調用StringBuilder的append方法
19: invokevirtual #39; //Method java/lang/StringBuilder.toString:()Ljava/l
ang/String;
22: areturn ――返回引用
public String constractStr()
{
return “str1″ + “str2″ + “str3″;
}
對應的字節碼:
Code:
0: ldc #24; //String str1str2str3 –將字符串常量壓入棧頂
2: areturn
public String constractStr(String str3)
{
return “str1″ + “str2″ + str3;
}
對應的字節碼:
Code:
0: new #24; //class java/lang/StringBuilder
3: dup
4: ldc #26; //String str1str2 –將字符串常量str1str2壓入棧頂
6: invokespecial #28; //Method java/lang/StringBuilder.”<init>”:(Ljava/la
ng/String;)V
9: aload_1
10: invokevirtual #31; //Method java/lang/StringBuilder.append:(Ljava/lang
/String;)Ljava/lang/StringBuilder; ――調用StringBuilder的append方法
13: invokevirtual #35; //Method java/lang/StringBuilder.toString:()Ljava/l
ang/String;
16: areturn