1. 程式人生 > >《深入理解 Java 虛擬機器》讀書筆記:Java 記憶體區域與記憶體溢位異常

《深入理解 Java 虛擬機器》讀書筆記:Java 記憶體區域與記憶體溢位異常

前言

最近開始看這本書,記得前段時間拿起這本書的時候,心情是相當沉重的!當時的劇本是這樣的——

內景。家裡 - 下午
我(畫外):唉,有點無聊啊!(偶然撇過書架)這麼多書得看到什麼時候啊,要不要拿一本翻翻呢?但是在家裡好像有點看不下去啊,是太安逸了嗎?最近那本《圖解 HTTP》也還沒看完,感覺暫時有點不想看了。(走到書架前)還是挑幾本優先順序比較高的帶到███下班的時候看吧。(沉思)嗯,這本帶過去~

當我拿起《深入理解 Java 虛擬機器》這本書的那一刻,心裡咯噔一下——唉,PM10 濃度又上升了,地球環境越來越差了啊,萬惡的地球人!

正文

一、執行時資料區域

Java 虛擬機器在執行 Java 程式時,會把它所管理的記憶體劃分為若干個不同的資料區域。這些區域都有各自的用途,以及建立和銷燬時間。

1、程式計數器

  • 是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器的概念模型裡,位元組碼直譯器就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。
  • 執行緒私有:為了執行緒切換後能恢復到正確的執行位置,因此每條執行緒都需要有一個獨立的程式計數器。
  • 唯一一個不會出現 OutOfMemoryError 異常的區域。

2、Java 虛擬機器棧

  • 虛擬機器棧描述的是 Java 方法執行的記憶體模型:Java 方法在執行時會建立一個棧幀,用於儲存區域性變量表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫到執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。
  • 執行緒私有。
  • 會出現 StackOverflowError 和 OutOfMemoryError 異常。
    • StackOverflowError:執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲該異常。
    • OutOfMemoryError:虛擬機器棧動態擴充套件時無法申請到足夠的記憶體,將丟擲該異常。

3、本地方法棧

  • 作用與虛擬機器棧相似,只不過虛擬機器棧為虛擬機器執行 Java 方法(位元組碼)服務,而本地方法棧為虛擬機器執行 Native 方法服務。
  • 執行緒私有。
  • 會出現 StackOverflowError 和 OutOfMemoryError 異常。

4、Java 堆

  • Java 虛擬機器所管理的記憶體中最大的一塊,用於存放物件例項。它是垃圾收集器管理的主要區域,也被稱為"GC堆”。
  • 可細分為新生代和老年代,新生代又可細分為 Eden 空間、From Survivor 空間、To Survivor 空間。
  • 執行緒共享。
  • 會出現 OutOfMemoryError 異常。

5、方法區

  • 用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。別名 Non-Heap(非堆)。
  • 也被稱為“永久代”,因為 HotSpot 虛擬機器使用永久代來實現方法區,但本質上兩者並不等價。
    PS:JDK1.8 已經徹底移除了永久代,改用元空間實現方法區。元空間使用的是直接記憶體。
  • 執行緒共享。
  • 會出現 OutOfMemoryError 異常。

6、執行時常量池

  • 是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用。
    PS:JKD1.7 已經從方法區移到了 Java 堆中。
  • 執行緒共享。
  • 會出現 OutOfMemoryError 異常。

7、直接記憶體

  • 不是虛擬機器執行時資料區的一部分,也不是 Java 虛擬機器規範中定義的記憶體區域。但是這部分記憶體也被頻繁使用。
  • 會出現 OutOfMemoryError 異常。

二、HotSpot 虛擬機器物件探祕

1、物件的建立

類載入檢查 -> 分配記憶體 -> 初始化零值 -> 設定物件頭 -> 執行 init 方法

(1)類載入檢查

虛擬機器遇到 new 指令時,會先檢查這個指令的引數能否在常量池中定位到一個類的符號引用,以及這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,就必須先執行相應的類載入過程。

(2)分配記憶體

物件所需記憶體的大小在類載入完成後便可確定,為物件分配記憶體空間等同於把一塊確定大小的記憶體從 Java 堆中劃分出來。

分配記憶體的兩種方式:

  • 指標碰撞: Java 堆中記憶體規整時,將用過的記憶體放在一邊,空閒的記憶體放在另一邊,中間放一個指標作為分界點的指示器。分配記憶體時,只需把那個指標向空閒記憶體那邊,移動一段與物件大小相等的距離即可。
  • 空閒列表: Java 堆中記憶體不規整時,虛擬機器通過維護一個列表,記錄哪些記憶體塊是可用的。在分配時從列表中找出一塊足夠大的空間劃分給物件例項,並更新列表上的記錄。

