- 棧幀(stack frame):
棧幀是一種用于幫助虛擬機(jī)執(zhí)行方法調(diào)用與方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),歸屬于特定的一個(gè)線程,不存在并發(fā)的問(wèn)題,本身是一種數(shù)據(jù)結(jié)構(gòu),實(shí)際上封裝了方法的局部變量表、動(dòng)態(tài)鏈接信息、方法的返回地址及操作數(shù)棧等信息。Java中方法的調(diào)用都存在棧幀的操作,存在入棧和出棧的操作。對(duì)于棧操作的簡(jiǎn)單理解:
3-2=1
3、2分別做入棧的操作,-出現(xiàn)分別出棧并計(jì)算出結(jié)果1 做入棧操作。
局部變量表:
- 用于存儲(chǔ)局部變量,都是用(slot)來(lái)描述局部變量的最小單位,32位int類型的數(shù)據(jù),都會(huì)占用一個(gè)slot 對(duì)于long類型用2個(gè)連續(xù)的slot表示。
- slot是可以復(fù)用的,對(duì)于10個(gè)局部變量可能存在的slot<10 方法體可以存在更小的作用域,方法體內(nèi)部的局部變量的作用域是不同的,在局部變量表不作區(qū)分,當(dāng)b、c占據(jù)的slot結(jié)束了生命周期后,可能會(huì)被d、e占據(jù),如:
public void test(){
int a=3;
if(a>4){
int b=4;
int c=5;
}
int d=7;
int e=10;
}
動(dòng)態(tài)鏈接是與C或者C++是不同的,對(duì)于C++來(lái)說(shuō)Class之間的關(guān)系在編譯期間就已經(jīng)確定好了,包括地址的偏移量會(huì)提前設(shè)置好,所以存在動(dòng)態(tài)鏈接庫(kù)DLL Java是不同的,在編譯期間Class之間方法的調(diào)用在加載或者在真正開(kāi)始調(diào)用的時(shí)候才能確定,基于上述的方面存在符號(hào)和直接引用。
- 符號(hào)引用與直接引用
符號(hào)引用:對(duì)于目標(biāo)類的,比如對(duì)于一個(gè)類的全局類名的描述,存放在常量池中的。
直接引用:是符合引用的內(nèi)存地址,有時(shí)候在加載(或者第一次使用)的時(shí)候符號(hào)引用轉(zhuǎn)換過(guò)來(lái),有時(shí)候在【每次】在運(yùn)行期間進(jìn)行轉(zhuǎn)換。分別稱為靜態(tài)解析(綁定)及動(dòng)態(tài)連接。這種動(dòng)態(tài)體現(xiàn)為Java的多態(tài)性。
Animal a=new Cat();
a.sleep();//invokevirtual
a=new Dog();
Java方法調(diào)用的字節(jié)碼指令:(5種)
1、invokeinterface:調(diào)用接口中的方法,實(shí)際上是在運(yùn)行期決定的,決定調(diào)用實(shí)現(xiàn)該接口的哪一個(gè)對(duì)象的特定方法。(需要定位實(shí)現(xiàn)類)
2、invokestatic: 調(diào)用靜態(tài)方法。
3、invokespecial: 可以自己的私有方法(注意私有方法是不可以被重寫),也可以是構(gòu)造方法(<init>),也可以調(diào)用父類的方法(成員或者構(gòu)造器)
4、invokevirtual: 調(diào)用虛方法(C++中是存在的),是和多態(tài)緊密相關(guān)的 也是運(yùn)行期動(dòng)態(tài)查找的過(guò)程 查找繼承這個(gè)類或接口的方法。
5、invokedynamic: 動(dòng)態(tài)調(diào)用方法。(1.7引入的)可以調(diào)用動(dòng)態(tài)語(yǔ)言比如javascript。不是討論的重點(diǎn)。
Java源文件: 用于觀察static方法的調(diào)用
public class MyTest4 {
public static void test(){
System.out.println("test invoked");
}
public static void main(String[] args) {
test();
}
}
反編譯結(jié)果:
C:\spring_lecture\target\classes\com\compass\spring_lecture\binarycode>javap -v MyTest4
警告: 二進(jìn)制文件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 通過(guò)使用invokestatic調(diào)用
3: return
LineNumberTable:
line 15: 0
line 17: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
}
方法調(diào)用的解析過(guò)程:invokestatic invokespecial是在解析的過(guò)程中就可以確定的,靜態(tài)及自身父類的構(gòu)造及成員方法。
靜態(tài)解析的4種情形:
- 靜態(tài)方法
- 父類方法
- 構(gòu)造方法
- 私有方法(公有方法會(huì)被重寫或者復(fù)寫 因此存在多態(tài))
以上的4種方法就稱為非虛方法,它們是在類加載階段就可以將符號(hào)引用轉(zhuǎn)換為直接引用。
方法重載(overload): 方法的參數(shù)類型或者參數(shù)的個(gè)數(shù)不同,簽名的修飾符不算。
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 {
}
上面的例子涉及到方法的靜態(tài)分派:
g1聲明的類型是Grandpa 是靜態(tài)類型 g1的真正指向的類型是Father(實(shí)際類型)。
我們可以得到這樣的一個(gè)結(jié)論:
變量的靜態(tài)類型是不會(huì)發(fā)生改變的,而變量的實(shí)際類型是可以發(fā)生變化的,是多態(tài)的一種體現(xiàn),實(shí)際類型是在運(yùn)行期間才可以確定的。
方法的重載是一種純粹靜態(tài)的一種行為,對(duì)JVM來(lái)說(shuō),是根據(jù)聲明的參數(shù),而不是根據(jù)實(shí)際類型來(lái)決定的,是根據(jù)靜態(tài)類型來(lái)進(jìn)行匹配的。是在編譯期間就可以完全確定的。
看一下的字節(jié)碼:

