虛擬機器棧的出現背景
(1)由於跨平臺性的設計,Java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計為基於暫存器的【如果設計成基於暫存器的,耦合度高,效能會有所提升,因為可以對具體的CPU架構進行最佳化,但是跨平臺性大大降低】.
(2)優點是跨平臺,指令集小,編譯器容易實現,缺點是效能下降,實現同樣的功能需要更多的指令
記憶體中的堆和棧
(1)首先棧是執行時的單位,堆是儲存的單位;
(2)即棧解決程式的執行問題,程式如何執行,如何處理資料。堆解決的是資料儲存的問題,即資料怎麼放,放哪裡;
虛擬機器棧的基本內容
每一個執行緒在建立時都會建立一個虛擬機器棧,其內部儲存一個個的棧幀,對應著一次次的Java方法呼叫,棧是執行緒私有的。
public class StackTest { public static void main(String[] args) { StackTest test = new StackTest(); test.methodA(); } public void methodA() { int i = 10; int j = 20; methodB(); } public void methodB(){ int k = 30; int m = 40; } }
虛擬機器棧的生命週期
生命週期和執行緒一致,執行緒結束了
虛擬機器棧的作用
主管Java的程式的執行,儲存方法的區域性變數(8 種基本資料型別、物件的引用地址)、部分結果,並參與方法的呼叫和返回。
虛擬機器棧的特點
(1)棧是一種快速有效的分配儲存方式,訪問速度僅次於程式計數器。
(2)JVM直接對Java棧的操作只有兩個;每個方法執行,伴隨著進棧(入棧、壓棧);執行結束後的出棧工作
(3)對於棧來說不存在垃圾回收問題,棧不需要GC,但是可能存在OOM
如果採用固定大小的Java虛擬機器棧,那每一個執行緒的Java虛擬機器棧容量可以線上程建立的時候獨立選定。如果執行緒請求分配的棧容量超過Java虛擬機器允許的最大容量,Java虛擬機器將會丟擲一個StackoverflowError 異常
設定棧記憶體的大小
可以使用引數-Xss選項來設定執行緒的最大棧空間,棧的大小直接決定了函式呼叫的最大可達深度
Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, and g or G to indicate GB. The default value depends on the platform: Linux/x64 (64-bit): 1024 KB macOS (64-bit): 1024 KB Oracle Solaris/x64 (64-bit): 1024 KB Windows: The default value depends on virtual memory
The following examples set the thread stack size to 1024 KB in different units:
-Xss1m -Xss1024k -Xss1048576
舉例
public class StackTest01 { private static int count = 1; public static void main(String[] args) { System.out.println(count); count ++ ; main(args); } }
在沒有設定棧大小前:棧在11408這個深度溢位了
11406
11407
11408
Exception in thread "main" java.lang.StackOverflowError
設定棧引數之後:棧在2457深度溢位
2456 2457 Exception in thread "main" java.lang.StackOverflowError at sun.nio.cs.UTF_8.updatePositions(UTF_8.java:77)
棧的儲存單位
棧中儲存什麼
(1)每個執行緒都有自己的棧,棧中的資料都是以棧幀(Stack Frame)的格式存在的;
(2)在這個執行緒上正在執行的每一個方法都有各自對應的一個棧幀;
(3)棧幀是一個記憶體區塊,是一個資料集,維繫著方法執行過程中的各種資料資訊。
棧執行原理
(1)JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循先進後出(後進先出)原則;
(2)在一條活動執行緒中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的。這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀相對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)
(3)執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作
(4)如果在該方法中呼叫了其他方法,對應的新的棧幀會被建立出來,放在棧的頂端,成為新的當前幀
(1)不同執行緒中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀之中引用另外一個執行緒的棧幀
(2)如果當前方法呼叫了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接著,虛擬機器會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀;
(3)Java方法有兩種返回函式的方式。一種是正常的函式返回,使用return指令;另一種是方法執行中出現未捕獲處理的異常,以丟擲異常的方式結束;但不管使用哪種方式,都會導致棧幀被彈出
棧幀的內部結構
每個棧幀中儲存著:
(1)區域性變數表(Local Variables );(2)運算元棧(Operand Stack)(或表示式棧);(3)動態連結(Dynamic Linking)(或指向執行時常量池的方法引用);(4)方法返回地址(Return Address)(或方法正常退出或者異常退出的定義)(5)一些附加資訊
並行每個執行緒下的棧都是私有的,因此每個執行緒都有自己的棧,並且每個棧裡面都有很多棧幀,棧幀的大小主要由區域性變數表和運算元棧決定的
區域性變數表
認識區域性變數表
概念
(1)區域性變數表也被稱之為區域性變數陣列或本地變數表;
(2)定義一個數字陣列,主要用於儲存方法引數和定義在方法內的區域性變數,這些資料型別包括各類基本資料型別、物件引用(reference),以及returnAddress返回值型別;
(3)由於區域性變數是建立線上程的棧上,是執行緒私有資料,因此不存在資料安全問題;
(4)區域性變數表所需的容量大小是編譯期確定下來的,並儲存在方法的code屬性maximum local variables資料項中,在方法執行期間是不會改變區域性變數表的大小的;
(5)方法巢狀呼叫的次數由棧的大小決定。一般來說,棧越大,方法巢狀呼叫次數越多
- 對一個函式而言,它的引數和區域性變數越多,使得區域性變數表膨脹,它的棧幀就越大,以滿足方法呼叫所需傳遞的資訊增大的需求。
- 進而函式呼叫就會佔用更多的棧空間,導致其巢狀呼叫次數就會減少。
(6)區域性變數表中的變數只在當前方法呼叫中有效
- 在方法執行時,虛擬機器透過使用區域性變數表完成引數值到引數變數列表的傳遞過程。
- 當方法呼叫結束後,隨著方法棧幀的銷燬,區域性變數表也會隨之銷燬。
public class LocalVariablesTest { private int count = 0; public static void main(String[] args) { LocalVariablesTest test = new LocalVariablesTest(); int num = 10; test.test1(); } //練習: 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 LocalVariablesTest(){ this.count = 1; } 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 void test3() { this.count++; } public void test4() { int a = 0; { int b = 0; b = a + 1; } //變數c使用之前已經銷燬的變數b佔據的slot的位置 int c = a + 1; } }
部分詳解
使用jclasslib看位元組碼,以main方法為例
(1)0-15表示有16行位元組碼
(2)方法異常表
(3)misc
(4)行號表
Java程式碼的行號和位元組碼指令行號的關係
(5)生效行數和剩餘有效行數都是針對位元組碼檔案的行數
a. 起始pc和長度表示區域性變數的作用域
b.Start PC==11表示在位元組碼的11行開始生效,也就是Java程式碼對應的第11行。而宣告int num在java程式碼的是第10行,說明是從宣告的下一行開始生效
c. Length== 5表示區域性變數剩餘有效行數,main方法位元組碼指令總共有16行,從11行開始生效,那麼剩下就是16-11 ==5
d. Ljava/lang/String
前面的L表示引用型別
關於slot的理解
(1)引數值的存放總是從區域性變數陣列索引 0 的位置開始,到陣列長度-1的索引結束
(2)區域性變數表,最基本的儲存單元是Slot(變數槽),區域性變數表中存放編譯期可知的各種基本資料型別(8種),引用型別(reference),returnAddress型別的變數
(3)在區域性變數表裡,32位以內的型別只佔用一個slot(包括returnAddress型別),64位的型別佔用兩個slot(1ong和double)。
- byte、short、char在儲存前被轉換為int,boolean也被轉換為int,0表示false,非0表示true
- long和double則佔據兩個slot
(4)JVM會為區域性變數表中的每一個Slot都分配一個訪問索引,透過這個索引即可成功訪問到區域性變數表中指定的區域性變數值
(5)當一個例項方法被呼叫的時候,它的方法引數和方法體內部定義的區域性變數將會按照順序被複制到區域性變數表中的每一個slot上
(6)如果需要訪問區域性變數表中一個64bit的區域性變數值時,只需要使用前一個索引即可。(比如:訪問long或double型別變數)
(7)如果當前幀是由構造方法或者例項方法建立的,那麼該物件引用this將會存放在index為0的slot處,其餘的引數按照參數列順序繼續排列。(this也相當於一個變數)
slot程式碼示例
this存放在index=0的位置
public void test3() { this.count++; }
區域性變數表:this存放在index=0的位置
64位的型別(long和double)佔用兩個slot
public String test2(Date dateP, String name2) { dateP = null; name2 = "songhongkang"; double weight = 130.5;//佔據兩個slot char gender = '男'; return dateP + name2; }
weight為double型別,index直接從3到5
static無法呼叫this
this不存在static方法的區域性變數表中,所有無法呼叫
slot的重複利用
棧幀中的區域性變數表中的槽位是可以重用的,如果一個區域性變數過了其作用域,那麼在其作用域之後申明新的區域性變數變就很有可能會複用過期區域性變數的槽位,從而達到節省資源的目的
區域性變數 c 重用了區域性變數 b 的 slot 位置
靜態變數和區域性變數的對比
變數的分類: 1、按照資料型別分:① 基本資料型別 ② 引用資料型別 2、按照在類中宣告的位置分: 2-1、成員變數:在使用前,都經歷過預設初始化賦值 2-1-1、類變數: linking的prepare階段:給類變數預設賦值 ---> initial階段:給類變數顯式賦值即靜態程式碼塊賦值 2-1-2、例項變數:隨著物件的建立,會在堆空間中分配例項變數空間,並進行預設賦值 2-2、區域性變數:在使用前,必須要進行顯式賦值的!否則,編譯不透過。
運算元棧
運算元棧的特點
(1)每個獨立的的棧幀除了包含區域性變數以外,還包含一個後進先出(Last - In - First -Out)的 運算元棧,也可以稱之為表示式棧(Expression Stack)
(2)運算元棧,在方法執行過程中,根據位元組碼指令,往棧中寫入資料或提取資料,即入棧(push)和出棧(pop)
某些位元組碼指令將值壓入運算元棧,其餘的位元組碼指令將運算元取出棧。使用它們後再把結果壓入棧
比如:執行復制、交換、求和等操作
運算元棧的作用
(1)運算元棧,主要用於儲存計算過程的中間結果,同時作為計算過程中變數臨時的儲存空間;
(2)運算元棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被建立出來,這時方法的運算元棧是空的
(3)每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯期就定義好了,儲存在方法的Code屬性中,為maxstack的值
(4)棧中的任何一個元素都是可以任意的Java資料型別
32bit的型別佔用一個棧單位深度;64bit的型別佔用兩個棧單位深度
(5)運算元棧並非採用訪問索引的方式來進行資料訪問的,而是隻能透過標準的入棧和出棧操作來完成一次資料訪問。只不過運算元棧是用陣列這個結構來實現的而已
(6)如果被呼叫的方法帶有返回值的話,其返回值將會被壓入當前棧幀的運算元棧中,並更新PC暫存器中下一條需要執行的位元組碼指令
(7)運算元棧中元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類載入過程中的類檢驗階段的資料流分析階段要再次驗證
(8)Java虛擬機器的解釋引擎是基於棧的執行引擎,其中的棧指的就是運算元棧
區域性變數表就相當於食材,運算元棧就相當於做法步驟
運算元棧程式碼追蹤
public void testAddOperation() { //byte、short、char、boolean:都以int型來儲存 byte i = 15; int j = 8; int k = i + j; // int m = 800; }
對應位元組碼指令
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
一步一步看流程
(1)首先執行第一條語句,PC暫存器指向的是0,也就是指令地址為0,然後使用bipush讓運算元15入運算元棧
(2)執行完成後,PC暫存器下移,指向下一行程式碼,下一行程式碼表示將運算元棧的元素儲存到區域性變數表1的位置(istore_1),我們可以看到區域性變數表中已經增加了一個元素,並且運算元棧為空了。
解釋為什麼區域性變數表索引從1開始,因為該方法為例項方法,區域性變數表索引0的位置存放的是this
(3)然後PC下移,指向下一行,8入棧,同時執行store操作,存入區域性變數表中;
(4)然後從區域性變數表中,依次將資料放在運算元棧中,等待執行add操作
iload_1:取出區域性變數表中索引為1的資料放入運算元棧中
(5)運算元棧中的兩個元素執行相加操作,並儲存在區域性變數表3的位置
關於型別轉換的說明
因為8可以存放在byte型別中,所以壓入運算元棧的型別為byte,而不是int,所以執行的位元組碼指令為bipush 8,但是儲存在區域性變數的時候,會轉成int型別的變數:istore_4
m改成800之後,byte儲存不了,就成了short型別,sipush 800
如果被呼叫的方法帶有返回值,返回值入運算元棧
public int getSum(){ int m = 10; int n = 20; int k = m + n; return k; } public void testGetSum(){ //獲取上一個棧楨返回的結果,並儲存在運算元棧中 int i = getSum(); int j = 10; }
getSum() 方法位元組碼指令:最後帶著ireturn
testGetSum() 方法位元組碼指令:一上來就載入 getSum() 方法的返回值()
棧頂快取技術
棧頂快取技術:Top Of Stack Cashing
(1)前面提過,基於棧式架構的虛擬機器所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味著將需要更多的指令分派(instruction dispatch)次數(也就是你會發現指令很多)和導致記憶體讀/寫次數多,效率不高
(2)由於運算元是儲存在記憶體中的,因此頻繁地執行記憶體讀/寫操作必然會影響執行速度。為了解決這個問題,HotSpot JVM的設計者們提出了棧頂快取(Tos,Top-of-Stack Cashing)技術,將棧頂元素全部快取在物理CPU的暫存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率
(3)暫存器的主要優點:指令更少,執行速度快,但是指令集(也就是指令種類)很多
動態連結
動態連結(執行執行時常量池的方法引用)
(1)每一個棧幀內部都包含一個指向執行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支援當前方法的程式碼能夠實現動態連結(Dynamic Linking),比如:invokedynamic指令
(2)在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++; } }
對應位元組碼
Classfile /F:/IDEAWorkSpaceSourceCode/JVMDemo/out/production/chapter05/com/atguigu/java1/DynamicLinkingTest.class Last modified 2020-11-10; size 712 bytes MD5 checksum e56913c945f897c7ee6c0a608629bca8 Compiled from "DynamicLinkingTest.java" public class com.atguigu.java1.DynamicLinkingTest minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant 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 { int num; descriptor: I flags: public com.atguigu.java1.DynamicLinkingTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: bipush 10 7: putfield #2 // Field num:I 10: return LineNumberTable: line 7: 0 line 9: 4 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/atguigu/java1/DynamicLinkingTest; public void methodA(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String methodA().... 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 12: 0 line 13: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 this Lcom/atguigu/java1/DynamicLinkingTest; 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; } SourceFile: "DynamicLinkingTest.java"
(1)在位元組碼指令中,methodB() 方法中透過 invokevirtual #7 指令呼叫了方法 A ,那麼 #7 是個啥呢?
(2)往上面翻,找到常量池的定義:#7 = Methodref #8.#31
先找 #8 :
#8 = Class #32
:去找 #32
#32 = Utf8 com/atguigu/java1/DynamicLinkingTest
結論:透過 #8 我們找到了 DynamicLinkingTest
這個類
再來找 #31:
#31 = NameAndType #19:#13
:去找 #19 和 #13
#19 = Utf8 methodA
:方法名為 methodA
#13 = Utf8 ()V
:方法沒有形參,返回值為 void
(3)結論:透過 #7 我們就能找到需要呼叫的 methodA() 方法,並進行呼叫
(4)在上面,其實還有很多符號引用,比如 Object、System、PrintStream 等等
為什麼要使用常量池呢?
(1)因為在不同的方法,都可能呼叫常量或者方法,所以只需要儲存一份即可,然後記錄其引用即可,節省了空間
(2)常量池的作用:就是為了提供一些符號和常量,便於指令的識別
方法的呼叫
靜態連結與動態連結
在JVM中,將符號引用轉換為呼叫方法的直接引用與方法的繫結機制相關
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();//表現為:晚期繫結 } }
{ public com.atguigu.java2.AnimalTest(); 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 54: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/atguigu/java2/AnimalTest; public void showAnimal(com.atguigu.java2.Animal); descriptor: (Lcom/atguigu/java2/Animal;)V flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: aload_1 1: invokevirtual #2 // Method com/atguigu/java2/Animal.eat:()V 4: return LineNumberTable: line 56: 0 line 57: 4 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/atguigu/java2/AnimalTest; 0 5 1 animal Lcom/atguigu/java2/Animal; public void showHunt(com.atguigu.java2.Huntable); descriptor: (Lcom/atguigu/java2/Huntable;)V flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: aload_1 1: invokeinterface #3, 1 // InterfaceMethod com/atguigu/java2/Huntable.hunt:()V 6: return LineNumberTable: line 60: 0 line 61: 6 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lcom/atguigu/java2/AnimalTest; 0 7 1 h Lcom/atguigu/java2/Huntable; } SourceFile: "AnimalTest.java"
invokevirtual 體現為晚期繫結
invokeinterface 也體現為晚期繫結
invokespecial 體現為早期繫結
多型與繫結
(1)隨著高階語言的橫空出世,類似於Java一樣的基於物件導向的程式語言如今越來越多,儘管這類程式語言在語法風格上存在一定的差別,但是它們彼此之間始終保持著一個共性,那就是都支援封裝、繼承和多型等物件導向特性,既然這一類的程式語言具備多型特性,那麼自然也就具備早期繫結和晚期繫結兩種繫結方式
(2)Java中任何一個普通的方法其實都具備虛擬函式的特徵,它們相當於C++語言中的虛擬函式(C++中則需要使用關鍵字virtual來顯式定義)。如果在Java程式中不希望某個方法擁有虛擬函式的特徵時,則可以使用關鍵字final來標記這個方法
虛方法與非虛方法
虛方法與非虛方法的區別
(1)如果方法在編譯期就確定了具體的呼叫版本,這個版本在執行時是不可變的。這樣的方法稱為非虛方法
(2)靜態方法、私有方法、final方法、例項構造器、父類方法都是非虛方法
(3)其他方法稱為虛方法
子類物件的多型的使用前提:
(1)類的繼承關係(2)方法的重寫
虛擬機器中呼叫方法的指令:
(1)普通指令
invokestatic:呼叫靜態方法,解析階段確定唯一方法版本;
invokespecial:呼叫<init>
方法、私有及父類方法,解析階段確定唯一方法版本
invokevirtual:呼叫所有虛方法
invokeinterface:呼叫介面方法
(2)動態呼叫指令
invokedynamic:動態解析出需要呼叫的方法,然後執行
前四條指令固化在虛擬機器內部,方法的呼叫執行不可人為干預。而invokedynamic指令則支援由使用者確定方法版本。其中invokestatic指令和invokespecial指令呼叫的方法稱為非虛方法,其餘的(final修飾的除外)稱為虛方法。
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!"); //invokespecial super.showCommon(); //invokevirtual showFinal();//因為此方法宣告有final,不能被子類重寫,所以也認為此方法是非虛方法。 //虛方法如下: /* invokevirtual 你沒有顯示的加super.,編譯器認為你可能呼叫子類的showCommon(即使son子類沒有重寫,也 會認為),所以編譯期間確定不下來,就是虛方法。 */ 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指令:
(1)JVM位元組碼指令集一直比較穩定,一直到Java7中才增加了一個invokedynamic指令,這是Java為了實現【動態型別語言】支援而做的一種改進
(2)但是在Java7中並沒有提供直接生成invokedynamic指令的方法,需要藉助ASM這種底層位元組碼工具來產生invokedynamic指令。直到Java8的Lambda表示式的出現,invokedynamic指令的生成,在Java中才有了直接的生成方式
(3)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; }); } }
動態語言和靜態語言:
(1)動態型別語言和靜態型別語言兩者的區別就在於對型別的檢查是在編譯期還是在執行期,滿足前者就是靜態型別語言,反之是動態型別語言
(2)說的再直白一點就是,靜態型別語言是判斷變數自身的型別資訊;動態型別語言是判斷變數值的型別資訊,變數沒有型別資訊,變數值才有型別資訊,這是動態語言的一個重要特徵
Java:String info = “mogu blog”; (Java是靜態型別語言的,會先編譯就進行型別檢查)
JS:var name = “shkstart”; var name = 10; (執行時才進行檢查)
Python: info = 130.5 (執行時才檢查)
Java語言中方法重寫的本質:
(1)找到運算元棧頂的第一個元素所執行的物件的實際型別,記作C
(2)如果在型別C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問許可權校驗
如果透過則返回這個方法的直接引用,查詢過程結束
如果不透過,則返回java.lang.IllegalAccessError 異常
(3)否則,按照繼承關係從下往上依次對C的各個父類進行第2步的搜尋和驗證過程。
(4)如果始終沒有找到合適的方法,則丟擲java.lang.AbstractMethodError異常。
上面的過程稱為動態分析
IllegalAccessError介紹
(1)程式試圖訪問或修改一個屬性或呼叫一個方法,這個屬性或方法,你沒有許可權訪問。一般的,這個會引起編譯器異常。這個錯誤如果發生在執行時,就說明一個類發生了不相容的改變
(2)比如,你把應該有的jar包放從工程中拿走了,或者Maven中存在jar包衝突
虛方法表:
(1)在物件導向的程式設計中,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法後設資料中搜尋合適的目標的話就可能影響到執行效率。因此,為了提高效能,JVM採用在類的方法區建立一個虛方法表(virtual method table)來實現,非虛方法不會出現在表中。使用索引表來代替查詢。【上面動態分派的過程,我們可以看到如果子類找不到,還要從下往上找其父類,非常耗時】
(2)每個類中都有一個虛方法表,表中存放著各個方法的實際入口
(3)虛方法表是什麼時候被建立的呢?虛方法表會在類載入的連結階段被建立並開始初始化,類的變數初始值準備完成之後,JVM會把該類的虛方法表也初始化完畢
(1)比如說son在呼叫toString的時候,Son沒有重寫過,Son的父類Father也沒有重寫過,那就直接呼叫Object類的toString。那麼就直接在虛方法表裡指明toString直接指向Object類
(2)下次Son物件再呼叫toString就直接去找Object,不用先找Son–>再找Father–>最後才到Object的這樣的一個過程
方法返回地址:
在一些帖子裡,方法返回地址、動態連結、一些附加資訊 也叫做幀資料區
方法退出的兩種方式:
反編譯位元組碼檔案,可得到 Exception table
from :位元組碼指令起始地址
to :位元組碼指令結束地址
target :出現異常跳轉至地址為 11 的指令執行
type :捕獲異常的型別
一些附加資訊
棧幀中還允許攜帶與Java虛擬機器實現相關的一些附加資訊。例如:對程式除錯提供支援的資訊。
棧相關面試題:
舉例棧溢位的情況
SOF(StackOverflowError),棧大小分為固定的,和動態變化。如果是固定的就可能出現StackOverflowError。如果是動態變化的,記憶體不足時就可能出現OOM
調整棧大小,就能保證不出現溢位麼?
不能保證不溢位,只能保證SOF出現的機率小
分配的棧記憶體越大越好麼?
不是,一定時間內降低了OOM機率,但是會擠佔其它的執行緒空間,因為整個虛擬機器的記憶體空間是有限的
垃圾回收是否涉及到虛擬機器棧?
不會
方法中定義的區域性變數是否執行緒安全?
具體問題具體分析:
(1)如果只有一個執行緒才可以操作此資料,則必是執行緒安全的。
(2)如果有多個執行緒操作此資料,則此資料是共享資料。如果不考慮同步機制的話,會存線上程安全問題
具體問題具體分析:
如果物件是在內部產生,並在內部消亡,沒有返回到外部,那麼它就是執行緒安全的,反之則是執行緒不安全的
/** * 面試題: * 方法中定義的區域性變數是否執行緒安全?具體情況具體分析 * * 何為執行緒安全? * 如果只有一個執行緒才可以操作此資料,則必是執行緒安全的。 * 如果有多個執行緒操作此資料,則此資料是共享資料。如果不考慮同步機制的話,會存線上程安全問題。 */ public class StringBuilderTest { int num = 10; //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的操作:是執行緒不安全的(有返回值,可能被其它執行緒操作) public static StringBuilder method3(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); return s1; } //s1的操作:是執行緒安全的(s1自己消亡了,最後返回的只是s1.toString的一個新物件) 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); } }