1. 程式人生 > >JVM學習-執行時資料區域

JVM學習-執行時資料區域

[TOC] --- JVM學習-執行時資料區域 ## 前言 本系列文章梳理了對《深入理解Java虛擬機器》和《Java虛擬機器規範(Java SE 8版)》兩本書的學習內容。 其中本文對JAVA執行時的資料區的基礎知識知識進行整理。我們如果要對程式記憶體佔用高的問題進行分析,首先我們需要了解具體是什麼資料導致記憶體佔用高,然後對具體的問題再具體分析。 ## 執行時資料區 Java虛擬機器在執行Java程式的過程中,會把它所管理的記憶體劃分以下幾個區域:程式計數器、Java堆、方法區、虛擬機器棧 、本地方法棧。另外還有不在Java虛擬機器直接管理的堆外記憶體,也被稱為直接(Native)記憶體。 Java執行環境是單程序多執行緒的,多個執行緒通過執行緒切換輪流分配處理器執行時間的方式來實現的,而實際的執行緒排程是由作業系統控制的。使用者執行緒通過程式計數器和虛擬機器棧用來儲存執行緒執行所必須的上下文資訊。每個執行緒都有自己的程式計數器和虛擬機器棧。 ![20210119201727.png](https://img2020.cnblogs.com/blog/580757/202101/580757-20210119201728828-609619348.png) ### 程式計數器 程式計數器在JAVA虛擬機器規範中稱為`Program Counter Register`,即為PC暫存器,它可以看作當前執行緒所執行的位元組碼行號指示器,位元組碼直譯器工作時通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。 需要注意,只有執行的是非本地(Native)方法,程式暫存器才會記錄JAVA虛擬機器正在執行的位元組碼指令地址,若當前執行方法是本地方法,則程式計數器的值為空(Undefined)。 ### Java虛擬機器棧 和程式計數器一樣,每一個JAVA虛擬機器執行緒都有自己**私有的**JAVA虛擬機器棧。Java虛擬機器規範允許Java虛擬機器棧被實現為固定大小,也允許動態擴充套件和收縮。 > 當執行緒請求的棧深度大於虛擬機器允許的棧深度,則會丟擲`StackOverflowError`異常。當棧動態擴充套件無法申請到足夠的記憶體時,則會排除`OutOfMemoryError`異常。 > ![20210110204312.png](https://img2020.cnblogs.com/blog/580757/202101/580757-20210110204309776-529317770.png) 每個方法執行的時候當前執行執行緒會在Java虛擬機器棧中分配當前方法的棧幀,用於儲存區域性變量表、運算元棧、動態連結。當方法執行完後,棧幀就會被丟棄,繼續執行下一個棧幀。 #### 區域性變量表 區域性變量表用於儲存基礎資料型別、物件引用和returnAddress型別。 區域性變量表實際上就是一個數組,陣列的一個元素被稱為區域性變數槽(Slot),一個槽大小為**32位**。區域性變量表所需的記憶體空間是在編譯時分配,執行時區域性變數所佔用的空間是確定的,也就是陣列的槽數。基礎資料型別佔用1個或2個槽,物件引用和returnAddress型別佔用1個槽。 ##### 基礎資料型別 JAVA有8個基礎資料型別:boolean、byte、char、short、int、float、long、double。其中long和double佔用2個槽,其他基礎資料型別都佔用1個槽。 區域性變數使用索引進行定位訪問。區域性變數的索引值從0開始。呼叫例項方法時,第0個區域性變數用於儲存當前物件例項(即this關鍵字)。區域性變數從第1個開始;而呼叫靜態方法時,區域性變數從第0個開始。 ##### 物件引用 物件引用包含指向物件的起始地址的引用指標或指向代表物件的控制代碼。 其中指向物件起始地址的引用可能是物件、陣列或介面。 ##### returnAddress `returnAddress`是一個指標,指向一條虛擬機器指令的操作碼。這些操作碼包括`jsr`、`ret`和`jsr_w`。在JDK 7之前,這些操作碼用於實現finally語句塊的跳轉和返回。從JDK 7開始,虛擬機器已不允許這幾個操作碼了,改為冗餘finally塊程式碼(在每個catch塊後生成冗餘的finally程式碼)實現,因此returnAddress型別基本就沒用了。 #### 運算元棧 每個棧幀內部都包含一個後進先出(LIFO)的運算元棧。運算元棧的最大深度由編譯期決定。運算元棧中儲存了局部變量表或物件例項中的常量或變數值。在呼叫方法時,也儲存呼叫方法的引數和返回值。 > 若區域性變數是long或double型別,則需要佔用2個單位的棧深度。 舉個例子,當執行以下程式碼。右邊註釋的`[]`表示運算元棧,左邊時棧底,右邊是棧頂。 ``` java // //[] int a = 1; //[1] int b = 2; //[1,2] int c = a+b;//[3]->[] ``` > 注意:`c=a+b`,通過`iadd`讀取棧頂的2個數相加後重新入到運算元棧,因此運算元棧中的內容為`3`,然後從運算元棧中出棧儲存到c變數中,運算元棧就空了。 #### 動態連結 每個棧幀內部都包含當前方法所在型別的執行時常量池的引用,以便對當前方法的程式碼實現動態連結。 在編譯時,會將呼叫的方法或成員變數通過符號引用的方式儲存。動態連結的作用就是將以符號引用所表示的方法轉換為方法的直接引用。 > 符號引用也被稱為描述符(Descriptor),是通過特定的語法來表示的。呼叫的方法的符號引用稱為方法描述符(Method Descriptor),成員變數稱為欄位描述符(Parameter Descriptor)。 #### 方法返回地址 當通過動態連結呼叫其他類方法時,棧幀中需要儲存被呼叫的位置,以便方法呼叫完成後可以返回到被呼叫時的位置。 當方法正常呼叫完成後,則棧幀正常恢復區域性變量表、運算元棧和呼叫者的程式計數器正確的位置,若有返回值,則將返回值壓入到呼叫者的棧幀的運算元棧中。 當方法異常呼叫完成後,則會導致Java虛擬機器丟擲異常,若當前方法沒有任何可以處理該異常的異常處理器,則當前方法的運算元棧和區域性變量表都會被丟棄,隨後恢復到呼叫者的棧幀,此時不會有任何返回值壓入到呼叫者的運算元棧中。同時將異常交易給呼叫者的異常處理器處理。 ### Java堆 Java虛擬機器中,Java堆用於儲存各種物件例項,是Java虛擬機器所管理的記憶體中最大的一塊,並且該記憶體被所有執行緒所共享。 Java棧由執行緒自動建立和銷燬,棧幀由方法的建立和銷燬自動管理。而Java堆則由垃圾收集器進行自動收集並回收。垃圾收集器在不同場景下通過最優的垃圾收集演算法對垃圾繼續收集。 ![20210117221122.png](https://img2020.cnblogs.com/blog/580757/202101/580757-20210117221120196-1321814857.png) 為了提高垃圾收集效能,Java堆將空間分為新生代、老年代。新生代又被分為Eden區和Survivor區。 通常情況下物件都被建立在新生代中的Eden區,隨後隨著垃圾回收的進行,未被回收的物件則被逐步從新生代轉移到老年代,具體垃圾回收相關細節不在這裡討論。 > 若新生代的空間不足以建立物件,則可能直接被建立到老年代 ### 方法區 方法區用於儲存被虛擬機器載入的類資訊、靜態變數、JIT後的程式碼位元組碼快取、執行池常量。虛擬機器規範把方法區列為堆的一部分,但是虛擬機器實現可以不實現方法區的自動垃圾回收,而是依賴於對常量池和型別的解除安裝來完成。 ![20210123142339.png](https://img2020.cnblogs.com/blog/580757/202101/580757-20210123142338559-499372559.png) #### 型別資訊 型別資訊包括程式碼中的類名、修飾符、欄位描述符和方法描述符。在class檔案中,型別資訊並不是我們程式碼中直接使用的字串,而是由內部的表現形式的字串。 ##### 欄位描述符 欄位描述符用於表示類、例項和區域性變數。比如用`L`表示物件,用`[`表示陣列等。 欄位描述符內部解釋表如下圖所示。 |欄位描述符|型別|含義| |-|-|-| |B|byte|有符號的位元組型數| |C|char|unicode字元碼點,UFT-16編碼| |D|double|雙精度浮點數| |F|float|單精度浮點數| |I|int|整型數| |J|long|長整數| |L className|reference|className的類的例項| |S|short|有符號短整數| |Z|boolean|布林值true/false| |[|referebce|一維陣列| ##### 方法描述符 方法描述符表示0個或多個引數描述符以及1個返回值描述符,用於表示方法的簽名信息。若返回值為void則用V表示。 方法描述符的格式: `(引數描述符)` + `返回值描述符`。 比如`Object m(int i, double d, Thread t)(){}`方法可以表示為`(IDLjava/lang/Thread;)Ljava/lang/Object;`。 * `I`是`int`型別的欄位描述符 * `D`是`double`型別的欄位描述符 * `Ljava/lang/Thread`是`Thread`型別的內部描述符 * `Ljava/lang/Object`是方法的返回值為`object`型別 > 方法描述符分割各識別符號的符號不用`.`,而用`/`表示。 ``` java public class SymbolTest{ private final static String staticParameter = "1245"; public static void main(String[] args) { String name = "jake"; int age = 54; System.out.println(name); System.out.println(age); } } ``` 上面一個簡單的例子,編譯通過後,可以通過`javap -s xxx.class`命令檢視內部簽名。 ``` cmd D:\study\java\symbolreference\out\production\symbolreference>javap -s com.company.SymbolTest Compiled from "SymbolTest.java" public class com.company.SymbolTest { public com.company.SymbolTest(); descriptor: ()V public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V } ``` 可以看出無參建構函式的方法描述符為`()V`,main方法的方法描述符為`([Ljava/lang/String;)V` #### 執行時常量池 執行時常量池儲存了編譯期常量和執行期常量。編譯期常量是在編譯時編譯器生成的字面量和符號引用。字面量指的是程式碼中直接寫的字串或數值等常量或宣告為`final`的常量值。比如`string str="abc"`或`int value = 1`這裡的`abc`和`1`都屬於字面量。執行期常量值的是執行期產生的新的常量,比如`String.intern()`方法產生的字串常量會被儲存到執行時常量池快取起來複用。 執行時常量在方法區中分配,在載入類和介面到虛擬機器後就會建立對應的執行時常量。若建立執行時常量所需的記憶體空間超過了方法區所能提供的最大值,則會丟擲`OutOfMemoryError`異常。 還是上面的程式碼示例,通過`javap -v`可以輸出包括執行時常量的附加資訊。下面列出了了部分常量輸出內容。 ``` shell D:\study\java\symbolreference\out\production\symbolreference>javap -v com.company.SymbolTest ... Constant pool: #1 = Methodref #7.#28 // java/lang/