天天看點

JVM- 運作時資料區-棧JVM- 運作時資料區-棧

目錄

  • JVM- 運作時資料區-棧
    • 背景
    • Java虛拟機棧
      • 生命周期
      • 作用
      • 優點
      • 缺點
      • 相關問題
    • 棧的特點(優點)
    • 設定棧記憶體大小
    • 棧的兩種異常
    • 棧運作原理
    • 棧中的存儲
    • 棧幀中的存儲
      • 局部變量表
        • 存儲單元:Slot
        • 靜态變量與局部變量的對比
        • 變量的分類:
      • 操作數棧
        • 操作數棧
        • 棧頂緩存
      • 動态連結
        • 方法的調用
          • 靜态連結
          • 動态連結
        • 方法的綁定機制
          • 早期綁定
          • 晚期綁定
        • 虛函數
        • 非虛方法
        • 調用指令:
          • 普通調用指令:
          • 動态調用指令:
        • 方法的調用:關于invokedynamic指令
        • 動态類語言 與 靜态類語言
        • 方法的調用:方法重寫的本質
          • Java語中方法重寫的本質:
          • IllegalAccessError介紹
        • 方法的調用:虛方法表
      • 方法傳回位址(return address)
      • 一些附加資訊
    • 問題

JVM- 運作時資料區-棧

背景

由于跨平台性的設計,Java的指令都是根據棧來設計的。

Java虛拟機棧

Java虛拟機棧 Java Virtual Machine Stack,早期也叫Java棧。

每個線程建立時都會建立一個虛拟機棧,其内部儲存一個個棧幀 Stack Frame,對應着一次次的Java方法的調用

線程是私有的

生命周期

生命周期和線程一緻

作用

主管Java程式運作,它儲存方法的局部變量、部分結果,并參與方法的調用和傳回。

• 局部變量 VS 成員變量(或屬性)

• 基本資料變量 VS 引用類型變量(類、接口、數組)

優點

• 跨平台,指令集小,編譯器容易實作

缺點

• 性能下降,實作同樣的功能需要更多的指令

相關問題

有不少Java開發人員一提到JAVA記憶體結構,就會非常粗粒度的将JVM中的資料區了解為僅有JAVA堆(Heap)和JAVA棧(Stack)?why?

  • 棧是運作時的機關
  • 堆是存儲的機關
  • 棧是解決程式的運作問題,即程式如何執行或如何處理資料。
  • 堆解決的是資料存儲的問題,即資料怎麼放,放哪兒
JVM- 運作時資料區-棧JVM- 運作時資料區-棧

棧的特點(優點)

  • 棧是一種快速有效的配置設定存儲方式,通路速度僅次于程式計數器
  • JVM直接對Java棧的操作隻有兩個:
    • 每個方法執行,伴随着進棧(入棧、壓棧)
    • 執行結束後的出棧工作
  • 對于棧來說不存在垃圾回收問題

設定棧記憶體大小

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

棧的兩種異常

Java虛拟機規範允許Java棧的大小是動态的或者固定不變的

  • 如果采用固定大小的Java虛拟機棧,那每一個線程的Java虛拟機棧容量可以線上程建立的時候獨立標明。如果線程請求配置設定的棧容量超過Java虛拟機允許的最大容量,Java虛拟機将會抛出一個StackOverflowError異常
  • 如果Java虛拟機棧可以動态擴充,并且嘗試擴充的時候無法申請到足夠的記憶體,或者在建立新的線程時沒有足夠的記憶體去建立對應的虛拟機棧,那麼Java虛拟機将會抛出一個OutofMemoryError異常
    JVM- 運作時資料區-棧JVM- 運作時資料區-棧

棧運作原理

  • JVM直接對Java棧的操作隻有兩個,就是對棧幀的壓棧和出棧。
    • 遵循“先進後出”/“後進先出”原則。
  • 在一條活動線程中,一個時間點上,隻會有一個活動的棧幀。
    • 即隻有目前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為目前棧幀Current Frame,與目前棧幀相對應的方法就是目前方法Current Method,定義這個方法的類就是目前類Current Class。
  • 執行引擎運作的所有位元組碼指令 隻針對目前棧幀 進行操作。
  • 如果在該方法中調用了其他方法,對應的新的棧幀會被建立出來,放在棧的頂端,稱為新的目前幀。
  • 不同線程中所包含的棧幀是不允許存在互相作用的,
    • 即不可能在一個棧幀之中引用另外一個線程的棧幀。
  • 如果目前方法調用了其他方法,方法傳回之際,目前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛拟機會丢棄目前棧幀,使前一個棧幀重新成為目前棧幀。
  • Java方法有兩種傳回函數的方式

    - 一種是正常的函數傳回,使用return指令;

    - 另一種是抛出異常。

    • 不管使用哪種方式,都會導緻棧幀被彈出。
JVM- 運作時資料區-棧JVM- 運作時資料區-棧

棧中的存儲

  • 每個線程都有自己的棧,棧中的資料都是以棧幀(Stack Frame)的格式存在。
  • 在這個線程上正在執行的每個方法都各自對應一個棧幀(Stack Frame)。
  • 棧幀是一個記憶體區塊,是一個資料集,維系着方法執行過程中的各種資料資訊。
    • 複習:oop的基本概念;

      - 類中基本結構:filed(屬性、字段、域)、method

棧幀中的存儲

  • 局部變量表(Local Variables)
  • 操作數棧(operand Stack)(或表達式棧)
  • 動态連結(Dynamic Linking)(或指向運作時常量池的方法引用)
  • 方法傳回位址(Return Address)(或方法正常/異常退出的定義)
  • 一些附加資訊
JVM- 運作時資料區-棧JVM- 運作時資料區-棧

局部變量表

局部變量表也被稱為局部變量數組或本地變量表

– 定義為一個數字數組,主要用于存儲方法參數和定義在方法體内的局部變量,這些資料類型包括各類基本資料類型、對象引用(reference),以及returnAddress類型。

  • 由于局部變量表是建立線上程的棧上,是線程的私有資料,是以不存在資料安全問題
  • 局部變量表所需的容量大小是在編譯期确定下來的,并儲存在方法的code屬性的maxinum local variables資料項中。在方法運作期間是不會改變局部變量表的大小的。
  • 方法嵌套調用的次數由棧的大小決定。一般來說,棧越大,方法嵌套調用次數越多。對于一個函數而言,他的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以滿足方法調用所需傳遞的資訊增大的需求。進而函數調用就會占用更多的棧空間,導緻其嵌套調用次數就會減少。
  • 局部變量表中的變量隻在目前方法調用中有效。在方法執行時,虛拟機通過使用局部變量表完成參數值到參數變量清單的傳遞過程。當方法調用結束後,随着方法棧幀的銷毀,局部變量表也會随之銷毀。

& 在棧幀中,與性能調優關系最為密切的部分就是局部變量表。在方法執行時,虛拟機使用局部變量表完成方法的傳遞。

& 局部變量表中的變量也是重要的垃圾回收根結點,隻要被局部變量表中直接或間接引用的對象都不會被回收。

JVM- 運作時資料區-棧JVM- 運作時資料區-棧

存儲單元: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的局部變量值時,隻需要使用前一個索引即可(eg:通路long或double類型變量)
  • 如果目前幀是由構造方法或執行個體方法建立的,那麼該對象引用this将會存放在index0的slot處,其餘的參數按照參數表順序繼續排列。

棧幀中的局部變量表中的槽位是可以重用的。

- 如果一個局部變量過了其作用域,那麼在其他作用域之後申明的新的局部變量表就很有可能會複用過期的局部變量的槽位,進而達到節省資源的目的。

JVM- 運作時資料區-棧JVM- 運作時資料區-棧

靜态變量與局部變量的對比

  • 參數表配置設定完畢後,再根據方法體内定義的變量的順序和作用域配置設定。
  • 變量表有兩次初始化的機會,第一次是在“準備階段”,執行系統初始化,對類變量設定零值,另一次則是在“初始化”階段,賦予代碼中定義的初始值。
  • 和類變量初始化不同的是,局部變量表不存在系統初始化的過程,這意味着一旦定義了局部變量,則必須人為的初始化,否則無法使用。

