- 棧幀(stack frame):
棧幀是一種用于幫助虛擬機執行方法調用與方法執行的數據結構,歸屬于特定的一個線程,不存在并發的問題,本身是一種數據結構,實際上封裝了方法的局部變量表、動態鏈接信息、方法的返回地址及操作數棧等信息。Java中方法的調用都存在棧幀的操作,存在入棧和出棧的操作。對于棧操作的簡單理解:
3-2=1
3、2分別做入棧的操作,-出現分別出棧并計算出結果1 做入棧操作。
局部變量表:
- 用于存儲局部變量,都是用(slot)來描述局部變量的最小單位,32位int類型的數據,都會占用一個slot 對于long類型用2個連續的slot表示。
- slot是可以復用的,對于10個局部變量可能存在的slot<10 方法體可以存在更小的作用域,方法體內部的局部變量的作用域是不同的,在局部變量表不作區分,當b、c占據的slot結束了生命周期后,可能會被d、e占據,如:
public void test(){
int a=3;
if(a>4){
int b=4;
int c=5;
}
int d=7;
int e=10;
}
動態鏈接是與C或者C++是不同的,對于C++來說Class之間的關系在編譯期間就已經確定好了,包括地址的偏移量會提前設置好,所以存在動態鏈接庫DLL Java是不同的,在編譯期間Class之間方法的調用在加載或者在真正開始調用的時候才能確定,基于上述的方面存在符號和直接引用。
- 符號引用與直接引用
符號引用:對于目標類的,比如對于一個類的全局類名的描述,存放在常量池中的。
直接引用:是符合引用的內存地址,有時候在加載(或者第一次使用)的時候符號引用轉換過來,有時候在【每次】在運行期間進行轉換。分別稱為靜態解析(綁定)及動態連接。這種動態體現為Java的多態性。
Animal a=new Cat();
a.sleep();//invokevirtual
a=new Dog();
Java方法調用的字節碼指令:(5種)
1、invokeinterface:調用接口中的方法,實際上是在運行期決定的,決定調用實現該接口的哪一個對象的特定方法。(需要定位實現類)
2、invokestatic: 調用靜態方法。
3、invokespecial: 可以自己的私有方法(注意私有方法是不可以被重寫),也可以是構造方法(<init>),也可以調用父類的方法(成員或者構造器)
4、invokevirtual: 調用虛方法(C++中是存在的),是和多態緊密相關的 也是運行期動態查找的過程 查找繼承這個類或接口的方法。
5、invokedynamic: 動態調用方法。(1.7引入的)可以調用動態語言比如javascript。不是討論的重點。
Java源文件: 用于觀察static方法的調用
public class MyTest4 {
public static void test(){
System.out.println("test invoked");
}
public static void main(String[] args) {
test();
}
}
反編譯結果:
C:\spring_lecture\target\classes\com\compass\spring_lecture\binarycode>javap -v MyTest4
警告: 二進制文件MyTest4包含com.compass.spring_lecture.binarycode.MyTest4
Classfile /C:/spring_lecture/target/classes/com/compass/spring_lecture/binarycode/MyTest4.class
Last modified 2019-7-4; size 664 bytes
MD5 checksum 123e058e19e1836c1250aa6b13b35182
Compiled from "MyTest4.java"
public class com.compass.spring_lecture.binarycode.MyTest4
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #25 // test invoked
#4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Methodref #6.#28 // com/compass/spring_lecture/binarycode/MyTest4.test:()V
#6 = Class #29 // com/compass/spring_lecture/binarycode/MyTest4
#7 = Class #30 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Lcom/compass/spring_lecture/binarycode/MyTest4;
#15 = Utf8 test
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 SourceFile
#21 = Utf8 MyTest4.java
#22 = NameAndType #8:#9 // "<init>":()V
#23 = Class #31 // java/lang/System
#24 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#25 = Utf8 test invoked
#26 = Class #34 // java/io/PrintStream
#27 = NameAndType #35:#36 // println:(Ljava/lang/String;)V
#28 = NameAndType #15:#9 // test:()V
#29 = Utf8 com/compass/spring_lecture/binarycode/MyTest4
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (Ljava/lang/String;)V
{
public com.compass.spring_lecture.binarycode.MyTest4();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/compass/spring_lecture/binarycode/MyTest4;
public static void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String test invoked
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 11: 0
line 12: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #5 // Method test:()V 通過使用invokestatic調用
3: return
LineNumberTable:
line 15: 0
line 17: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
}
方法調用的解析過程:invokestatic invokespecial是在解析的過程中就可以確定的,靜態及自身父類的構造及成員方法。
靜態解析的4種情形:
- 靜態方法
- 父類方法
- 構造方法
- 私有方法(公有方法會被重寫或者復寫 因此存在多態)
以上的4種方法就稱為非虛方法,它們是在類加載階段就可以將符號引用轉換為直接引用。
方法重載(overload): 方法的參數類型或者參數的個數不同,簽名的修飾符不算。
public class MyTest5 {
public void test(Grandpa grandpa){
System.out.println("grandpa");
}
public void test(Father father){
System.out.println("father");
}
public void test(Son son){
System.out.println("son");
}
public static void main(String[] args) {
Grandpa p1=new Father();
Grandpa p2=new Son();
MyTest5 myTest5=new MyTest5();
myTest5.test(p1);//grandpa
myTest5.test(p2);//grandpa
}
}
class Grandpa {
}
class Father extends Grandpa {
}
class Son extends Father {
}
上面的例子涉及到方法的靜態分派:
g1聲明的類型是Grandpa 是靜態類型 g1的真正指向的類型是Father(實際類型)。
我們可以得到這樣的一個結論:
變量的靜態類型是不會發生改變的,而變量的實際類型是可以發生變化的,是多態的一種體現,實際類型是在運行期間才可以確定的。
方法的重載是一種純粹靜態的一種行為,對JVM來說,是根據聲明的參數,而不是根據實際類型來決定的,是根據靜態類型來進行匹配的。是在編譯期間就可以完全確定的。
看一下的字節碼:
重載和重寫:重載和重寫是不同的,重載是靜態的,重寫是動態的。體現在靜態類型上面。
重寫的代碼示例:
public class MyTest6 {
// apple
// orange
// orange
public static void main(String[] args) {
Furit apple = new Apple();
Furit orange = new Orange();
apple.test();
orange.test();
apple = new Orange();
apple.test();
}
}
class Furit {
public void test() {
System.out.println("fruit");
}
}
class Apple extends Furit {
@Override
public void test() {
System.out.println("apple");
}
}
class Orange extends Furit {
@Override
public void test() {
System.out.println("orange");
}
}
編譯生成的字節碼:
public class com.compass.spring_lecture.binarycode.MyTest6 {
public com.compass.spring_lecture.binarycode.MyTest6();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/compass/spring_lecture/binarycode/Apple
3: dup
4: invokespecial #3 // Method com/compass/spring_lecture/binarycode/Apple."<init>":()V
7: astore_1
8: new #4 // class com/compass/spring_lecture/binarycode/Orange
11: dup
12: invokespecial #5 // Method com/compass/spring_lecture/binarycode/Orange."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method com/compass/spring_lecture/binarycode/Furit.test:()V
20: aload_2
21: invokevirtual #6 // Method com/compass/spring_lecture/binarycode/Furit.test:()V
24: new #4 // class com/compass/spring_lecture/binarycode/Orange
27: dup
28: invokespecial #5 // Method com/compass/spring_lecture/binarycode/Orange."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method com/compass/spring_lecture/binarycode/Furit.test:()V
36: return
}
方法的動態分派:
從上述的字節碼看,方法的字節碼是一致的,但是從實際的調用上看,它們是不同的。
方法的動態分派涉及到一個重要的概念:方法的接收者,也就是方法的調用者
涉及到invokevirtual字節碼指令的多態查找流程:
- 到操作數棧頂上第一個元素并尋找棧頂元素所指向的實際類型,并不是靜態類型。
- 如果獲取到并且訪問權限也是ok的,就直接調用并返回,如果獲取不到按照繼承體系進行查找,找到就調用。
比較方法重寫和方法重載,我們得到這樣一個結論,方法重載是靜態的,是編譯期行為,方法重寫是動態的,是運行期行為。
動態分派的一個最直接的例子是重寫。對于重寫,我們已經很熟悉了,那么Java虛擬機是如何在程序運行期間確定方法的執行版本的呢?
解釋這個現象,就不得不涉及Java虛擬機的invokevirtual指令了,這個指令的解析過程有助于我們更深刻理解重寫的本質。該指令的具體解析過程如下:
- 找到操作數棧棧頂的第一個元素所指向的對象的實際類型,記為C
2 . 如果在類型C中找到與常量中描述符和簡單名稱都相符的方法,則進行訪問權限的校驗,如果通過則返回這個方法的直接引用,查找結束;如果不通過,則返回非法訪問異常
如果在類型C中沒有找到,則按照繼承關系從下到上依次對C的各個父類進行第2步的搜索和驗證過程
如果始終沒有找到合適的方法,則拋出抽象方法錯誤的異常
從這個過程可以發現,在第一步的時候就在運行期確定接收對象(執行方法的所有者程稱為接受者)的實際類型,所以當調用invokevirtual指令就會把運行時常量池中符號引用解析為不同的直接引用,這就是方法重寫的本質。
虛方法表和動態分派機制:
針對于方法調用動態分派的過程:
虛擬機會在類的方法區建立一個虛方法表的數據結構(virtual method table)簡稱vtable,
針對于invokeinterface指令來說,虛擬機會建立一個接口方法表的數據結構,(interface method table),簡稱itable。