執行時資料區域
總覽
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虛擬機器》第三版 ——周志明 (吹爆)