Java 堆是否規整(是否有記憶體碎片),由所採用垃圾收集器的演算法所決定。“標記-清除”演算法會產生記憶體碎片,而“標記-整理”和複製演算法則不會。

如何保證分配記憶體的執行緒安全:

  • CAS 同步機制:採用 CAS 配上失敗重試的方式保證更新操作的原子性。
  • 本地執行緒分配緩衝(TLAB):每個執行緒在 Java 堆中預先分配一小塊記憶體(TLAB),執行緒要分配記憶體時,先在 TLAB 上分配,TLAB 用完後再採用 CAS 同步機制進行分配。

(3)初始化零值

將分配到的記憶體空間初始化為零值(不包括物件頭),保證物件的例項欄位在 Java 程式碼中可以不賦初始值就直接使用。

(4)設定物件頭

虛擬機器需要對物件進行必要的設定,例如這個物件是哪個類的例項、如何找到類的元資料資訊、物件的雜湊碼、物件的 GC 分代年齡等。這些資訊存放在物件的物件頭中。

(5)執行 init 方法

把物件按照程式設計師的意願進行初始化。

2、物件的記憶體佈局

HotSpot 虛擬機器中,物件在記憶體中儲存的佈局可分為 3 塊區域:物件頭、例項資料和對齊填充。

(1)物件頭

物件頭包含兩部分資訊:

  • Mark Word:用於儲存物件自身的執行時資料,如雜湊碼、GC 分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳等。
  • 型別指標:物件指向它的類元資料的指標,虛擬機器通過這個指標來確定物件是哪個類的例項。

(2)例項資料

物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。

(3)對齊填充

僅僅起著佔位符的作用,不是必然存在的,也沒有特別的含義。

由於 HotSpot 虛擬機器的自動記憶體管理系統,要求物件起始地址必須是 8 位元組的整倍數,換句話說,物件的大小必須是 8 位元組的整倍數。而物件頭部分正好是 8 位元組的整倍數,因此,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。

3、物件的訪問定位

Java 程式需要通過棧上的 reference 資料來訪問堆上的具體物件。目前主流的訪問方式有控制代碼和直接指標兩種。

(1)控制代碼

  • reference 中儲存的是物件的控制代碼地址。
  • Java 堆中劃分出一塊記憶體作為控制代碼池,控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊。
  • 好處:reference 中儲存的是穩定的控制代碼地址,在物件被移動時只會改變控制代碼中的例項資料指標,reference 本身不需要修改。

(2)直接指標

  • reference 中儲存的直接就是物件的地址。
  • Java 堆物件的佈局必須考慮如何放置訪問型別資料的相關資訊。
  • 好處:節省了一次指標定位的時間開銷,速度更快。

三、OutOfMemoryError 異常

Java 虛擬機器中,除了程式計數器外,其他幾個執行時區域都有發生 OutOfMemoryError(OOM)異常的可能。

1、Java 堆溢位

異常堆疊資訊:java.lang.OutOfMemoryError: Java heap space。

異常原因:記憶體洩露、記憶體溢位。

  • 記憶體洩露:存在 GC 無法回收的物件。
  • 記憶體溢位:堆中存活物件過多。

異常處理:

  • 通過工具檢視洩露物件到 GC Roots 的引用鏈,從而定位出洩露程式碼的位置。
  • 調大堆引數(-Xmx、-Xms),例:-Xmx256m -Xms128m
  • 檢查程式碼中是否存在物件生命週期過長的情況。

2、虛擬機器棧和本地方法棧溢位

異常堆疊資訊:java.lang.OutOfMemoryError: unable to create new native thread。

異常原因:建立執行緒過多。

  • 作業系統分配給每個程序的記憶體是有限制的,因此每個執行緒分配到的棧容量越大(棧是執行緒私有的),可建立的執行緒數量就越少,建立執行緒時就越容易把剩下的記憶體耗盡。

異常處理:

  • 減少執行緒數。
  • 更換 64 位虛擬機器。
  • 減少最大堆容量(-Xmx)。
  • 減少棧容量(-Xss),例:-Xss128k

3、方法區和執行時常量池溢位

異常堆疊資訊:java.lang.OutOfMemoryError: PermGen space。

異常原因:載入記憶體的類、常量過多。

異常處理:調大方法區容量(-XX:PermSize、-XX:MaxPermSize),例:-XX:PermSize=64m -XX:MaxPermSize=128m

4、本機直接記憶體溢位

異常堆疊資訊:java.lang.OutOfMemoryError: Direct buffer memory。

異常原因:使用了 NIO 等用到直接記憶體的技術時就有可能出現。

異常處理:調大直接記憶體容量(-XX:MaxDirectMemorySize),例:-XX:MaxDirectMemorySize=512m