JVM 運行時的數據區域
首先獲取一個直觀的認識:java
總共也就這麼 5 個區(直接內存不屬於 JVM 運行時數據區的一部分),除了程序計數器其餘的地方都有可能出現 OOM (OutOfMemoryError),其中像是程序計數器和兩個棧(Java 虛擬機棧 & 本地方法棧)都是每一個線程要有一個的,因此確定是線程隔離的。而其餘 2 個區就是線程共享的了,也就是說,若是有多個線程要同時訪問這兩個區的數據,是會出現線程安全問題的。接下來,咱們將對這些區域進行詳細的介紹。數組
程序計數器
- 當前線程所執行的字節碼的行號指示器,字節碼解釋器工做時就是經過改變這個計數器的值來肯定下一條要執行的字節碼指令的位置
- 執行 Java 方法和 native 方法時的區別:
- 執行 Java 方法時:記錄虛擬機正在執行的字節碼指令地址;
- 執行 native 方法時:無定義;
Java 虛擬機棧
- Java 方法執行的內存模型,每一個方法執行的過程,就是它所對應的棧幀在虛擬機棧中入棧到出棧的過程;
- 服務於 Java 方法;
- 可能拋出的異常:
- OutOfMemoryError(在虛擬機棧能夠動態擴展的狀況下,擴展時沒法申請到足夠的內存);
- StackOverflowError(線程請求的棧深度 > 虛擬機所容許的深度);
本地方法棧
- 服務於 native 方法;
- 可能拋出的異常:與 Java 虛擬機棧同樣。
Java 堆
- 惟一的目的:存放對象實例;
- 垃圾收集器管理的主要區域;
- 能夠處於物理上不連續的內存空間中;
- 可能拋出的異常:
- OutOfMemoryError(堆中沒有內存能夠分配給新建立的實例,而且堆也沒法再繼續擴展了)。
- 虛擬機參數設置:
- 最大值:
-Xmx
- 最小值:
-Xms
- 兩個參數設置成相同的值可避免堆自動擴展。
方法區
- 存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據;
- 類信息:即 Class 類,如類名、訪問修飾符、常量池、字段描述、方法描述等。
- 垃圾收集行爲在此區域不多發生;
- 不過也不能不清理,對於常常動態生成大量 Class 的應用,如 Spring 等,須要特別注意類的回收情況。
- 運行時常量池也是方法區的一部分;
- Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項是常量池,用於存放編譯器生成的各類字面量(就是代碼中定義的 static final 常量)和符號引用,這部分信息就存儲在運行時常量池中。
- 可能拋出的異常:
- OutOfMemoryError(方法區沒法知足內存分配需求時)。
直接內存
- JDK 1.4 的 NIO 類可使用 native 函數庫直接分配堆外內存,這是一種基於通道與緩衝區的 I/O 方式,它在 Java 堆中存儲一個 DirectByteBuffer 對象做爲堆外內存的引用,這樣就能夠對堆外內存進行操做了。由於能夠避免 Java 堆和 Native 堆之間來回複製數據,在一些場景能夠帶來顯著的性能提升。
- 虛擬機參數設置:
-XX:MaxDirectMemorySize
- 默認等於 Java 堆最大值,即
-Xmx
指定的值。
- 將直接內存放在這裏講解的緣由是它也可能會出現 OutOfMemoryError;
- 服務器管理員在配置 JVM 參數時,會根據機器的實際內存設置
-Xmx
等信息,但常常會忽略直接內存(默認等於 -Xmx
設置值),這可能會使得各個內存區域的總和大於物理內存限制,從而致使動態擴展時出現 OOM。
HotSpot 虛擬機堆中的對象
這一小節將對 JVM 對 Java 堆中的對象的建立、佈局和訪問的全過程進行講解。安全
對象的建立(遇到一條 new 指令時)
- 檢查這個指令的參數可否在常量池中定位到一個類的符號引用,並檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,先把這個類加載進內存;
- 類加載檢查經過後,虛擬機將爲新對象分配內存,此時已經能夠肯定存儲這個對象所需的內存大小;
- 在堆中爲新對象分配可用內存;
- 將分配到的內存初始化;
- 設置對象頭中的數據;
- 此時,從虛擬機的角度看,對象已經建立好了,但從 Java 程序的角度看,對象建立纔剛剛開始,構造函數尚未執行。
第 3 步,在堆中爲新對象分配可用內存時,會涉及到如下兩個問題:服務器
如何在堆中爲新對象劃分可用的內存?多線程
- 指針碰撞(內存分配規整)
- 用過的內存放一邊,沒用過的內存放一邊,中間用一個指針分隔;
- 分配內存的過程就是將指針向沒用過的內存那邊移動所需的長度;
- 空閒列表(內存分配不規整)
- 維護一個列表,記錄哪些內存塊是可用的;
- 分配內存時,從列表上選取一塊足夠大的空間分給對象,並更新列表上的記錄;
如何處理多線程建立對象時,劃份內存的指針的同步問題?架構
- 對分配內存空間的動做進行同步處理(CAS);
- 把內存分配動做按照線程劃分在不一樣的空間之中進行;
- 每一個線程在 Java 堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB);
- 哪一個線程要分配內存就在哪一個線程的 TLAB 上分配,TLAB 用完須要分配新的 TLAB 時,才須要同步鎖定;
- 經過
-XX:+/-UseTLAB
參數設定是否使用 TLAB。
對象的內存佈局
- 對象頭:
- 第一部分:存儲對象自身運行時的數據,HashCode、GC分代年齡等(Mark Word);
- 第二部分:類型指針,指向它的類元數據的指針,虛擬機經過這個指針來判斷這個對象是哪一個類的實例(HotSpot 採用的是直接指針的方式訪問對象的);
- 若是是個數組對象,對象頭中還有一塊用於記錄數組長度的數據。
- 實例數據:
- 默認分配順序:longs/doubles、ints、shorts/chars、bytes/booleans、oops (Ordinary Object Pointers),相同寬度的字段會被分配在一塊兒,除了 oops,其餘的長度由長到短;
- 默認分配順序下,父類字段會被分配在子類字段前面。
注:HotSpot VM要求對象的起始地址必須是8字節的整數倍,因此不夠要補齊。
對象的訪問
Java 程序須要經過虛擬機棧上的 reference 數據來操做堆上的具體對象,reference 數據是一個指向對象的引用,不過如何經過這個引用定位到具體的對象,目前主要有如下兩種訪問方式:句柄訪問和直接指針訪問。函數
句柄訪問
句柄訪問會在 Java 堆中劃分一塊內存做爲句柄池,每個句柄存放着到對象實例數據和對象類型數據的指針。oop
優點:對象移動的時候(這在垃圾回收時十分常見)只需改變句柄池中對象實例數據的指針,不須要修改reference自己。佈局
直接指針訪問
直接指針訪問方式在 Java 堆對象的實例數據中存放了一個指向對象類型數據的指針,在 HotSpot 中,這個指針會被存放在對象頭中。性能
優點:減小了一次指針定位對象實例數據的開銷,速度更快。
原文:Java架構筆記