參考文章:
- 《Java Se11 虛擬機器器規範》
- 《深入理解Java虛擬機器器-JVM高階特性與最佳實踐 第3版》- 周志明
本文基於Java Se 11講解。
根據《Java虛擬機器器規範》的規定,Java虛擬機器器所管理的記憶體將會包括以下幾個執行時資料區域:
對於不同的虛擬機器器實現,在執行時資料區的實現上並不完全相同。對於常用的HotSpot虛擬機器器來說,它的執行時資料區如下:
主要區別在於,HotSpot使用了直接使用本地記憶體(即機器本身記憶體)的元空間(metaspace)來實現方法區。
下面針對每個具體的資料區域進行詳細的介紹。
1. 程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。
JVM可以同時支援多個執行執行緒。每個Java虛擬機器器執行緒都有自己的pc(程式計數器)暫存器。在任何時候,每個Java虛擬機器器執行緒都在執行單個方法的程式碼,即該執行緒的當前方法。如果該方法不是native方法,則pc暫存器包含當前正在執行的Java虛擬機器器指令的地址。如果執行緒當前正在執行的方法是native的,則pc暫存器的值為undefined
。Java虛擬機器器的pc暫存器足夠寬,可以容納特定平臺上的returnAddress
或native指標。
此記憶體區域是唯一一個在《Java虛擬機器器規範》中沒有規定任何OutOfMemoryError
情況的區域。
2. Java虛擬機器器棧
與程式計數器一樣,是執行緒私有的,生命週期與執行緒相同。虛擬機器器棧描述的是Java方法執行的執行緒記憶體模型。
「虛擬機器器棧」裡面的每條資料就是「棧幀」,在 Java 方法執行的時候則建立一個「棧幀」併入棧「虛擬機器器棧」。呼叫結束則「棧幀」出棧。
每個棧幀包含四個區域:
每個執行緒擁有一個「虛擬機器器棧」,每個「虛擬機器器棧」擁有多個「棧幀」,而棧幀則對應著一個方法。每個「棧幀」包含區域性變數表、運算元棧、動態連結、方法返回地址。方法執行結束則意味著該「棧幀」出棧。
在《Java虛擬機器器規範》中,對這個記憶體區域規定了兩類異常狀況:
StackOverflowError
異常;OutOfMemoryError
異常。本地方法棧(Native Method Stacks)與虛擬機器器棧所發揮的作用是非常相似的,其區別只是虛擬機器器棧為虛擬機器器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器器使用到的本地(Native)方法服務。
《Java虛擬機器器規範》對本地方法棧中方法使用的語言、使用方式與資料結構並沒有任何強制規定,因此具體的虛擬機器器可以根據需要自由實現它,甚至有的Java虛擬機器器(譬如Hot-Spot虛擬機器器)直接就把本地方法棧和虛擬機器器棧合二為一。與虛擬機器器棧一樣,本地方法棧也會在棧深度溢位或者棧擴充套件失敗時分別丟擲StackOverflowError
和OutOfMemoryError
異常。
所有執行緒共用,虛擬機器器啟動時建立。唯一的目的是用於存放物件範例和陣列,絕大部分物件範例在堆上分配記憶體。
在 Java 中,陣列也是物件。
現代垃圾收集器大部分基於分代收集理論設計。「新生代」、「老年代」這些名詞僅僅是一部分GC的設計風格,而不是《Java虛擬機器器規範》定義的。而從G1收集器出現之後,出現了不採用分代設計的新垃圾收集器。
JDK8之後Class物件、static
變數、字串常數池都放在堆裡。
static
變數作為類的資訊,儲存在Class物件裡。
Java 的物件可以分為基本資料型別和普通物件。普通物件會在堆上分配。對於基本資料型別,如果是區域性變數,則會在棧上分配。其他情況,通常在在堆上分配,逃逸分析的情況下可能會在棧分配。
如果在Java堆中沒有記憶體完成範例分配,並且堆也無法再擴充套件時,Java虛擬機器器將會丟擲OutOfMemoryError
異常。
字串常數池是由String
類維護的一個字串池。是一種池化思想的實現,是為了節省重複建立字串物件的效能開銷和記憶體空間。
每當程式碼建立字串常數時,JVM會首先檢查字串常數池。如果字串已經存在池中,就返回池中的範例參照。如果字串不在池中,就會範例化一個字串並放到池中。Java能夠進行這樣的優化是因為字串是不可變的,可以不用擔心資料衝突進行共用。
字串常數池從JDK7開始挪到了堆中。
可以通過呼叫String.intern()
方法把一個字串物件放到字串常數池中。如果池中已經存在相等的物件,則會返回已存在物件的參照;否則會把這個字串物件加入到池中,並返回新加入的字串物件的參照。
String s = new String("hello")
會建立幾個物件?
如果字串常數池中沒有"hello",則生成2個,否則只生成一個。
String s = new String("abc"); System.out.println((s.intern() == s));
列印結果是什麼?
列印結果為false。s
指向的是堆中的物件,s.intern()
返回的是字串常數池中的物件的參照。
字面量(literal) :用於表達原始碼中的一個固定值的符號(notation)。如整數、浮點數及字串等。如1
、0x01
是整數位面量,Hello World
是字串字面量。
常數:在java中,final
修飾的變數也可以被稱為是常數。任何具有不變性的東西都可以稱為常數。如String
物件是常數。
物件池:是Java語言層面實現的,如Integer.valueOf()
(Integer i = 10
也會調該方法)會使用IntegerCache
的快取物件。如果使用new Integer(10)
則不會使用物件池中的範例。
字串常數池:類似於物件池,但它是JVM層面的技術。字串常數池的實現是c++實現的StringTable
,實際上是一個固定容量的Hashtable
,每一個bucket包含一系列相同hash碼的字串。
用於儲存被JVM載入的class的後設資料資訊,比如類的結構、執行時的常數池、欄位、常數、方法資料、方法建構函式以及介面初始化等特殊方法。還有JIT編譯器編譯後的程式碼快取等資料。
JDK8之前,HotSpot採用永久代的概念實現方法區,JDK8開始廢棄了永久代的概念,改用在本地記憶體(Native Memory)中實現的元空間(Meta-space)來代替。
方法區的GC比較少出現,回收目標主要是針對常數池的回收和對型別的解除安裝。
根據《Java虛擬機器器規範》的規定,如果方法區無法滿足新的記憶體分配需求時,將丟擲OutOfMemoryError
異常。
執行時常數池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常數池表(Constant Pool Table),用於存放編譯期生成的各種字面量與符號參照,這部分內容將在類載入後存放到方法區的執行時常數池中。
一般來說,除了儲存Class檔案中描述的符號參照外,還會把由符號參照翻譯出來的直接參照也儲存在執行時常數池中。
既然執行時常數池是方法區的一部分,自然受到方法區記憶體的限制,當常數池無法再申請到記憶體時會丟擲OutOfMemoryError
異常。