對於 Java 程序員來說,在 JVM 自動內存管理機制的幫助下,不再需要爲每一個 new 操作去寫對應的 delete/free 代碼,不容易出現內存泄露和內存溢出的問題。不過正因如此,如果不瞭解虛擬機是怎樣使用內存的,一旦出現內存泄露和內存溢出的問題,那麼排查錯誤將會非常艱難。
一. 內存區域
虛擬機在執行 Java 程序的過程中會把它管理的內存劃分爲若干個不同的數據區域,這些區域各司其職
1. 線程私有
下面這 3 個區域都是線程私有的區域,每個線程獨佔一份
(1)程序計數器
- 當前線程所執行的字節碼的行號指示器
- 通過改變計數器的值來選取下一條執行的字節碼指令
- 幫助完成分支,循環,跳轉,異常處理,線程恢復等基礎功能
- 多線程環境中,爲了正常完成線程的切換,使得各個線程能恢復到正確的執行位置,因此每條線程都需要一個程序計數器
- 唯一一個不會出現 OutOfMemoryError 情況的區域
(2)虛擬機棧
- 每個方法執行時都會創建一個棧幀,當方法被調用,棧幀入棧,方法執行完成,出棧
- 每個棧幀存儲局部變量表,操作棧,動態鏈接,方法出口等
- 局部變量表存儲了當前方法的局部變量,包括基本數據類型,對象引用(指針)以及 returnAddress 類型(指向一條字節碼指令的地址)
- 可能出現的兩種異常:
StackOverflowError:棧溢出,線程請求的棧深度大於虛擬機允許的深度
OutOfMemoryError:內存溢出,如果虛擬機棧可以動態擴展,那麼如果擴展時無法申請到足夠的內存,則拋出異常
(3)本地方法棧
- 作用,運行機制,異常類型等與虛擬機棧相同
- 爲 native 方法服務
- 很多虛擬機中,本地方法棧和虛擬機棧合二爲一
2. 線程共享
下面兩個爲線程共享的區域
(4)堆
- 虛擬機管理的內存中最大的一塊
- 存放對象實例
- 垃圾回收器管理的主要區域,也被稱爲 GC 堆,並因此可以細分爲新生代和老年代
- 可以處於不連續的空間中
- 當沒有內存完成實例分配,堆也無法擴展時,拋出 OutOfMemoryError
(5)方法區
- 存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等
- 垃圾回收行爲在這個區域很少出現,因此也被稱作爲“永久代”
值得注意的是,從 Java 8 開始,方法區被移除,取而代之的是一個叫元空間(Metaspace)的區域
(6)運行時常量池
- Java 6 及之前屬於方法區的一部分;Java 7 後被移入堆區域
- 存放編譯器生產的各種字面量和符號引用,在類加載時期存入運行時常量池
二. 瞭解 Java 對象
1. 對象的創建
對象的創建過程中有以下幾大步驟:
(1)類加載檢查
當虛擬機遇到一個 new 指令,說明要創建對象了;但在創建對象之前,會先去檢查這個類是否已經加載過,解析和初始化過,如果沒有,則先執行類加載過程
(2)分配內存
在堆中劃分出一塊確定大小的內存,分配方式有兩種
分配方法
- 指針碰撞:使用這種方法,堆內存必須是規整的(用過的放一邊,空閒的放一邊),然後中間放一個指針作爲分界點。分配內存時只需要將指針挪一段與對象大小相等的距離即可
- 空閒列表:如果堆內存不規整,就只能使用空閒列表法了。JVM 維護一個列表來記錄內存塊的使用情況;分配時找到一塊足夠大的空間劃分給對象然後更新表記錄即可
保證線程安全
爲了保證線程安全,避免同一塊區域同時分配給多個對象,通常使用兩種方法
- CAS:每當要寫入數據時,先比較當前值(工作內存)與主內存中的值是否一致,是則進行寫入,否則重新獲取值
- TLAB:每個線程預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB);如果某個線程的 TLAB 用完,需要分配新的 TLAB,這些則需要進行同步鎖定
(3)初始化零值
將分配到的內存空間都初始化爲零值;這樣可以使得對象在代碼中不賦初始值就直接使用
public class Person {
int age;
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.age);
}
}
類似這樣的情況,age 被默認賦值爲 0
(4)設置對象頭
將類的自身運行時數據(HashCode,GC 分代年齡,鎖狀態標誌等);類型指針(指向類元數據,確定是哪個類的實例)存放在對象頭中
(5)執行 init 方法
前面幾步完成後,對於虛擬機,一個新對象已經產生了,但對於 java 程序,需要執行 init 方法後,一個真正的對象才完全產生出來
二. 對象的內存佈局
堆裏面存放的是對象實例,對象實例由三個部分組成
- 對象頭:存儲自身運行時數據(HashCode,GC 分代年齡,鎖狀態標誌等);類型指針,指向類元數據(確定是哪個類的實例)
- 實例變量:記錄對象的各種屬性數據信息
- 填充數據:用於對齊字節(JVM 對對象起始地址的字節數有要求,是8字節的整數倍
猜你喜歡
- 算法必學:經典的 Top K 問題
- Java 程序員都該懂的 volatile 關鍵字
- 談一談 JVM 對鎖的優化
- 教你 Shiro + SpringBoot 整合 JWT
- 教你 Shiro 整合 SpringBoot,避開各種坑
- 你必須搞清楚的String,StringBuilder,StringBuffer
- 分享一些 Java 後端的個人乾貨