JVM學習-運行時數據區域


JVM學習-運行時數據區域

前言

本系列文章梳理了對《深入理解Java虛擬機》和《Java虛擬機規範(Java SE 8版)》兩本書的學習內容。

其中本文對JAVA運行時的數據區的基礎知識知識進行整理。我們如果要對程序內存佔用高的問題進行分析,首先我們需要了解具體是什麼數據導致內存佔用高,然後對具體的問題再具體分析。

運行時數據區

Java虛擬機在執行Java程序的過程中,會把它所管理的內存劃分以下幾個區域:程序計數器、Java堆、方法區、虛擬機棧 、本地方法棧。另外還有不在Java虛擬機直接管理的堆外內存,也被稱為直接(Native)內存。

Java運行環境是單進程多線程的,多個線程通過線程切換輪流分配處理器執行時間的方式來實現的,而實際的線程調度是由操作系統控制的。用戶線程通過程序計數器和虛擬機棧用來存儲線程執行所必須的上下文信息。每個線程都有自己的程序計數器和虛擬機棧。

20210119201727.png

程序計數器

程序計數器在JAVA虛擬機規範中稱為Program Counter Register,即為PC寄存器,它可以看作當前線程所執行的位元組碼行號指示器,位元組碼解釋器工作時通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。

需要注意,只有執行的是非本地(Native)方法,程序寄存器才會記錄JAVA虛擬機正在執行的位元組碼指令地址,若當前執行方法是本地方法,則程序計數器的值為空(Undefined)。

Java虛擬機棧

和程序計數器一樣,每一個JAVA虛擬機線程都有自己私有的JAVA虛擬機棧。Java虛擬機規範允許Java虛擬機棧被實現為固定大小,也允許動態擴展和收縮。

當線程請求的棧深度大於虛擬機允許的棧深度,則會拋出StackOverflowError異常。當棧動態擴展無法申請到足夠的內存時,則會排除OutOfMemoryError異常。

20210110204312.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是一個指針,指向一條虛擬機指令的操作碼。這些操作碼包括jsrretjsr_w。在JDK 7之前,這些操作碼用於實現finally語句塊的跳轉和返回。從JDK 7開始,虛擬機已不允許這幾個操作碼了,改為冗餘finally塊代碼(在每個catch塊後生成冗餘的finally代碼)實現,因此returnAddress類型基本就沒用了。

操作數棧

每個棧幀內部都包含一個後進先出(LIFO)的操作數棧。操作數棧的最大深度由編譯期決定。操作數棧中保存了局部變量表或對象實例中的常量或變量值。在調用方法時,也保存調用方法的參數和返回值。

若局部變量是long或double類型,則需要佔用2個單位的棧深度。

舉個例子,當執行以下代碼。右邊注釋的[]表示操作數棧,左邊時棧底,右邊是棧頂。

//          //[]
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

為了提高垃圾收集性能,Java堆將空間分為新生代、老年代。新生代又被分為Eden區和Survivor區。

通常情況下對象都被創建在新生代中的Eden區,隨後隨着垃圾回收的進行,未被回收的對象則被逐步從新生代轉移到老年代,具體垃圾回收相關細節不在這裡討論。

若新生代的空間不足以創建對象,則可能直接被創建到老年代

方法區

方法區用於存儲被虛擬機加載的類信息、靜態變量、JIT後的代碼位元組碼緩存、運行池常量。虛擬機規範把方法區列為堆的一部分,但是虛擬機實現可以不實現方法區的自動垃圾回收,而是依賴於對常量池和類型的卸載來完成。

20210123142339.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;

  • Iint類型的字段描述符
  • Ddouble類型的字段描述符
  • Ljava/lang/ThreadThread類型的內部描述符
  • Ljava/lang/Object是方法的返回值為object類型

方法描述符分割各標識符的符號不用.,而用/表示。

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命令查看內部簽名。

 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這裡的abc1都屬於字面量。運行期常量值的是運行期產生的新的常量,比如String.intern()方法產生的字符串常量會被保存到運行時常量池緩存起來複用。
運行時常量在方法區中分配,在加載類和接口到虛擬機後就會創建對應的運行時常量。若創建運行時常量所需的內存空間超過了方法區所能提供的最大值,則會拋出OutOfMemoryError異常。

還是上面的代碼示例,通過javap -v可以輸出包括運行時常量的附加信息。下面列出了了部分常量輸出內容。

D:\study\java\symbolreference\out\production\symbolreference>javap -v com.company.SymbolTest
...
Constant pool:
   #1 = Methodref          #7.#28         // java/lang/Object."<init>":()V
   #2 = String             #29            // jake
   #3 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
  ...
   #7 = Class              #36            // java/lang/Object
   #8 = Utf8               staticParameter
   #9 = Utf8               Ljava/lang/String;
  #10 = Utf8               ConstantValue
  #11 = String             #37            // 1245
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  ...
  #18 = Utf8               Lcom/company/SymbolTest;
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               name
  #24 = Utf8               age
  #25 = Utf8               I
  #26 = Utf8               SourceFile
  #27 = Utf8               SymbolTest.java
  #28 = NameAndType        #12:#13        // "<init>":()V
  #29 = Utf8               jake
  ...
  #35 = Utf8               com/company/SymbolTest
  #36 = Utf8               java/lang/Object
  #37 = Utf8               1245
  ...

通過輸出的靜態常量信息可以很清楚的看出JVM編譯時對字面量和符號引用的處理,包括類型名、變量名、方法等都用符號來代替了。比如第一個常量為對象類構造方法java/lang/Object."<init>":()V。去除其他不相關的常量,最終的符號引用和字面量關係如下表。

索引 類型
0 Methodref #7.#28(java/lang/Object."<init>":()V)
7 Class #36
12 Utf8 <init>
13 Utf8 ()V
28 NameAndType #12:#13("<init>":()V)
36 Utf8 java/lang/Object

實現方式

在JDK1.7之前,HotSpot是使用GC的永久代來實現方法區,省去了專門編寫方法區的內存管理代碼。
從JDK1.8開始,使用元空間替代永久代來存放方法區的數據。元空間屬於本地內存。簡而言之使用了本地內存替換堆內存來存放方法區的數據。

若方法區內存空間不滿足內存分配的請求時,將拋出OutOfMemoryError異常。

本地方法棧

若虛擬機支持本地方法,則需要提供本地方法棧,本地方法棧在線程創建的時候按線程分配。HotSpot虛擬機將本地方法棧和虛擬機棧合二為一。

本地方法棧和虛擬機棧一樣也會拋出StackOverflowErrorOutOfMemoryError異常。

參考文檔

  1. JVM jsr和ret指令始終理解不了?returnAddress又怎麼理解呢?
  2. 如何理解ByteCode、IL、彙編等底層語言與上層語言的對應關係?
  3. The Java Virtual Machine Instruction Set
  4. 《深入理解Java虛擬機》
  5. 《Java虛擬機規範(Java SE 8版)》

本文地址://www.cnblogs.com/Jack-Blog/p/14332247.html
作者博客:傑哥很忙
歡迎轉載,請在明顯位置給出出處及鏈接

Tags: