Java虛擬機棧和PC寄存器


PC Register介紹

  • JVM中的程序計數寄存器(Program Counter Register)中,Register 的命名源於CPU的寄存器,寄存器存儲指令相關的現場信息。CPU只有把數據裝載到寄存器才能夠運行

  • 這里,並非是廣義上所指的物理寄存器,或許將其翻譯為PC計數器會更貼切

作用:

  • 用來存儲指向下一條指令的地址,也即將要執行的指令代碼。由執行引擎讀取下一條指令
  • 它是一塊很小的內存空間,幾乎可以忽略不計,也是運行速度最快的存儲區域
  • 在JVM規范中,每個線程都有它自己的程序計數器,是線程私有的,生命周期與線程的生命周期保持一致

程序執行過程大概是執行引擎從PC寄存器中讀到將要執行的下一條指令,然后操作局部變量表,操作數棧。。。。

為什么使用PC寄存器記錄當前線程的執行地址呢?

  • 因為CPU需要不停的切換各個線程,這時候切換回來以后,就得知道接着從那開始繼續執行
  • JVM的字節碼解釋器就需要通過改變PC寄存器值來明確下一條應該執行什么樣的字節碼指令

Java虛擬機棧介紹

Java虛擬機棧是什么

Java虛擬機棧(Java Virtual Machine Stack),早期也叫Java棧

每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次的Java方法調用。

  • 是線程私有的

虛擬機棧的生命周期

生命周期和線程一致

作用

主管Java程序的運行,它保存方法的局部變量(8種基本類型、對象的引用地址),部分結果,並參與方法的調用和返回。

優點

棧是一種快速有效的分配存儲方式,訪問速度僅次於程序計數器

對於棧來說不存在垃圾回收問題

開發中遇到的異常有哪些

  • Java虛擬機規范允許Java棧的大小是動態的或者是固定不變的

    • 如果采用固定大小的Java虛擬機棧,那每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量,Java虛擬機將會拋出一個StackOverflowError(例如遞歸調用沒有出口)
    • 如果Java虛擬機棧可以動態擴展,並且再嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutOfMemoryError
  • 設置棧內存大小

    • 我們可以使用參數-Xss選項來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度.

分配的棧內存越大越好么?

棧空間變大了,可分配的線程數就會變少,可能會出現OOM


棧的存儲單位 --棧幀

  • 每個線程都有自己的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在
  • 在這個線程上正在執行的每個方法都各自對應一個棧幀
  • 棧幀是一個內存區塊,是一個數據集,維系着方法執行過程中的各種數據信息

運行原理

  • JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循"后進先出"原則
  • 執行引擎運行的所有字節碼指令只針對當前棧幀進行操作.
  • 如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前幀

棧幀的內部結構

每個棧幀中存儲着:

  • 局部變量表(Local Variables)
  • 操作數棧(Operand Stack)
  • 動態鏈接(Dynamic Linking)
  • 方法返回地址(Return Address)
  • 一些附加信息

局部變量表

  • 定義為一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各類基本數據類型/對象引用,以及returnAddress類型
  • 局部變量表所需的容量大小是在編譯期確定下來的,並保存在方法的Code屬性的maximum local variables數據項中.在方法運行期間是不會改變局部變量表的大小的.
  • 由於局部變量表是建立在線程的棧上,是線程的私有數據,因此不存在數據的安全問題
  • 局部變量表中的變量只在當前方法調用中有效.在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程.當方法調用結束后,隨着方法棧幀的銷毀,局部變量表也會隨之銷毀

靜態方法的局部變量表:

  • 起始PC代表變量在字節碼文件中的起始位置
  • 長度代表變量的作用范圍

普通方法的局部變量表:

方法中定義的局部變量是否線程安全?

局部變量如果是內部產生,內部消亡的,都是線程安全的.例如:

//線程安全
public static String method1(){
    StringBuilder s1 = new StringBuilder();
    s1.append("a");
    s2.append("b");
}

如果從外部傳入,或者變量return出去了則不安全.


操作數棧

  • 主要用於保存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間.
  • 每一個操作數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,為max_stack的值.

動態鏈接(指向運行時常量池的方法引用)

  • 每一個棧幀內部都包含一個指向運行時常量池中該棧幀所述方法的引用。

    包含這個引用的目的就是為了支持當前方法的代碼能夠實現動態鏈接。比如: invokedynamic指令

  • 在Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作為符號引用保存在class文件的常量池里。

    比如: 描述一個方法調用了林外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,動態連接的作用就是為了將這些符號引用轉換為調用方法的直接引用

為什么需要常量池呢?

  • 常量池的作用,就是為了提供一些符號和常量,便於指令的識別

方法的調用

在JVM中,將符號引用轉換為調用方法的直接引用與方法的綁定機制相關.

  • 靜態鏈接

    當一個字節碼文件被裝在進JVM內部時,如果被調用的目標方法在編譯器可知,且運行期保持不變時,這種情況下將調用方法的符號引用轉換為直接引用的過程稱之為靜態鏈接

  • 動態鏈接

    如果被調用的方法在編譯期無法被確定下來,也就是說,只能夠在程序運行期將被調用方法的符號引用轉換為直接引用,由於這種引用轉換過程具備動態性,因此也就被稱之為動態鏈接

相對應的有前期綁定和后期綁定

  • 前期綁定

    若程序在執行前進行綁定,由編譯器和鏈接程序實現,叫做前期綁定

  • 后期綁定

    在運行時根據對象的類型進行綁定,叫做后期綁定,也叫動態綁定運行時綁定

某類語言具備多態特性,那么自然也就具備早期綁定和晚期綁定兩種綁定方式

Java中任何一個普通的方法其實都具備虛函數的特征,他們相當於C++中的虛函數.如果在Java程序中不希望某個方法擁有虛函數的特征時,則可以使用關鍵字final來標記這個方法

虛方法與非虛方法

  • 非虛方法
    • 如果方法在編譯期就確定了具體的調用版本,這個版本在運行時是不可變的,這樣的方法稱為非虛方法.
    • 靜態方法,私有方法,final方法,實例構造器,父類方法都是非虛方法
    • 其他方法稱為虛方法

動態類型語言和靜態類型語言

  • 靜態類型語言是判斷變量自身的類型信息;
  • 動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值才有類型信息,這是動態語言的一個重要特征.
- 靜態
	Java: String info = "longda";
- 動態
	JS: var name = "longda";
	python: name = "longda"

方法返回地址

  • 存放調用該方法的PC寄存器的值
  • 一個方法的結束,有兩種方式
    • 正常執行完成
    • 出現未處理的異常,非正常退出
  • 無論通過哪種方式退出,在方法退出后都返回到該方法被調用的位置.方法正常退出時,調用者的PC計數器的值作為返回地址,即調用該方法的指令的下一條指令的地址.而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息

正常完成出口和異常完成出口的區別在於:通過一場完成出口退出的不會給他的上層調用者產生任何的返回值.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM