深入理解JVM-java虛擬機棧


1.java虛擬機棧

  

 

  1. Java虛擬機棧也是線程私有的,它的生命周期與線程相同(隨線程而生,隨線程而滅

  2. 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;

   如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常;

  (當前大部分JVM都可以動態擴展,只不過JVM規范也允許固定長度的虛擬機棧)

  3. Java虛擬機棧描述的是Java方法執行的內存模型:每個方法執行的同時會創建一個棧幀

   對於我們來說,主要關注的stack棧內存,就是虛擬機棧中局部變量表部分

2.棧幀(Stack Frame)

  棧幀(Stack Frame)是用於支持虛擬機進行方法調用方法執行的數據結構。它是虛擬機運行時數據區中的java虛擬機棧的棧元素

  棧幀存儲了方法的局部變量表操作數棧動態連接方法返回地址等信息。

  每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機里面從入棧到出棧的過程。

  注意:

在編譯程序代碼的時候,棧幀中需要多大的局部變量表內存多深的操作數棧都已經完全確定了。

因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。

  棧結構圖如下:

  

  注意:

  在活動線程中,只有位於棧頂的棧幀才是有效的,稱為當前棧幀,與這個棧幀相關聯的方法稱為當前方法

  執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作。

3.局部變量表

   1.局部變量表(Local Variable Table)是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量

    並且在Java編譯為Class文件時,就已經確定了該方法所需要分配的局部變量表的最大容量

   2.局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)「String是引用類型」,

    對象引用(reference類型) 和 returnAddress類型(它指向了一條字節碼指令的地址)

  注意:

  很多人說:基本數據和對象引用存儲在棧中

  當然這種說法雖然是正確的,但是很不嚴謹,只能說這種說法針對的是局部變量

  局部變量存儲在局部變量表中,隨着線程而生,線程而滅。並且線程間數據不共享。

  但是,如果是成員變量,或者定義在方法外對象的引用,它們存儲在堆中

  因為在堆中,是線程共享數據的,並且棧幀里的命名就已經清楚的划分了界限 : 局部變量表!

4.變量槽(Variable Slot)

  局部變量表的容量以變量槽為最小單位,每個變量槽都可以存儲32位長度的內存空間,例如boolean、byte、char、short、int、float、reference。

  對於64位長度的數據類型(long,double),虛擬機會以高位對齊方式為其分配兩個連續的Slot空間,也就是相當於把一次long和double數據類型讀寫分割成為兩次32位讀寫。

  擴展知識點:

Slot復用 

為了盡可能節省棧幀空間,局部變量表中的Slot是可以重用的,
也就是說當PC計數器的指令指已經超出了某個變量的作用域(執行完畢),
那這個變量對應的Slot就可以交給其他變量使用。

優點 : 節省棧幀空間。 

缺點 : 影響到系統的垃圾收集行為。

(如大方法占用較多的Slot,執行完該方法的作用域后沒有對Slot賦值或者清空設置null值,垃圾回收器便不能及時的回收該內存。)

5.reference(對象實例的引用)

  我的理解是:一個超鏈接

  一般來說,虛擬機都能從引用中直接或者間接的查找到對象的以下兩點 :

  a.在Java堆中的數據存放的起始地址索引。

  b.所屬數據類型在方法區中的存儲類型。

  例如:我們在創建一個Student對象時的數據存儲結構:

  

6.動態連接

  每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,

  持有這個引用是為了支持方法調用過程中的動態連接(Dynamic Linking)。

  在類加載階段中的解析階段會將符號引用轉為直接引用,這種轉化也稱為靜態解析

  另外的一部分將在每一次運行時期轉化為直接引用。這部分稱為動態連接

  這里簡單提一下動態連接的概念,后面在詳細講解.

7.方法出口

  當一個方法開始執行后,只有2種方式可以退出這個方法 :

  方法返回指令 : 執行引擎遇到一個方法返回的字節碼指令,這時候有可能會有返回值傳遞給上層的方法調用者,這種退出方式稱為正常完成出口。

  異常退出 : 在方法執行過程中遇到了異常,並且沒有處理這個異常,就會導致方法退出。

  無論采用任何退出方式,在方法退出之后,都需要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能需要在棧幀中保存一些信息。

    一般來說,方法正常退出時,調用者的PC計數器的值可以作為返回地址,棧幀中會保存這個計數器值。

  而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。

8.實戰案例

  如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常

 1 package com.wfd360.demo01;
 2 
 3 /**
 4  * @Copyright (C)
 5  * @Author: LI DONG PING
 6  * @Date: 2019-07-15 17:17
 7  * @Description: 棧內存溢出測試
 8  * <p>
 9  * 測試代碼設計思路
10  * 修改默認堆棧大小后,利用遞歸調用一個方法,達到棧深度過大的異常目的,同時在遞歸調用過程中記錄調用此次,得出最大深度的數據
11  * jvm參數
12  * -Xss 180k:設置每個線程的堆棧大小(最小180k),默認是1M
13  */
14 public class TestStackOverflowErrorDemo {
15     //棧深度統計值
16     private int stackLength = 1;
17 
18     /**
19      * 遞歸方法,導致棧深度過大異常
20      */
21     public void stackLeak() {
22         stackLength++;
23         stackLeak();
24     }
25 
26     /**
27      * 啟動方法
28      * 測試結果:當-Xss 180k為180k時,stackLength~=1544,隨着-Xss參數變大時stackLength值隨之變大
29      * @param args
30      */
31     public static void main(String[] args) {
32         TestStackOverflowErrorDemo demo = new TestStackOverflowErrorDemo();
33         try {
34             demo.stackLeak();
35         } catch (Throwable e) {
36             System.out.println("當前棧深度:stackLength=" + demo.stackLength);
37             e.printStackTrace();
38         }
39     }
40 }
View Code

  測試結果:

  

 

  完美!

 


免責聲明!

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



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