JVM基礎之內存空間和異常(一)

“合抱之木,生於毫末;千里之行,始於足下;九層之臺,起於壘土。”

一、虛擬機運行時數據區

  1. 程序計數器
  • 是一塊較小的內存空間,可以看作是當前線程執行的字節碼的行號指示器。根據虛擬機的概念模型,字節碼解釋器就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令(分支、循環、跳轉、異常處理、線程恢復都依賴於計數器)。
  • Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻一個“處理器(多核處理器是一個內核)”只會執行一條線程中的指令。
  • 爲線程切換後恢復到正確的位置,每個線程都需要一個獨立的程序計數器(各線程的計數器互不影響)。
  • 線程執行Java方法時,計數器記錄正在執行的虛擬機字節碼指令的地址;如果是Native方法,計數器值爲空(Undefined)。
  • 是Java虛擬機規範中,沒有規定任何OOM情況的區域。
  1. Java虛擬機棧
  • 生命週期與線程相同,描述Java方法執行的內存模型:每個方法在執行時會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。
  • 局部變量表存放了基本數據類型(boolean、byte、char、short、int、float、long、double、reference對象引用,其中long和double佔用2個局部變量空間Slot)。局部變量表需要的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的大小。
  • 申請棧深度大於虛擬機允許值,拋出StackOverflowError;允許動態擴展棧深度時,如果擴展無法申請到足夠的內存,會拋出OutOfMemoryError。
  • 只存儲對象object的內存地址(指針或引用),不直接保存對象。
  1. 本地方法棧
  • 發揮的作用與虛擬機棧相似,本地方法棧爲Native方法服務,虛擬機棧爲Java方法(字節碼)服務。會因與虛擬機棧同樣的原因拋出SOF和OOM異常。
  • Sun HotSpot虛擬機將“本地方法棧”與“虛擬機棧”合二爲一。
  1. Java堆
  • Java堆(Java Heap)是Java虛擬機管理的內存中最大的一塊。
  • 被所有線程共享,在虛擬機啓動時創建。
  • 此區域唯一的目的是存放對象實例(包括數組)。
  • 所有對象實例和數據都要在堆(Heap)上進行分配,但是隨着JIT編譯器的發展和逃逸分析技術逐漸成熟,導致了一些微妙的變化。
  • Java堆是垃圾收集器管理的主要區域,通常被稱爲“GC堆”。由於垃圾收集器(GC)通常採用分代收集算法,通常又細分爲:新生代和老年代;再細緻可分爲Eden空間、From Survivor空間、To Survivor空間等。
  • 堆中可以劃分出出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,即TLAB)。
  • 根據Java虛擬機規範,Java堆可以處於物理上不連續的內存空間中,只需保證邏輯上的連續。即可以爲固定大小,也可以爲可擴展(通過-Xmx和-Xms控制)。
  • 當堆中沒有內存可以完成實例分配,且無法繼續擴展,則拋出OOM異常。
  1. 方法區
  • 方法區(Method Area)是各個線程共享的內存區域,用於存儲已被虛擬機加在的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
  • 虛擬機規範把方法區描述爲堆的邏輯部分,卻有別名Non-Heap(爲了與堆區分開)。
  • 在HotSpot虛擬機中,使用“永久代(Permanent Generation)”實現方法區(其他虛擬機不使用這種辦法,如IBM J9),但兩者並不等價,HotSpot中的GC作用範圍擴大到永久代,省去專門的管理代碼。
  • 永久代通過-XX:MaxPermSize設置上限,而其他虛擬機(如IBM J9)可以達到內存的上限(內存上限,32位系統爲232字節,64位是264字節)。
  • 在HotSpot虛擬機中,逐步放棄使用永久代實現方法區,改用Native Memory實現方法區的規劃,在JDK1.7的HotSpot中把原本永久代中的“字符串常量池”移出。
  • 方法區無法滿足內存分配需求時拋出OOM異常。
  1. 運行時常量池
  • 運行時常量池(Runtime Constant Pool)是“方法區”的一部分。Class文件中有類的版本、字段、方法、接口等描述信息,還有一項信息是“常量池”,用於存放編譯器生成的各種字面常量和符號引用,“常量池”內容在類加載後進入方法區的運行時常量池中存放。
  • Java虛擬機規範關於運行時常量池沒有任何細節的要求,不同的虛擬機可以按自己的需求實現這個內存區域,通常“直接引用”也會存儲在運行時常量池中。
  • 除使用String.intern()方法可以在運行期間將新的常量放入池中,其他情況都是預置入Class文件中常量池的內容才能進入方法區運行時常量池。
  • 會拋出OOM異常。
  1. 直接內存
  • 直接內存(Direct Memory)不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。但這部分內存也被頻繁使用,並會拋出OOM異常。
  • 使用Native函數庫可以直接分配堆外內存,通過Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,因爲避免了Java堆和Native堆中來回複製數據,所以顯著的提高了性能。
  • 受到物理內存的限制,會在動態擴展時出現OOM異常。