變量的分類:

  • A 按資料類型分:
    • 基本資料類型
    • 引用資料類型
  • B 按類聲明的位置分:
    • 成員變量:在使用前,都經曆過預設初始化指派。

      ○ 類變量(靜态變量):

      linking的prepare階段:給類變量預設指派–>initial階段:給類變量顯示指派即靜态代碼塊指派。

      ○ 執行個體變量:

      随着對象的建立,會在堆空間中配置設定執行個體變量空間,并進行預設指派。

    • 局部變量:在使用前,必須要進行顯式指派,否則,編譯不通過。
      JVM- 運作時資料區-棧JVM- 運作時資料區-棧

操作數棧

  • 每一個獨立的棧幀中除了包含局部變量表以外,還有包含一個後進先出Last-In-First-Out的操作數棧,也可以稱之為表達式棧Expression Stack。
  • 操作數棧,在方法執行過程中,根據位元組碼指令,往棧中寫入資料或提取資料,即入棧push/出棧pop。
    • 某些位元組碼指令将值壓入操作數棧,其餘的位元組碼指令将操作數取出棧。使用它們後再把結果壓入棧。
    • 比如:執行複制、交換、求和等操作
  • 如果被調用的方法帶有傳回值的話,其傳回值将會被壓入目前棧幀的操作數棧中,并更新PC寄存器中下一條需要執行的位元組碼指令。
  • 操作數棧中元素的資料類型必須與位元組碼指令的序列嚴格比對,這由編譯器在編譯期間進行驗證,同時在類加載過程中的類校驗階段的資料流分析階段要再次驗證。
JVM- 運作時資料區-棧JVM- 運作時資料區-棧

操作數棧

  • Java虛拟機的解釋引擎是基于棧的執行引擎,其中的棧指的是操作數棧。
  • 操作數棧,主要用于儲存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間。
  • 操作數棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會随之被建立出來,這個方法的操作數棧是空的。
  • 每一個操作數棧都會擁有一個明确的棧深度用于存儲數值,其所需的最大深度在編譯期就定義好了,儲存在方法的code屬性中,為max_stack的值。
  • 棧中的任何一個元素都是可以任意的Java資料類型。
    • 32bit的類型占用一個棧機關深度
    • 64bit的類型占用兩個棧機關深度
  • 操作數棧并非采用通路索引的方式來進行資料通路的,而是隻能通過标準的入棧(push)和出棧(pop)操作來完成一次資料通路。

棧頂緩存

  • 基于棧式架構的虛拟機所使用的零位址指令更加緊湊,在完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味着将需要更多的指令分派instruction dispatch次數和記憶體讀/寫次數。
  • 由于操作數是存儲在記憶體中的,是以頻繁地執行記憶體讀/寫操作必然會影響執行速度。
    • 為了解決這個問題,HotSpot JVM的設計者們提出了棧頂緩存ToS(Top-of-Stack Cashing)技術。
    • 将棧頂元素全部緩存在實體CPU的寄存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率。
      JVM- 運作時資料區-棧JVM- 運作時資料區-棧

動态連結

指向運作時常量池的方法引用

  • 每一個棧幀内部都包含一個指向運作時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支援目前的代碼能夠實作動态連結Dynamic Linking。比如:invokedynamic指令
  • 在Java源檔案被編譯到位元組碼檔案中時,所有的變量和方法引用都作為符号引用Symbolic Reference儲存在class檔案的常量池裡。比如:描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符号引用來表示的,那麼動态連結的作用就是為了将這些符号引用轉換為調用方法的直接引用。
JVM- 運作時資料區-棧JVM- 運作時資料區-棧

方法的調用

在JVM中,将符号引用轉換為調用方法的直接引用,與方法的綁定機制相關。

靜态連結

當一個位元組碼檔案被裝在進JVM内部時,如果被調用的目标方法在編譯期可知,且運作期保持不變時。這種情況下将調用方法的符号引用轉換為直接引用的過程稱為靜态連結。

動态連結

如果被調用的方法在編譯期無法确定下來,隻能夠在程式運作過程中将調用方法的符号引用轉換為直接引用。由于這種引用轉換過程具備動态性,是以稱為動态連結。

方法的綁定機制

早期綁定Early Binding和晚期綁定Late Binging。

綁定是一個字段、方法或者類在符号引用被替換為直接引用的過程,這僅僅發生一次。

早期綁定

早期綁定就是指被調用的目标方法如果在編譯期可知,且運作期保持不變時,即可将這個方法與所屬的類型進行綁定,這樣一來,由于明确了被調用的目标方法究竟是哪一個,是以也就可以使用靜态連結的方式将符号引用轉換為直接引用。

