文章目錄
前一章講述了java字節碼文件的生成以及字節碼文件中各個字段代表的含義,在本章節將講述字節碼是什麼運行的
JVM的一些基礎概念
要理解java字節碼的運行情況,首先要了解有關JVM的一些知識,這些是java字節碼運行的先決條件。
JVM數據類型
Java是靜態類型的,它會影響字節碼指令的設計,這樣指令就會期望自己對特定類型的值進行操作。例如,就會有好幾個add指令用於兩個數字相加:iadd、ladd、fadd、dadd。他們期望類型的操作數分別是int、long、float和double。大多數字節碼都有這樣的特性,它具有不同形式的相同功能,這取決於操作數類型。
JVM定義的數據類型包括:
- 基本類型:
- 數值類型: byte (8位), short (16位), int (32位), long (64-bit位), char (16位無符號Unicode), float(32-bit IEEE 754 單精度浮點型), double (64-bit IEEE 754 雙精度浮點型)
- 布爾類型
- 指針類型: 指令指針。
- 引用類型:
- 類
- 數組
- 接口
在字節碼中布爾類型的支持是受限的。舉例來說,沒有結構能直接操作布爾值。布爾值被替換轉換成 int 是通過編譯器來進行的,並且最終還是被轉換成 int 結構。Java 開發者應該熟悉所有上面的類型,除了 returnAddress,它沒有等價的編程語言類型。類數組接口在字節碼中布爾類型的支持是受限的。舉例來說,沒有結構能直接操作布爾值。布爾值被替換轉換成 int 是通過編譯器來進行的,並且最終還是被轉換成 int 結構。
Java 開發者應該熟悉所有上面的類型,除了 returnAddress,它沒有等價的編程語言類型。
JVM的內存結構
JVM的內存分佈如上圖所示。方法區和堆是線程共享的,而寄存器、java方法棧、本地方法棧是各個線程私有的。
1.方法區
方法區是用來存儲已被JVM加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
這個區域很少進行垃圾回收,回收目標主要是針對常量池的回收和對類型的卸載。
2.堆
此區域唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存.
3.PC寄存器
程序計數器是一塊較小的內存空間,線程私有。它可以看作是當前線程所執行的字節碼的行號指示器
4. Java方法棧和本地方法棧
JVM棧描述的是java方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息.
Java字節碼的運行就是在JVM方法棧中進行的
Java字節碼運行過程
簡單的示例
1.示例源碼
先來看我們的例子代碼,源碼如下:
public class Test{
public static void main(String[] args){
Integer a = 1;
Integer b = 2;
Integer c = a + b;
}
}
2.main函數的字節碼展示
使用javac Test.java
進行編譯,然後使用javap -v Test.class
查看該java文件的字節碼,爲了排除干擾,去除了很多不必要的字節碼
*** 省略部分字節碼
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Methodref #15.#16 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Methodref #15.#17 // java/lang/Integer.intValue:()I
*** 省略部分字節碼
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: iconst_2
6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: aload_1
11: invokevirtual #3 // Method java/lang/Integer.intValue:()I
14: aload_2
15: invokevirtual #3 // Method java/lang/Integer.intValue:()I
18: iadd
19: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: astore_3
23: return
3.字節碼指令運行過程
接下來分析Code
中字節碼運行的過程。這裏說一下,每個指令前的數字爲指令在寄存器中的偏移量。
0: iconst_1
將int常量1進行放入操作數棧。這裏稍微做個拓展,如果將float常量2進行入棧操作,name該指令是fconst_2
,詳細的指令種類及意義請查看下一章 Java字節碼指令詳解。
1: invokestatic #2
調用常量池中序號爲#2
的靜態方法,這裏調用的是 Integer.valueOf()方法,表示將該int類型進行裝箱操作,變爲Integer類型
4: astore_1
在索引爲1的位置將第一個操作數出棧(一個Integer值)並且將其存進本地變量,相當於變量a。
5: iconst_2
將int常量2進行放入操作數棧
6: invokestatic #2
調用常量池中序號爲#2
的靜態方法,這裏調用的是 Integer.valueOf()方法,表示將該int類型進行裝箱操作,變爲Integer類型
9: astore_2
在索引爲2的位置將第一個操作數出棧(一個Integer值)並且將其存進本地變量,相當於變量b。
10: aload_1
從索引1的本地變量中加載一個int值,放入操作數棧
11: invokevirtual #3
調用常量池中序號爲#3
的實例方法,這裏調用的是 Integer.intValue()方法
14: aload_2
從索引1的本地變量中加載一個int值,放入操作數棧
15: invokevirtual #3
調用常量池中序號爲#3
的實例方法,這裏調用的是 Integer.intValue()方法
18: iadd
把操作數棧中的前兩個int值出棧並相加,將相加的結果放入操作數棧。
19: invokestatic #2
調用常量池中序號爲#2
的靜態方法,這裏調用的是 Integer.valueOf()方法
22: astore_3
在索引爲3的位置將第一個操作數出棧(一個Integer值)並且將其存進本地變量,相當於變量c。
23: return
方法結束
方法調用
上面的示例是比較簡單的,而且只有一個main函數,接下來將展示在多個函數時候字節碼的形式以及運行的具體過程。這裏就直接拿參考文章的示例,原文寫得真的很好,有條件可以去看英文原文。 字節碼的介紹
1.示例源碼
public class Test{
public static void main(String[] args){
int a = 1;
int b = 2;
int c = calc(1,2);
}
static int calc(int a,int b){
return (int) Math.sqrt(Math.pow(a,2)+Math.pow(b,2));
}
}
2.字節碼展示
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iconst_1
5: iconst_2
6: invokestatic #2 // Method calc:(II)I
9: istore_3
10: return
static int calc(int, int);
descriptor: (II)I
flags: ACC_STATIC
Code:
stack=6, locals=2, args_size=2
0: iload_0
1: i2d
2: ldc2_w #3 // double 2.0d
5: invokestatic #5 // Method java/lang/Math.pow:(DD)D
8: iload_1
9: i2d
10: ldc2_w #3 // double 2.0d
13: invokestatic #5 // Method java/lang/Math.pow:(DD)D
16: dadd
17: invokestatic #6 // Method java/lang/Math.sqrt:(D)D
20: d2i
21: ireturn
3. 指令執行過程詳解
上面就是main方法和calc方法的字節碼,由於main方法的指令跟上個例子很相似,唯一不同的是 c=a+b
變爲由calc方法去執行並且返回。這裏就不再贅述main方法,接下來主要講解calc方法的執行過程。
0: iload_0
將方法中第一個參數入棧
1: i2d
將int類型轉爲double類型
2: ldc2_w #3
將常量池序號爲#3
的long型常量從常量池推送至棧頂(寬索引)
5: invokestatic #5
調用靜態方法:Math.pow:() ,並且將結果放入棧頂
8: iload_1
9: i2d
10: ldc2_w #3
13: invokestatic #5
以上的指令跟上一個一樣,進行平方運算
16: dadd
將result和result2相加,並推入棧頂
17: invokestatic #6
調用Math.sqrt()方法
20: d2i
將double類型轉爲int類型
21: ireturn
返回int類型的數值
實例調用
修改上面的代碼,加入對象,並調用對象的方法。
public class Test {
public static void main(String[] args){
Point a =new Point (1,2);
Point b = new Point (3,4);
int c = a.area(b);
}
static class Point{
private int x;
private int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
public int area(Point p){
int length = Math.abs(p.y-this.y);
int width = Math.abs(p.x-this.x);
return length*width;
}
}
}
使用javap -v Test
查看編譯後的字節碼:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=1
0: new #2 // class Test3$Point
3: dup
4: iconst_1
5: iconst_2
6: invokespecial #3 // Method Test3$Point."<init>":(II)V
9: astore_1
10: new #2 // class Test3$Point
13: dup
14: iconst_3
15: iconst_4
16: invokespecial #3 // Method Test3$Point."<init>":(II)V
19: astore_2
20: aload_1
21: aload_2
22: invokevirtual #4 // Method Test3$Point.area:(LTest3$Point;)I
25: istore_3
26: return
這個main方法比上一個例子多了幾個新的指令:new
,dup
,invokespecial
-
new
new 指令與編程語言中的 new 運算符類似,它根據傳入的操作數所指定類型來創建對象(這是對 Point 類的符號引用)。 -
dup
dup指令會複製頂部操作數的棧值,這意味着現在我們在棧頂部有兩個指向Point對象的引用。
-
iconst_1
,iconst_2
,invokespecial
,將x,y的值(1,2)壓入棧頂,接下來進行Point初始化工作,將x,y的值進行賦值。初始化完成後會將棧頂的三個操作引用銷燬,只留下最初的Point的對象引用。
-
astore_1
將該Point引用出棧,並將其賦值到索引1所保存的本地變量(astore_1中的a表明這是一個引用值)
接下來進行第二個Point實例的初始化和賦值操作
20: aload_1
,21: aload_2
將a,b的Point實例的引用入棧22: invokevirtual #4
調用area
方法,25: istore_3
將返回值放入索引3中(即賦值給c)return
方法結束
總結
本章節寫了字節碼運行的詳細過程,詳細的指令介紹在下一章,有興趣可以看看。
參考文章:
字節碼的介紹