文件網址
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
虛擬機器棧出現的背景
記憶體中的棧與堆
首先棧是執行時的單位,而堆是儲存的單位
虛擬機器棧的基本內容
Java虛擬機器棧是什麼?
Java虛擬機器棧(Java Virtual Machine Stack),早期也叫Java棧。每個執行緒在建立時都會建立一個虛擬機器棧,其內部儲存一個個的棧幀(Stack Frame),對應着一次次的Java方法呼叫,棧是執行緒私有的
虛擬機器棧的生命週期
生命週期和執行緒一致,也就是執行緒結束了,該虛擬機器棧也銷燬了
虛擬機器棧的作用
主管Java程式的執行,它儲存方法的區域性變數(8 種基本數據型別、物件的參照地址)、部分結果,並參與方法的呼叫和返回。
棧的特點
棧是一種快速有效的分配儲存方式,存取速度僅次於程式計數器。JVM直接對Java棧的操作只有兩個:
對於棧來說不存在垃圾回收問題(棧存在溢位的情況)
棧中可能出現的異常
面試題:棧中可能出現的異常
Java 虛擬機器規範允許Java棧的大小是動態的或者是固定不變的。
如果採用固定大小的Java虛擬機器棧,那每一個執行緒的Java虛擬機器棧容量可以線上程建立的時候獨立選定。
如果執行緒請求分配的棧容量超過Java虛擬機器棧允許的最大容量,Java虛擬機器將會拋出一個StackoverflowError 異常。
如果Java虛擬機器棧可以動態擴充套件,並且在嘗試擴充套件的時候無法申請到足夠的記憶體,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的虛擬機器棧,那Java虛擬機器將會拋出一個 OutofMemoryError 異常。
棧異常演示
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
設定棧記憶體的大小
-Xss1024m // 棧記憶體爲 1024MBS
-Xss1024k // 棧記憶體爲 1024KB
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
棧儲存什麼?
棧的執行原理
JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循先進後出(後進先出)原則
在一條活動執行緒中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的
執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作。
如果在該方法中呼叫了其他方法,對應的新的棧幀會被建立出來,放在棧的頂端,成爲新的當前幀。
不同線程中所包含的棧幀是不允許存在相互參照的,即不可能在一個棧幀之中參照另外一個執行緒的棧幀。
如果當前方法呼叫了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機器會丟棄當前棧幀,使得前一個棧幀重新成爲當前棧幀。
Java方法有兩種返回函數的方式,但不管使用哪種方式,都會導致棧幀被彈出
程式碼範例:
public class StackFrameTest {
public static void main(String[] args) {
StackFrameTest test = new StackFrameTest();
test.method1();
}
public void method1() {
System.out.println("method1()開始執行...");
method2();
System.out.println("method1()執行結束...");
}
public int method2() {
System.out.println("method2()開始執行...");
int i = 10;
int m = (int) method3();
System.out.println("method2()即將結束...");
return i + m;
}
public double method3() {
System.out.println("method3()開始執行...");
double j = 20.0;
System.out.println("method3()即將結束...");
return j;
}
}
method1()開始執行...
method2()開始執行...
method3()開始執行...
method3()即將結束...
method2()即將結束...
method1()執行結束...
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String method1()開始執行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #8 // Method method2:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #9 // String method1()執行結束...
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
line 19: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/atguigu/java1/StackFrameTest;
public int method2();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String method2()開始執行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: bipush 10
10: istore_1
11: aload_0
12: invokevirtual #11 // Method method3:()D
15: d2i
16: istore_2
17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #12 // String method2()即將結束...
22: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: iload_1
26: iload_2
27: iadd
28: ireturn
LineNumberTable:
line 22: 0
line 23: 8
line 24: 11
line 25: 17
line 26: 25
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/atguigu/java1/StackFrameTest;
11 18 1 i I
17 12 2 m I
public double method3();
descriptor: ()D
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String method3()開始執行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: ldc2_w #14 // double 20.0d
11: dstore_1
12: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #16 // String method3()即將結束...
17: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: dload_1
21: dreturn
LineNumberTable:
line 30: 0
line 31: 8
line 32: 12
line 33: 20
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/atguigu/java1/StackFrameTest;
12 10 1 j D
棧幀內部結構
每個棧幀中儲存着:
並行每個執行緒下的棧都是私有的,因此每個執行緒都有自己各自的棧,並且每個棧裏面都有很多棧幀,棧幀的大小主要由區域性變數表 和 運算元棧決定的
認識區域性變數表
區域性變數表所需的容量大小是在編譯期確定下來的
public class LocalVariablesTest {
private int count = 0;
public static void main(String[] args) {
LocalVariablesTest test = new LocalVariablesTest();
int num = 10;
test.test1();
}
public void test1() {
Date date = new Date();
String name1 = "atguigu.com";
test2(date, name1);
System.out.println(date + name1);
}
public String test2(Date dateP, String name2) {
dateP = null;
name2 = "songhongkang";
double weight = 130.5;//佔據兩個slot
char gender = '男';
return dateP + name2;
}
}
思考:
public static void main(String[] args) {
if(args == null){
LocalVariablesTest test = new LocalVariablesTest();
}
int num = 10;
}
位元組碼中方法內部結構的剖析
關於 Slot 的理解
參數值的存放總是從區域性變數陣列索引 0 的位置開始,到陣列長度-1的索引結束。
區域性變數表,最基本的儲存單元是Slot(變數槽),區域性變數表中存放編譯期可知的各種基本數據型別(8種),參照型別(reference),returnAddress型別的變數。
在區域性變數表裏,32位元以內的型別只佔用一個slot(包括returnAddress型別),64位元的型別佔用兩個slot(1ong和double)。
JVM會爲區域性變數表中的每一個Slot都分配一個存取索引,通過這個索引即可成功存取到區域性變數表中指定的區域性變數值
當一個實體方法被呼叫的時候,它的方法參數和方法體內部定義的區域性變數將會按照順序被複制到區域性變數表中的每一個slot上
如果需要存取區域性變數表中一個64bit的區域性變數值時,只需要使用前一個索引即可。(比如:存取long或doub1e型別變數)
如果當前幀是由構造方法或者實體方法建立的,那麼該物件參照this將會存放在index爲0的slot處,其餘的參數按照參數表順序繼續排列。
Slot 程式碼範例
this 存放在 index = 0 的位置:
public void test3() {
this.count++;
}
64位元的型別(1ong和double)佔用兩個slot
public String test2(Date dateP, String name2) {
dateP = null;
name2 = "songhongkang";
double weight = 130.5;//佔據兩個slot
char gender = '男';
return dateP + name2;
}
static 無法呼叫 this
//練習:
public static void testStatic(){
LocalVariablesTest test = new LocalVariablesTest();
Date date = new Date();
int count = 10;
System.out.println(count);
//因爲 this 變數不存在於當前方法的區域性變數表中!!
//System.out.println(this.count);
}
Slot 的重複利用
棧幀中的區域性變數表中的槽位是可以重用的,如果一個區域性變數過了其作用域,那麼在其作用域之後申明新的區域性變數變就很有可能會複用過期區域性變數的槽位,從而達到節省資源的目的。
public void test4() {
int a = 0;
{
int b = 0;
b = a + 1;
}
//變數c使用之前已經銷燬的變數b佔據的slot的位置
int c = a + 1;
}
靜態變數與區域性變數的對比
變數的分類:
程式碼範例
補充說明
在棧幀中,與效能調優關係最爲密切的部分就是前面提到的區域性變數表。在方法執行時,虛擬機器使用區域性變數表完成方法的傳遞。
區域性變數表中的變數也是重要的垃圾回收根節點,只要被區域性變數表中直接或間接參照的物件都不會被回收。
運算元棧的特點
運算元棧:Operand Stack
每一個獨立的棧幀除了包含區域性變數表以外,還包含一個後進先出(Last - In - First -Out)的 運算元棧,也可以稱之爲表達式棧(Expression Stack)
運算元棧,在方法執行過程中,根據位元組碼指令,往棧中寫入數據或提取數據,即入棧(push)和 出棧(pop)
某些位元組碼指令將值壓入運算元棧,其餘的位元組碼指令將運算元取出棧。使用它們後再把結果壓入棧,比如:執行復制、交換、求和等操作
程式碼舉例
運算元棧的作用
運算元棧,主要用於儲存計算過程的中間結果,同時作爲計算過程中變數臨時的儲存空間。
運算元棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被建立出來,這時方法的運算元棧是空的(這個時候陣列是有長度的,只是運算元棧爲空)
每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯期就定義好了,儲存在方法的Code屬性中,爲maxstack的值。
棧中的任何一個元素都是可以任意的Java數據型別
運算元棧並非採用存取索引的方式來進行數據存取的,而是只能通過標準的入棧和出棧操作來完成一次數據存取
如果被呼叫的方法帶有返回值的話,其返回值將會被壓入當前棧幀的運算元棧中,並更新PC暫存器中下一條需要執行的位元組碼指令。
運算元棧中元素的數據型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類載入過程中的類檢驗階段的數據流分析階段要再次驗證。
另外,我們說Java虛擬機器的解釋引擎是基於棧的執行引擎,其中的棧指的就是運算元棧。
運算元棧的深度
通過反編譯生成的位元組碼指令檢視運算元棧的深度
運算元棧程式碼追蹤
public void testAddOperation() {
//byte、short、char、boolean:都以int型來儲存
byte i = 15;
int j = 8;
int k = i + j;
}
0 bipush 15
2 istore_1
3 bipush 8
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return
程式執行流程如下
然後PC+1,指向的是下一行。讓運算元8也入棧,同時執行store操作,存入區域性變數表中
然後從區域性變數表中,依次將數據放在運算元棧中,等待執行 add 操作
關於 int j = 8; 的說明
關於呼叫方法,返回值入運算元棧的說明
public int getSum(){
int m = 10;
int n = 20;
int k = m + n;
return k;
}
public void testGetSum(){
//獲取上一個棧楨返回的結果,並儲存在運算元棧中
int i = getSum();
int j = 10;
}
++i 與 i++ 的區別
// 程式設計師面試過程中, 常見的i++和++i 的區別,放到位元組碼篇章時再介紹。
public void add(){
//第1類問題:
int i1 = 10;
i1++;
int i2 = 10;
++i2;
//第2類問題:
int i3 = 10;
int i4 = i3++;
int i5 = 10;
int i6 = ++i5;
//第3類問題:
int i7 = 10;
i7 = i7++;
int i8 = 10;
i8 = ++i8;
//第4類問題:
int i9 = 10;
int i10 = i9++ + ++i9;
}
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 bipush 10
8 istore_2
9 iinc 2 by 1
12 bipush 10
14 istore_3
15 iload_3
16 iinc 3 by 1
19 istore 4
21 bipush 10
23 istore 5
25 iinc 5 by 1
28 iload 5
30 istore 6
32 bipush 10
34 istore 7
36 iload 7
38 iinc 7 by 1
41 istore 7
43 bipush 10
45 istore 8
47 iinc 8 by 1
50 iload 8
52 istore 8
54 bipush 10
56 istore 9
58 iload 9
60 iinc 9 by 1
63 iinc 9 by 1
66 iload 9
68 iadd
69 istore 10
71 return
i++
//第2類問題:
int i3 = 10;
int i4 = i3++;
12 bipush 10
14 istore_3
15 iload_3
16 iinc 3 by 1
19 istore 4
++i
int i5 = 10;
int i6 = ++i5;
21 bipush 10
23 istore 5
25 iinc 5 by 1
28 iload 5
30 istore 6
總結:
棧頂快取技術:Top Of Stack Cashing
前面提過,基於棧式架構的虛擬機器所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味着將需要更多的指令分派(instruction dispatch)次數和記憶體讀/寫次數。
由於運算元是儲存在記憶體中的,因此頻繁地執行記憶體讀/寫操作必然會影響執行速度。爲了解決這個問題,HotSpot JVM的設計者們提出了棧頂快取(Tos,Top-of-Stack Cashing)技術,將棧頂元素全部快取在物理CPU的暫存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率。
暫存器的主要優點:指令更少,執行速度快
動態鏈接(或指向執行時常數池的方法參照)
動態鏈接:Dynamic Linking
每一個棧幀內部都包含一個指向執行時常數池中該棧幀所屬方法的參照
包含這個參照的目的就是爲了支援當前方法的程式碼能夠實現動態鏈接(Dynamic Linking),比如:invokedynamic指令
在Java原始檔被編譯到位元組碼檔案中時,所有的變數和方法參照都作爲符號參照(Symbolic Reference)儲存在class檔案的常數池裏
比如:描述一個方法呼叫了另外的其他方法時,就是通過常數池中指向方法的符號參照來表示的,那麼動態鏈接的作用就是爲了將這些符號參照轉換爲呼叫方法的直接參照
程式碼範例
public class DynamicLinkingTest {
int num = 10;
public void methodA(){
System.out.println("methodA()....");
}
public void methodB(){
System.out.println("methodB()....");
methodA();
num++;
}
}
public void methodB();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String methodB()....
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #7 // Method methodA:()V
12: aload_0
13: dup
14: getfield #2 // Field num:I
17: iconst_1
18: iadd
19: putfield #2 // Field num:I
22: return
LineNumberTable:
line 16: 0
line 18: 8
line 20: 12
line 21: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/atguigu/java1/DynamicLinkingTest;
#7 = Methodref #8.#31
#8 = Class #32
:去找 #32#32 = Utf8 com/atguigu/java1/DynamicLinkingTest
DynamicLinkingTest
這個類#31 = NameAndType #19:#13
:去找 #19 和 #13#19 = Utf8 methodA
:方法名爲 methodA#13 = Utf8 ()V
:方法沒有形參,返回值爲 voidConstant pool:
#1 = Methodref #9.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#24 // com/atguigu/java1/DynamicLinkingTest.num:I
#3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #27 // methodA()....
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = String #30 // methodB()....
#7 = Methodref #8.#31 // com/atguigu/java1/DynamicLinkingTest.methodA:()V
#8 = Class #32 // com/atguigu/java1/DynamicLinkingTest
#9 = Class #33 // java/lang/Object
#10 = Utf8 num
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/atguigu/java1/DynamicLinkingTest;
#19 = Utf8 methodA
#20 = Utf8 methodB
#21 = Utf8 SourceFile
#22 = Utf8 DynamicLinkingTest.java
#23 = NameAndType #12:#13 // "<init>":()V
#24 = NameAndType #10:#11 // num:I
#25 = Class #34 // java/lang/System
#26 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#27 = Utf8 methodA()....
#28 = Class #37 // java/io/PrintStream
#29 = NameAndType #38:#39 // println:(Ljava/lang/String;)V
#30 = Utf8 methodB()....
#31 = NameAndType #19:#13 // methodA:()V
#32 = Utf8 com/atguigu/java1/DynamicLinkingTest
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (Ljava/lang/String;)V
爲什麼要用常數池呢?
因爲在不同的方法,都可能呼叫常數或者方法,所以只需要儲存一份即可,然後記錄其參照即可,節省了空間
常數池的作用:就是爲了提供一些符號和常數,便於指令的識別
靜態鏈接機制 機製與動態鏈接機制 機製
在JVM中,將符號參照轉換爲呼叫方法的直接參照與方法的系結機制 機製相關
靜態鏈接:
當一個位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯期確定,且執行期保持不變時,這種情況下將呼叫方法的符號參照轉換爲直接參照的過程稱之爲靜態鏈接
動態鏈接:
如果被呼叫的方法在編譯期無法被確定下來,也就是說,只能夠在程式執行期將呼叫的方法的符號轉換爲直接參照,由於這種參照轉換過程具備動態性,因此也被稱之爲動態鏈接。
方法的系結機制 機製
靜態鏈接和動態鏈接對應的方法的系結機制 機製爲:早期系結(Early Binding)和晚期系結(Late Binding)。系結是一個欄位、方法或者類在符號參照被替換爲直接參照的過程,這僅僅發生一次。
早期系結
早期系結就是指被呼叫的目標方法如果在編譯期可知,且執行期保持不變時,即可將這個方法與所屬的型別進行系結,這樣一來,由於明確了被呼叫的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號參照轉換爲直接參照。
晚期系結
如果被呼叫的方法在編譯期無法被確定下來,只能夠在程式執行期根據實際的型別系結相關的方法,這種系結方式也就被稱之爲晚期系結。
程式碼範例
/**
* 說明早期系結和晚期系結的例子
*
* @author shkstart
* @create 2020 上午 11:59
*/
class Animal {
public void eat() {
System.out.println("動物進食");
}
}
interface Huntable {
void hunt();
}
class Dog extends Animal implements Huntable {
@Override
public void eat() {
System.out.println("狗吃骨頭");
}
@Override
public void hunt() {
System.out.println("捕食耗子,多管閒事");
}
}
class Cat extends Animal implements Huntable {
public Cat() {
super();//表現爲:早期系結
}
public Cat(String name) {
this();//表現爲:早期系結
}
@Override
public void eat() {
super.eat();//表現爲:早期系結
System.out.println("貓吃魚");
}
@Override
public void hunt() {
System.out.println("捕食耗子,天經地義");
}
}
public class AnimalTest {
public void showAnimal(Animal animal) {
animal.eat();//表現爲:晚期系結
}
public void showHunt(Huntable h) {
h.hunt();//表現爲:晚期系結
}
}
多型性與方法系結機制 機製
隨着高階語言的橫空出世,類似於Java一樣的基於物件導向的程式語言如今越來越多,儘管這類程式語言在語法風格上存在一定的差別,但是它們彼此之間始終保持着一個共性,那就是都支援封裝、繼承和多型等物件導向特性,既然這一類的程式語言具備多型特性,那麼自然也就具備早期系結和晚期系結兩種系結方式。
Java中任何一個普通的方法其實都具備虛擬函式的特徵,它們相當於C++語言中的虛擬函式(C++中則需要使用關鍵字virtual來顯式定義)。如果在Java程式中不希望某個方法擁有虛擬函式的特徵時,則可以使用關鍵字final來標記這個方法。
虛方法與非虛方法
虛方法與非虛方法的區別
子類物件的多型的使用前提:
虛擬機器中呼叫方法的指令
四條普通指令:
<init>
方法、私有及父類別方法,解析階段確定唯一方法版本一條動態呼叫指令
invokedynamic:動態解析出需要呼叫的方法,然後執行
區別
程式碼範例:
/**
* 解析呼叫中非虛方法、虛方法的測試
*
* invokestatic指令和invokespecial指令呼叫的方法稱爲非虛方法
* @author shkstart
* @create 2020 下午 12:07
*/
class Father {
public Father() {
System.out.println("father的構造器");
}
public static void showStatic(String str) {
System.out.println("father " + str);
}
public final void showFinal() {
System.out.println("father show final");
}
public void showCommon() {
System.out.println("father 普通方法");
}
}
public class Son extends Father {
public Son() {
//invokespecial
super();
}
public Son(int age) {
//invokespecial
this();
}
//不是重寫的父類別的靜態方法,因爲靜態方法不能被重寫!
public static void showStatic(String str) {
System.out.println("son " + str);
}
private void showPrivate(String str) {
System.out.println("son private" + str);
}
public void show() {
//invokestatic
showStatic("atguigu.com");
//invokestatic
super.showStatic("good!");
//invokespecial
showPrivate("hello!");
//invokevirtual
//雖然位元組碼指令中顯示爲invokevirtual,但因爲此方法宣告有final,不能被子類重寫,所以也認爲此方法是非虛方法。
showFinal();
//invokespecial
super.showCommon();
//invokevirtual
//有可能子類會重寫父類別的showCommon()方法
showCommon();
info();
MethodInterface in = null;
//invokeinterface
in.methodA();
}
public void info() {
}
public void display(Father f) {
f.showCommon();
}
public static void main(String[] args) {
Son so = new Son();
so.show();
}
}
interface MethodInterface {
void methodA();
}
關於 invokedynamic 指令
JVM位元組碼指令集一直比較穩定,一直到Java7中才增加了一個invokedynamic指令,這是Java爲了實現【動態型別語言】支援而做的一種改進。
但是在Java7中並沒有提供直接生成invokedynamic指令的方法,需要藉助ASM這種底層位元組碼工具來產生invokedynamic指令。直到Java8的Lambda表達式的出現,invokedynamic指令的生成,在Java中纔有了直接的生成方式。
Java7中增加的動態語言型別支援的本質是對Java虛擬機器規範的修改,而不是對Java語言規則的修改,這一塊相對來講比較複雜,增加了虛擬機器中的方法呼叫,最直接的受益者就是執行在Java平臺的動態語言的編譯器。
程式碼範例
@FunctionalInterface
interface Func {
public boolean func(String str);
}
public class Lambda {
public void lambda(Func func) {
return;
}
public static void main(String[] args) {
Lambda lambda = new Lambda();
Func func = s -> {
return true;
};
lambda.lambda(func);
lambda.lambda(s -> {
return true;
});
}
}
動態語言和靜態語言
動態型別語言和靜態型別語言兩者的區別就在於對型別的檢查是在編譯期還是在執行期,滿足前者就是靜態型別語言,反之是動態型別語言。
說的再直白一點就是,靜態型別語言是判斷變數自身的型別資訊;動態型別語言是判斷變數值的型別資訊,變數沒有型別資訊,變數值纔有型別資訊,這是動態語言的一個重要特徵。
Java:String info = "mogu blog"; (Java是靜態型別語言的,會先編譯就進行型別檢查)
JS:var name = "shkstart"; var name = 10; (執行時才進行檢查)
方法重寫的本質
Java 語言中方法重寫的本質:
IllegalAccessError介紹
回看解析階段
虛方法表
在物件導向的程式設計中,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元數據中搜尋合適的目標的話就可能影響到執行效率。
因此,爲了提高效能,JVM採用在類的方法區建立一個虛方法表(virtual method table)來實現,非虛方法不會出現在表中。使用索引表來代替查詢。
每個類中都有一個虛方法表,表中存放着各個方法的實際入口。
虛方法表是什麼時候被建立的呢?虛方法表會在類載入的鏈接階段被建立並開始初始化,類的變數初始值準備完成之後,JVM會把該類的虛方法表也初始化完畢。
如圖所示:如果類中重寫了方法,那麼呼叫的時候,就會直接在該類的虛方法表中查詢
方法返回地址(return address)
存放呼叫該方法的pc暫存器的值。一個方法的結束,有兩種方式:
無論通過哪種方式退出,在方法退出後都返回到該方法被呼叫的位置。方法正常退出時,呼叫者的pc計數器的值作爲返回地址,即呼叫該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會儲存這部分資訊。
本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的區域性變數表、運算元棧、將返回值壓入呼叫者棧幀的運算元棧、設定PC暫存器值等,讓呼叫者方法繼續執行下去。
正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他的上層呼叫者產生任何的返回值。
方法退出的兩種方式
當一個方法開始執行後,只有兩種方式可以退出這個方法,
正常退出:
異常退出:
在方法執行過程中遇到異常(Exception),並且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜尋到匹配的例外處理器,就會導致方法退出,簡稱異常完成出口。
方法執行過程中,拋出異常時的例外處理,儲存在一個異常處理表,方便在發生異常的時候找到處理異常的程式碼
程式碼舉例
public class ReturnAddressTest {
public boolean methodBoolean() {
return false;
}
public byte methodByte() {
return 0;
}
public short methodShort() {
return 0;
}
public char methodChar() {
return 'a';
}
public int methodInt() {
return 0;
}
public long methodLong() {
return 0L;
}
public float methodFloat() {
return 0.0f;
}
public double methodDouble() {
return 0.0;
}
public String methodString() {
return null;
}
public Date methodDate() {
return null;
}
public void methodVoid() {
}
static {
int i = 10;
}
public void method2() {
methodVoid();
try {
method1();
} catch (IOException e) {
e.printStackTrace();
}
}
public void method1() throws IOException {
FileReader fis = new FileReader("atguigu.txt");
char[] cBuffer = new char[1024];
int len;
while ((len = fis.read(cBuffer)) != -1) {
String str = new String(cBuffer, 0, len);
System.out.println(str);
}
fis.close();
}
}
方法正常返回
ireturn
dreturn
areturn
異常處理表:
棧幀中還允許攜帶與Java虛擬機器實現相關的一些附加資訊。例如:對程式偵錯提供支援的資訊。
舉例棧溢位的情況?(StackOverflowError)
通過 -Xss 設定棧的大小
調整棧大小,就能保證不出現溢位麼?
不能保證不溢位
分配的棧記憶體越大越好麼?
不是,一定時間內降低了OOM概率,但是會擠佔其它的執行緒空間,因爲整個虛擬機器的記憶體空間是有限的
垃圾回收是否涉及到虛擬機器棧?
不會
方法中定義的區域性變數是否執行緒安全?
何爲執行緒安全?
具體問題具體分析:
/**
* 面試題:
* 方法中定義的區域性變數是否執行緒安全?具體情況具體分析
*
* 何爲執行緒安全?
* 如果只有一個執行緒纔可以操作此數據,則必是執行緒安全的。
* 如果有多個執行緒操作此數據,則此數據是共用數據。如果不考慮同步機制 機製的話,會存線上程安全問題。
* @author shkstart
* @create 2020 下午 7:48
*/
public class StringBuilderTest {
//s1的宣告方式是執行緒安全的
public static void method1(){
//StringBuilder:執行緒不安全
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
//...
}
//sBuilder通過參數傳遞方法內,存線上程不安全的問題
public static void method2(StringBuilder sBuilder){
sBuilder.append("a");
sBuilder.append("b");
//...
}
//操作s1之後,將s1作爲返回值返回,存線上程不安全的問題
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s1的操作:是執行緒安全的
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
new Thread(() -> {
s.append("a");
s.append("b");
}).start();
method2(s);
}
}
執行時數據區,哪些部分存在Error和GC?
執行時數據區 | 是否存在Error | 是否存在GC |
---|---|---|
程式計數器 | 否 | 否 |
虛擬機器棧 | 是(SOF) | 否 |
本地方法棧 | 是 | 否 |
方法區 | 是(OOM) | 是 |
堆 | 是(OOM) | 是 |