二、內存中的對象

  1. 對象的創建
  • 首先,檢查new指令的參數是否能在常量池中定位到一個類的符號引用,並檢查這個符號引用的類是否已經被加載、解析和初始化過,如果沒有就必須進行類加載過程。
  • 類加載檢查通過後,在虛擬機的堆中爲新對象分配內存。因爲對象的大小在類加載完成後便可以確定,因此爲對象分配空間等同於在堆中劃分一塊確定大小的內存。
  • 堆中劃分內存分爲兩種方式,當堆中內存是絕對規整的(所有的用過的內存放在一邊,空閒的內存放在一邊,中間放着一個指針作爲分界點),分配內存只需將指針挪動一段與對象大小相等的距離,稱爲“指針碰撞(Bump the Pointer)”;當堆中的內存不是規整的,虛擬機需要維護一個列表記錄哪些內存可用,分配的時候從列表中找到一塊足夠大的恐懼劃分給對象實例,並更新到列表的記錄。
  • Java堆是否規整由垃圾收集器是否帶有壓縮整理功能決定。
  • 虛擬機採用CAS和失敗重試的方式保證更新操作的原子性,或者把內存分配的動作按照線程劃分在不同的空間進行(稱爲本地線程分配緩存Thread Local Allocation Buffer,TLAB,可以使用-XX:+/-UseTLAB參數設定)。使用TLAB分配時,會在分配TLAB空間時初始化空間爲零值。
  • 關於對象的信息,如對象是哪個類的實例、類的元數據、對象的哈希碼、對象的GC分代年齡等信息,存放在對象的對象頭(Object Header)中。
  1. 對象的內存佈局
  • 對象在內存中存儲的佈局分爲三個區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
  1. 對象的訪問和定位
  • Java程序需要通過棧上的reference數據來操作堆上的具體對象,目前主流的訪問方式有使用句柄和直接指針兩種。

三、OutOfMemoryError異常

  1. 程序計數器
    沒有規定OOM異常的內存區域。
  2. 堆空間
    堆內存的OOM異常是實際應用中常見的內存溢出異常情況,通過-XX:HeapDumpOnOutOfMemoryError可以讓虛擬機在出現內存溢出異常時Dump出當前內存堆轉儲快照以便事後進行分析,使用-Xms20m和-Xms20m設置最小和最大值。
  3. 棧空間
    棧空間在單線程時棧空間內存不足時拋出SOF異常,在多線程時建立新的線程時會因內存不足拋出OOM異常。
  4. 方法區
    方法區通常會在大量動態生成對象時(如Spring啓動時)導致OOM異常,運行時常量池無限制增大同樣會導致方法區OOM異常。
  5. 直接內存
    向操作系統申請分配內存時,通過計算得知內存無法分配,會拋出OOM異常。通過-XX:MaxDirectMemorySize指定大小,默認值與堆空間-Xmx值相同。

說明

  • 文中內容主要來自於《深入理解Java虛擬機》,更多內容請關注原著。
  • 此文是讀書時對重點知識的記錄,如有錯誤請一定指出。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章