重載和重寫:重載和重寫是不同的,重載是靜態(tài)的,重寫是動(dòng)態(tài)的。體現(xiàn)在靜態(tài)類型上面。
重寫的代碼示例:
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");
}
}
編譯生成的字節(jié)碼:
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
}
方法的動(dòng)態(tài)分派:
從上述的字節(jié)碼看,方法的字節(jié)碼是一致的,但是從實(shí)際的調(diào)用上看,它們是不同的。
方法的動(dòng)態(tài)分派涉及到一個(gè)重要的概念:方法的接收者,也就是方法的調(diào)用者
涉及到invokevirtual字節(jié)碼指令的多態(tài)查找流程:
- 到操作數(shù)棧頂上第一個(gè)元素并尋找棧頂元素所指向的實(shí)際類型,并不是靜態(tài)類型。
- 如果獲取到并且訪問(wèn)權(quán)限也是ok的,就直接調(diào)用并返回,如果獲取不到按照繼承體系進(jìn)行查找,找到就調(diào)用。
比較方法重寫和方法重載,我們得到這樣一個(gè)結(jié)論,方法重載是靜態(tài)的,是編譯期行為,方法重寫是動(dòng)態(tài)的,是運(yùn)行期行為。
動(dòng)態(tài)分派的一個(gè)最直接的例子是重寫。對(duì)于重寫,我們已經(jīng)很熟悉了,那么Java虛擬機(jī)是如何在程序運(yùn)行期間確定方法的執(zhí)行版本的呢?
解釋這個(gè)現(xiàn)象,就不得不涉及Java虛擬機(jī)的invokevirtual指令了,這個(gè)指令的解析過(guò)程有助于我們更深刻理解重寫的本質(zhì)。該指令的具體解析過(guò)程如下:
- 找到操作數(shù)棧棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類型,記為C
2 . 如果在類型C中找到與常量中描述符和簡(jiǎn)單名稱都相符的方法,則進(jìn)行訪問(wèn)權(quán)限的校驗(yàn),如果通過(guò)則返回這個(gè)方法的直接引用,查找結(jié)束;如果不通過(guò),則返回非法訪問(wèn)異常
如果在類型C中沒(méi)有找到,則按照繼承關(guān)系從下到上依次對(duì)C的各個(gè)父類進(jìn)行第2步的搜索和驗(yàn)證過(guò)程
如果始終沒(méi)有找到合適的方法,則拋出抽象方法錯(cuò)誤的異常
從這個(gè)過(guò)程可以發(fā)現(xiàn),在第一步的時(shí)候就在運(yùn)行期確定接收對(duì)象(執(zhí)行方法的所有者程稱為接受者)的實(shí)際類型,所以當(dāng)調(diào)用invokevirtual指令就會(huì)把運(yùn)行時(shí)常量池中符號(hào)引用解析為不同的直接引用,這就是方法重寫的本質(zhì)。
虛方法表和動(dòng)態(tài)分派機(jī)制:
針對(duì)于方法調(diào)用動(dòng)態(tài)分派的過(guò)程:
虛擬機(jī)會(huì)在類的方法區(qū)建立一個(gè)虛方法表的數(shù)據(jù)結(jié)構(gòu)(virtual method table)簡(jiǎn)稱vtable,
針對(duì)于invokeinterface指令來(lái)說(shuō),虛擬機(jī)會(huì)建立一個(gè)接口方法表的數(shù)據(jù)結(jié)構(gòu),(interface method table),簡(jiǎn)稱itable。