晚期綁定

如果被調用的方法在編譯期無法被确定下來,隻能夠在程式運作期根據實際的類型綁定相關的方法,這種綁定方式也就被稱為晚期綁定。

JVM- 運作時資料區-棧JVM- 運作時資料區-棧

虛函數

随着進階語言的橫空出世,類似于Java一樣的基于面向對象的程式設計語言如今越來越多,盡管這類變成語言在文法風格上存在一定的差别,但是它們彼此之間始終保持着一個共性,那就是都支援封裝、繼承和多态等面向對象特性,既然這一類的變成語言具備多态特性,那麼自然也就具備早期綁定和晚期綁定兩種綁定方式。

Java中任何一個普通的方法其實都具備虛函數的特征,它們相當于C++語言中的虛函數(C++中則需要使用關鍵字virtual來顯式定義)。如果在Java程式中不希望某個方法擁有虛函數的特征時,則可以使用關鍵字final來标記這個方法。

非虛方法

  • 如果方法在編譯期就确定了具體的調用版本,這個版本在運作時是不可變的。這樣的方法稱為非虛方法。
  • 靜态方法、私有方法、final方法、執行個體構造器、父類方法都是非虛方法。
  • 其他方法稱為虛方法。
子類對象的多态性的使用前提
A. 類的繼承關系;
B. 方法的重寫

調用指令:

普通調用指令:
  1. invokestatic 調用靜态方法,解析階段确定唯一方法版本。
  2. invokespecial 調用方法、私有及父類方法,解析階段确定唯一方法版本
  3. invokevirtual 調用所有虛方法
  4. invokeinterface 調用接口方法
動态調用指令:
  1. Invokedynamic 動态解析出需要調用的方法,然後執行

    前四條指令固化在虛拟機内部,方法的調用執行不可人為的幹預,而invokedynamic指令則支援由使用者确定方法版本。

    其中invokestatic指令和invokespecial指令調用的方法稱為非虛方法,其餘的(final修飾的除外)稱為虛方法。

JVM- 運作時資料區-棧JVM- 運作時資料區-棧

方法的調用:關于invokedynamic指令

• JVM位元組碼指令集一直比較穩定,一直到Java7中才增加了一個invokedynamic指令,這是Java為了實作“動态類型語言”支援而做的一種改進。

• 但是在Java7中并沒有提供直接生成invokedynamic指令的方法,需要借助ASM這種底層位元組碼工具來産生invokedynamic指令。直到Java8的Lambda表達式的出現,才實作了對invokedynamic指令的生成,在Java中才有了直接的生成方式。

• Java7中增加的動态語言類型支援的本質是對Java虛拟機規範的修改,而不是對Java語言規則的修改,這一塊相對來講比較複雜,增加了虛拟機中的方法調用,最直接的受益者就是運作在Java平台的動态語言的編譯器。

動态類語言 與 靜态類語言

• 動态類語言和靜态類型語言兩者的差別就在于對類型的檢查是在編譯期還是在運作期,滿足前者就是靜态類型的語言,反之就是動态類語言。

• 再直白一點就是:靜态類型語言是判斷變量自身的類型資訊;動态類型語言是判斷變量值的類型資訊,變量沒有類型資訊,變量值才有類型資訊,這是動态語言的一個重要特性。

JVM- 運作時資料區-棧JVM- 運作時資料區-棧

方法的調用:方法重寫的本質

Java語中方法重寫的本質:
  1. 找到操作數棧頂的第一個元素所執行的對象的實際類型,記作 C。
  2. 如果在類型C中找到與常量中的描述符合簡單名稱都相同的方法,則進行通路權限校 驗,如果通過則傳回這個方法的直接引用,查找過程結束;如果不通過,則傳回 java.lang.IllegalAccessError異常。
  3. 否則,按照繼承關系從下往上依次對C的各個父類進行第2步的搜尋和驗證過程。
  4. 如果始終沒有找到合适的方法,則抛出java.lang.AbstractMethodErrory異常。
IllegalAccessError介紹
  • 程式試圖通路或修改一個屬性或者調用一個方法,
  • 這個屬性或方法,沒有通路權。
  • 一般的,會引起編譯器異常。
  • 這個錯誤如果發生在運作時,就說明一個類發生了不相容的改變。

