虛擬機棧
虛擬機棧出現的背景
由于跨平臺性的設計,Java的指令都是根據棧來設計的. 不同平臺CPU架構不同,所以不能設置為基于寄存器的
優點是跨平臺,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令.
有不少Java開發人員一提到Java內存結構,就會非常粗粒度地將JVM中的內存區理解為僅有Java堆(Heap)和Java棧(stack)? 為什么
內存中的棧與堆
棧是運行時的單位,而堆是存儲的單位
即: 棧解決程序的運行問題,即程序如何執行,或者說如何處理數據.
堆: 堆解決的是數據的存儲的問題,即數據怎么放,放在哪兒.
比喻
java 虛擬機棧是什么?
java虛擬機棧(java virtual machine stack)早期,也叫java棧
每個線程在創建是都會創建一個虛擬機棧, 其 內部保存一個個的棧楨(stack Frame)對應著一次次的Java方法調用.
是線程私有的
聲明周期
生命周期和線程一致.
作用
- 主管java程序的運行,它保存的方法的局部變量(8種基本數據類型,對象的引用地址),部分結果,并參與方法的調用和返回.
- 局部變量 vs 成員變量(或屬性)
- 基本數據變量 vs 引用類型變量(類,數組,接口)
實例
package com.atguigu.java5;/*** ClassName: StackTest <br/>* Description: StackTest <br/>* Date: 2020-10-23 10:36 <br/>* <br/>** @author * @version 產品版本信息 2020年10月23日10:36分 新建<br/>* <p>* 修改記錄* @email * @project study_note_01* @package com.atguigu.java5*/
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;}
}
分析一波
棧的特點(優點)
- 棧是一種快速有效的分配存儲方式,訪問速度僅此云程序計數器.
- JVM 直接對 Java棧的操作只有兩個:
- 每個方法執行,伴隨著進棧(入棧,壓棧)
- 執行結束后的出棧工作
- 對于棧來說不存在垃圾回收問題
- GC ; OOM ;
棧: 先入后出 子彈夾 FILO
隊列: 先入先出 FIFO
面試題: 開發中遇到的異常有哪些
棧可能出現的異常
- java 虛擬機規范允許java棧的大小是動態的或者是固定不變的
- 如果采用固定大小的java虛擬機棧,那每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定.如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量,Java虛擬機將會拋出一個StackOverflowError異常.
- 如果Java虛擬機可以動態擴展,并且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存區創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutOfMemoryError異常
設置內存中的棧的大小
package com.atguigu.java5;/*** ClassName: StackErrorTest <br/>* Description: StackErrorTest <br/>* Date: 2020-10-23 11:01 <br/>* <br/>** @author yufengming* @version 產品版本信息 2020年10月23日11:01分 * 新建<br/>* <p>* 修改記錄* @email * @project study_note_01* @package com.atguigu.java5* 默認情況下: count:11420* 設置棧的大小: -Xss 256k:*/
public class StackErrorTest {private static int count = 1;public static void main(String[] args) {System.out.println(count);count++;main(args);}
}
11410
11411
11412
Exception in thread "main" java.lang.StackOverflowErrorat sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)at sun.nio.cs.StreamEncoder.write(StreamEncoder.java:125)at java.io.OutputStreamWriter.write(OutputStreamWriter.java:207)at java.io.BufferedWriter.flushBuffer(BufferedWriter.java:129)at java.io.PrintStream.write(PrintStream.java:526)at java.io.PrintStream.print(PrintStream.java:597)at java.io.PrintStream.println(PrintStream.java:736)at com.atguigu.java5.StackErrorTest.main(StackErrorTest.java:20)
棧的存儲單位
棧中存儲什么?
- 每個線程都有自己的棧,棧中的數據都是以棧楨(Stack Frame)的格式存在
- 在這個線程上正在執行的每個方法都各自對應一個棧楨(Stack Frame).
- 棧楨是一個內存區塊,是一個數據集,維系著方法執行過程中的各種數據信息.
復習
- OOP的基本概念: 類,對象
- 類中基本結構: field(屬性,字段,域),method
棧運行原理
- JVM 直接對Java棧的操作只有兩個,就是對棧楨的壓棧和出棧,遵循"先進后出"/"后進先出"原則
- 在一條活動線程中,一個時間點上,只會有一個活動的棧楨.即只有當前正在執行的方法的棧楨(棧頂棧楨)是有效的,這個棧楨被稱為當前棧楨(current frame),與當前棧楨相對應的方法就是當前方法(current method),定義這個方法的類就是當前類(current class)
- 執行引擎運行的所有字節碼指令只針對當前棧楨進行操作
- 如果在該方法中調用了其他方法,對應的新的棧楨會被創建出來,放在棧的頂端,成為新的當前幀.
當前棧楨,一直在移動
依次執行
- 不同線程中所包含的棧楨是不允許存在相互引用的,即不可能在一個棧楨之中引用另外一個線程的棧楨.
- 如果當前方法調用了其他方法,方法返回之際,當前棧楨會傳回此方法的執行結果給前一個棧楨,接著,虛擬機會丟棄當前棧楨,使得前一個棧楨重新成為當前棧楨.
- Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常.不管使用哪種方式,都會導致棧楨被彈出.
棧楨內部結構
在虛擬機這塊兒,有錯誤的帖子很多,你不能全信
垃圾回收算法
局部變量表
- 局部變量表也被稱之為局部變量數組或本地變量表
- **定義為一個數字數組,主要用于存儲方法參數和定義在方法體內的局部變量,**這些數據類型包括各類基本數據類型,對象引用(reference),以及returnAddress類型.
- 由于局部變量表示建立在線程的棧上,是線程的私有數據,因此不存在數據安全問題
- 局部變量表所需的容量大小是在編譯期確定下來的,并保存在方法Code屬性的maximum local variables 數據項中.在方法運行期間是不會改變局部變量表的大小的.
javap 命令 解析,這個idea具有反編譯功能
idea中jclasslib 插件
- 方法嵌套調用的次數由棧的大小決定.一般來說,**棧越大,方法嵌套調用次數越多.**對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧楨就越大,以滿足方法調用所需傳遞的信息增大的需求.進而函數調用就會占用更多的棧空間,導致其嵌套調用次數就會減少.
- **局部變量表中的
- **.在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程.**當方法調用結束后,隨著方法棧楨的銷毀,局部變量表也會隨之銷毀.
關于Slot的理解
-
參數值的存放總是在局部變量數組index0 開始,到數組長度-1 的索引結束.
-
局部變量表,最基本的存儲單元是Slot(變量槽)
-
局部變量表中存放編譯期可知的各種基本數據類型(8種),引用類型(reference),returnAddress類型的變量.
-
在局部變量表里,32位以內的類型只占用一個slot(包括returnAddress類型),64位的類型(long和double)占用兩個slot.
-
byte,short,char 在存儲前被轉換為int,boolean也被轉換為int,0 表示 false,非0 表示 true.
-
long和double 則占據兩個Slot.
-
JVM會為局部變量表中的每一個Slot都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值
-
當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照順序被復制到局部變量表中的每一個Slot上
-
如果需要訪問局部變量表中一個64bit的局部變量值時,只需要使用前一個索引即可.(比如: 訪問long或double類型變量)
-
如果當前幀是由構造方法或者實例方法創建的,name該對象引用this將會存放在index為0的slot處,其余的參數按照參數表順序繼續排列
4分鐘
構造器中的this,表示當前創建的變量
棧楨中的布局變量表中的槽位是可以重用的,如果一個局部變量過了其作用域,那么在其作用域之后申明的新的局部變量就會很有可能會復用過期局部變量的槽位,從而達到節省資源的目的
變量的分類
按照數據類型:
- 基本數據類型
- 引用數據類型
按照在類中聲明的位置:
- 成員變量: 在使用前都經歷過初始化賦值
- 使用靜態來修飾的(類變量):
- linking的prepare階段:給類變量默認賦值 —>
- initial階段: 給類變量顯示賦值即靜態代碼
- 實例變量(歸具體的對象所有)
:隨著對象的創建,會在堆空間中分配實例變量空間,并進行默認賦值
- 局部變量: 在使用前,比如要進行顯示賦值,否則,編譯不通過
public void test5Temp(){int num;System.out.println(num);// 錯誤信息,變量num沒有進行賦值(未初始化錯誤)
}
補充說明
- 在棧楨當中,與性能調優關系最為密切的部分就是前面提到的局部變量表.
在方法的執行時,虛擬機使用局部變量表完成方法的傳遞. - 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收.
在靜態的方法當中是不可以引用這個this的
Slot的重復利用
棧楨當中的局部變量表中的槽位是可以重用的,如果一個局部變量過了其作用域,那么在其他作用域之后申明的新的局部變量就很喲與可能會復用過期局部變量的槽位,從而達到節省資源的目的
52 操作數棧(Operate Stack)
這個是數據結構中的一種,先進后出的特點
棧: 可以通過數組或鏈表來實現
棧可以看成滿足特殊條件的數組或者鏈表.
-
每一個獨立的棧楨當中除了包含局部變量表以外,還包含一個后進獻出(Last-In-First-Out)的操作數棧,也可以稱之為表達式棧(Expression Stack)
-
操作數棧,在方法執行過程中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push)/出棧(pop).
- 某些字節碼指令將值亞入操作數棧,其余的字節碼指令將操作數取出棧.使用它們后再把結果壓入棧.
- 比如:執行復制交換,求和等操作.
-
操組數棧,主要用于保存計算過程的中間計算結果,同時作為計算過程中變量臨時的存儲空間
-
操作數棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧楨也會隨之被創建出來,這個方法的操作數棧是空的
-
每一個操作數棧都會擁有一個明確的棧深度用于存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,為Max_stack的值.
-
棧中的任何一個元素都是可以任意Java數據類型
- 32bit的類型占用一個棧單位深度
- 64bit的類型占用兩個棧單位深度
-
操作數棧并非采用訪問索引的方式來進行數據訪問的,而是只能通過標準的入棧(push)和出棧(pop)操作來完成一次數據訪問.
-
如果被調用的方法帶有返回值的話,其返回值將會被壓入當期棧楨的操作數棧中,并更新PC寄存器中嚇一跳需要執行的字節碼指令.
-
操組數棧中元素的數據類型比如與字節碼執行的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證.
-
另外,我們說Java虛擬機的解釋引擎是基于棧的執行引擎,其中的棧值的就是操作數棧.
動態鏈接(或指向運行時常量池的方法引用)
動態鏈接
方法的調用
在java虛擬機當中,將符號引用轉換為調用方法的直接引用于方法的綁定機制相關
- 靜態鏈接:
當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,且運行期保持不變時.這種情況下降調用方法的符號引用轉換為直接引用的過程稱之為靜態鏈接. - 動態鏈接:
如果**如果被調用的方法在編譯期無法被確定下來,**也就是說,只能夠在程序運行期將調用方法的符號引用轉換為直接引用,由于這種引用轉換過程具備動態性,因此也就被稱之為動態鏈接.
方法的調用
對應的方法的綁定機制為: 早期綁定(Early Binding) 和晚期綁定(Late Binding). **綁定是一個字段,方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次.
- 早期綁定:
早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的類型進行綁定,這樣一來,由于明確了被調用的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號引用轉換為直接引用. - 晚期綁定:
如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之為晚期綁定
隨著高級語言的橫空出世,類似于Java一樣的基于面向對象的編程語言如今越來越多,盡管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持著一個共性,那就是都支持封裝\繼承,和多態等面向對象特性,既然這一類的編程語言具備多態特性,那么自然也就具備早期綁定和晚期綁定兩種綁定方式.
Java 中任何一個普通的方法其實都具備虛函數的特征,它們相當于C++語言中的虛函數(C++中則需要使用關鍵字Virtual來進行顯示的定義).如果在Java程序匯總不希望某個方法擁有虛函數的特征時,則可以使用關鍵字final
來標記這個方法.
虛方法和非虛方法
非虛方法
- 如果方法在編譯期就確定了具體的調用版本,這個版本在運行時是不可變的.這樣的方法稱為非虛方法
- 靜態方法,私有方法,final方法,實例構造器,父類方法都是非虛方法.
- 其他方法稱為虛方法.
多態的使用前提:
1 類的繼承關系
2 方法的重寫
虛擬機中提供了一些幾條方法調用指令:
- 普通調用指令:
- invokestatic: 調用靜態方法,解析階段確定唯一方法版本
- invokespecial: 調用方法,私有及父類方法,解析階段確定唯一方法版本
- invokevirtual: 調用所有虛方法
- invokeinterface: 調用接口方法
- 動態調用指令:
5. invokedynamic: 動態解析出需要調用的方法,然后執行
前4條指令固化在虛擬機內部,方法的調用執行不可人為干預,而invokedynamic指令則支持由用戶確定方法版本.其中invokestatic指令和invokespecial指令調用的方法稱為非虛方法,其余的(final修飾的除外)稱為虛方法.
練習一下
package com.atguigu.java6;/**\*解析*/
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() {super();}public Son(int age) {this();}/*** 不是重寫的父類方法* @param str*/public static void showStatic(String str) {System.out.println(str);}private void showPrivate(String str) {System.out.println("son private"+str);}public void show() {showStatic("autaied.com");super.showStatic("ggoaset");showPrivate("hellow!");super.showCommon();showFinal();showCommon();info();// MethodInterface in = null;
// in.methodA();}public void info() {}public static void main(String[] args) {new Son().show();}
}