JVM虛擬機-運行時數據區概述
運行時數據區域
總覽
JDK. 1.7 之後版本略有不同
Java 虛擬機在執行 Java 程式的過程中會把它管理的記憶體劃分成若干個不同的數據區域。
有必要深入了解這塊的內容,因為它將決定伺服器性能,除此之外還有助於快速定位虛擬機的相關Error。
首先來對整個運行時區域有一個整體的認識。
如下圖
JDK 1.7 之前:
JDK 1.7 以及之後(1.8正式使用,1.7還需要手動設置一下) :
-
執行緒私有的(圖中紅色)
-
執行緒共享的(圖中綠色、藍色)
概念掃盲
什麼是棧幀(Stack Frame)
每一次函數的調用,都會在調用棧上維護一個獨立的棧幀,每個獨立的棧幀一般包括:
- 函數的返回地址和參數
- 臨時變數
- 函數調用的上下文
棧是從高地址向低地址延伸,一個函數的棧幀用ebp 和 esp 這兩個暫存器來劃定範圍。
ebp 指向當前的棧幀的底部,esp 始終指向棧幀的頂部。
- ebp 暫存器又被稱為幀指針(Frame Pointer)
- esp 暫存器又被稱為棧指針(Stack Pointer)
JVM常見出現兩種錯誤
StackOverFlowError
: 若 Java 虛擬機棧的記憶體大小不允許動態擴展,那麼當執行緒請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候,就拋出StackOverFlowError
錯誤。OutOfMemoryError
: Java 虛擬機棧的記憶體大小可以動態擴展, 如果虛擬機在動態擴展棧時無法申請到足夠的記憶體空間,則拋出OutOfMemoryError
異常異常。
程式計數器
程式計數器佔用較小的一塊記憶體空間,每條執行緒都需要有一個獨立的程式計數器,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被來回切換的時候,能夠知道該執行緒上次運行到哪兒了。
位元組碼解釋器工作時通過改變這個計數器的值,來選取下一條需要執行的位元組碼指令,從而實現程式碼的流程式控制制,如:順序執行、選擇、循環、異常處理。
它的生命周期隨著執行緒的創建而創建,隨著執行緒的結束而死亡。
程式計數器是唯一一個不會出現
OutOfMemoryError
的記憶體區域。
虛擬機棧
結構
虛擬機棧也是執行緒私有,而且生命周期與執行緒相同。
每個Java方法在執行的時候都會創建一個棧幀,用於存儲局部變數表、操作數棧、動態鏈接、方法出口等資訊。
局部變數表
- 存放編譯器可知的各種基本數據類型(boolean、byte等)
- 對象引用(reference類型,它不等同於對象本身)
- 可能是一個指向對象起始地址的引用指針
- 也可能是指向另一個代表對象的句柄
- 其他次對象相關的位置
- returnAddress類型,指向了一條位元組碼指令的地址
方法是如何調用的
每一次函數調用都會有一個對應的棧幀被壓入 Java 棧,每一個函數調用結束後,都會有一個棧幀被彈出。
Java 方法有兩種返回方式:
- return 語句。
- 拋出異常。
不管哪種返回方式都會導致棧幀被彈出。
本地方法棧
主要為虛擬機使用到的Native方法服務,作用其實類似虛擬機棧,其結構也和虛擬機棧一樣
二者的區別是虛擬機棧為虛擬機執行位元組碼服務。
本地方法被執行的時候,在本地方法棧也會創建一個棧幀,用於存放該本地方法的局部變數表、操作數棧、動態鏈接、出口資訊。
方法執行完畢後相應的棧幀也會出棧並釋放記憶體空間。
在 HotSpot 虛擬機中和虛擬機棧合二為一
堆
Java 堆是所有執行緒共享的一塊記憶體區域,在虛擬機啟動時創建。
此記憶體區域的目的是存放對象實例,幾乎所有的對象實例以及數組都在這裡分配記憶體。
說是幾乎是因為由於多項技術的進步與成熟,如:逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術,一些對象也可能在棧上分配記憶體。
Java 堆是JVM中最大的一塊記憶體區域,也是是垃圾回收(Garbage Collected)管理的主要區域,故又叫做GC堆。
淺堆和深堆
淺堆和深堆是兩個非常重要的概念,理解他們之前需要先了解什麼是保留集。
保留集,即為只被單一對象所持有的對象的集合,如圖:
- 淺堆是指一個對象所消耗的記憶體。如上圖
- 深堆是指對象的保留集中所有的對象的淺堆大小之和。
堆的細分
HotSpot中還有永久代的概念,不過已經是歷史了。
JDK 8 HotSpot 的永久代被徹底移除,取而代之是元空間,元空間使用的是直接記憶體。
現在垃圾收集器基本都採用分代垃圾收集演算法,所以 Java 堆還可以細分,堆分為新生代(占堆1/3),老生代(占堆2/3)
- 新生代(內部比例8:1:1)
- Eden 空間
- From Survivor 空間
- To Survivor 空間
- 老年代
進一步劃分的目的是更好地回收記憶體,或者更快地分配記憶體。
流程:
- 大多數情況,對象都會首先在 Eden 區域分配
- 在一次新生代垃圾回收後,如果對象還存活,則會進入兩個Survivor中的一個,然後對象的年齡加 1
- 它的年齡增加到年齡閾值(默認為 15 ),就會被晉陞到老年代中
對象晉陞到老年代的年齡閾值,可以通過參數
-XX:MaxTenuringThreshold
設置
方法區
方法區與 Java 堆一樣,也是所有執行緒共享的。
主要用於存儲類的資訊、常量池、方法數據、方法程式碼等。
方法區邏輯上屬於堆的一部分,但是為了與堆進行區分,有一個別名叫做 Non-Heap(非堆)
該區域的記憶體回收目標主要針對常量池的回收和類型的卸載。
在HotSpot虛擬機中,用永久代來實現方法區,但是這樣容易遇到記憶體溢出的問題,所以在Java 8之後就取消了方法區。
方法區和永久代的關係
摘自《深入理解Java虛擬機》第三版
《Java 虛擬機規範》只是規定了有方法區這麼個概念和它的作用,並沒有規定如何去實現它。那麼,在不同的 JVM 上方法區的實現肯定是不同的了。 方法區和永久代的關係很像 Java 中介面和類的關係,類實現了介面,而永久代就是 HotSpot 虛擬機對虛擬機規範中方法區的一種實現方式。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機規範中的定義,是一種規範,而永久代是一種實現,一個是標準一個是實現,其他的虛擬機實現並沒有永久代這一說法。
為什麼要將永久代替換為元空間 ?
- 永久代記憶體有一個JVM固定的上限,經常會出現
OutOfMemoryError
。 - 元空間使用的是直接記憶體,受本機可用記憶體的限制,雖然元空間仍舊可能溢出,但是比原來出現的幾率會更小。
- 元空間裡面存放的是類的元數據,由系統的實際可用空間來控制,這樣能載入的類就變多了。
- 在 JDK8,合併 HotSpot 和 JRockit 的程式碼時,JRockit 沒有永久代,如果強行保留實現起來困難重重。
當元空間溢出時會得到如下錯誤:
java.lang.OutOfMemoryError: MetaSpace
運行時常量池
運行時常量池用於存放編譯期間生成的各種字面量和符號引用,是方法區的一部分。
運行時常量池用來動態獲取類資訊,包括:
- Class文件元資訊描述
- 編譯後的程式碼數據
- 引用類型數據
- 類文件常量池
運行時常量池是在類載入完成之後,將每個Class常量池中的符號引用值轉存到運行時常量池中。
每個Class都有一個運行時常量池,類在解析之後將符號引用替換成直接引用,與全局常量池中的引用值保持一致。
運行時常量池相的另外一個重要特性是具備動態性,Java語言並不要求常量一定只有編譯器才能產生,也就是並非預置入Class文件中的常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中。
直接記憶體
直接記憶體並不是虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。
使用的方式是通過 JDK1.4 中加入的NIO(New Input/Output)
類,它可以直接使用 Native 函數庫直接分配堆外記憶體。
通過一個存儲在 Java 堆中的 DirectByteBuffer
對象作為這塊記憶體的引用進行操作。
避免了在 Java 堆和 Native 堆之間來回複製數據,在一些場景中顯著提高了性能,
本機直接記憶體的分配不受 Java 堆的限制,但受到本機總記憶體大小,以及處理器定址空間的限制,因此也可能導致 OutOfMemoryError
錯誤出現。
總結
以上的各個分區,各司其職,是了解Java虛擬機的基礎。
理解各區域的指責和作用,對JVM後續的學習有非常大的幫助,如果這些沒搞懂,後面學起來是真頭大😮💨。
結合圖例,相信可以較為清晰了理解各分區的架構和指責,覺得有用歡迎點個推薦、點個贊。
參考:
《深入理解Java虛擬機》第三版 ——周志明 (吹爆)