方法的調用:虛方法表

  • 在面向對象的程式設計中,會很頻繁的使用到動态配置設定。
    • 如果在每次動态配置設定的過程中都要重新在類的方法中繼資料中搜尋合适的目标的話,就可能影響執行效率。是以,為了提高性能,JVM采用在類的方法區建立一個虛方法表virtual method table(非虛方法不會出現在表中)來實作。使用索引表來替代查找。
  • 每個類都有一個虛方法表,表中存放着各個方法的實際入口。
  • 虛方法表會在類加載的連結階段被建立并開始初始化,類的變量初始值準備完成後,JVM會把該類的方法表也初始化完畢。
    JVM- 運作時資料區-棧JVM- 運作時資料區-棧

方法傳回位址(return address)

存放調用該方法的PC寄存器的值。

  • 一個方法的結束,有兩種方式:
    • 正常執行完成;出現未處理的異常,非正常退出。
  • 無論哪種退出,在方法退出後都會傳回到該方法被調用的位置。
    • 方法正常退出時,調用者的PC計數器的值作為傳回位址,即調用該方法的指令的下一條指令的位址。
    • 方法異常退出的,傳回位址是要通過異常表來确定,棧幀中一般不會儲存這部資訊。
  • 本質上,方法的退出就是目前棧幀出棧的過程。
    • 此時,需要恢複上層方法的局部變量表、操作數棧、将傳回值壓入調用者棧幀的操作數棧、設定PC寄存器值等,讓調用者方法繼續執行下去。
  • 正常完成出口和異常完成出口的差別在于:
    • 通過異常完成出口退出的不會給他的上層調用者産生任何的傳回值。

當一個方法開始執行後,隻有兩種方式可以退出方法:

  1. 執行引擎遇到任意一個方法傳回的位元組碼指令return,會傳回值傳遞給上層的方法調用者,簡稱正常完成出口。

    一個方法在正常調用完成之後究竟需要使用哪一個傳回指令還需要根據方法傳回值的實際資料類型而定。

    在位元組碼指令中,傳回指令包含ireturn(當傳回值是boolean、byte、char、short和int類型時使用)、lreturn(當傳回值是long類型時使用)、freturn(當傳回值是float類型時使用) 、dreturn(當傳回值是double類型時使用)以及areturn(當傳回值是引用類型時使用),另外還有一個return指令供聲明為void的方法、執行個體初始化方法、類和接口的初始化方法使用。

  2. 在方法執行過程中遇到了異常Exception,并且這個異常沒有在方法内進行處理,也就是隻要在本方法的異常表中沒有搜尋到比對的異常處理器,就會導緻方法退出。簡稱為異常完成出口。
    方法執行過程中抛出異常時的異常處理,存儲在一個異常處理表,友善在發生異常的時候找到處理異常的代碼。
JVM- 運作時資料區-棧JVM- 運作時資料區-棧

一些附加資訊

棧幀中還允許攜帶與Java虛拟機實作相關的一些附加資訊

eg:對程式調試提供支援的資訊。

問題

  • 問題1:舉例棧溢出的情況
    • 棧溢出會報異常:StackOverflowError
    • 可以通過Xss設定棧的大小;
    • 棧的大小通過動态調整的時候,記憶體不足會出現OOM異常
  • 問題2:調整棧的大小,就能保證不出現溢出嗎?
    • 不能
    • 調整棧的大小可能會延緩棧的溢出。
  • 問題3:配置設定的棧記憶體越大越好嗎?
    • 不是
    • 配置設定的棧記憶體越大雖然可以延緩棧的溢出,但是會使得記憶體中的線程數變少,不一定越好。
  • 問題4:垃圾回收是否會涉及到虛拟機棧?
    • 不會
    • 棧隻存在出棧入棧操作,有可能會溢出,但不會出現垃圾回收操作。
  • 問題5:方法中定義的局部變量是否線程安全?
    • 具體問題具體分析
    • 方法内部定義的局部變量如果在方法入棧是生命周期開始,出棧的時候生命周期就結束,對于其他的棧幀無法操作的情況下是線程安全的,或者變量的定義類型本身就是線程安全的,否則是不安全的。
JVM- 運作時資料區-棧JVM- 運作時資料區-棧