Java虛擬機運行時數據區
程序計數器(線程私有)
是一個較小的內存區域,可以看做是當前線程所執行的字節碼的行號指示,由於Java是多線程的,線程中斷切換需要恢復執行位置,所以每個線程都需要一個獨立的程序計數器來記錄當前線程執行的位置。虛擬機棧(線程私有)
是Java方法執行的內存模型,生命週期和線程相同。每個方法在執行時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口燈信息。每個方法調用到執行完成,就是對應棧幀在虛擬機棧中入棧出棧的過程。
局部變量表用於存放基本數據類型、對象引用、returnAddress類型(執行字節碼指令地址)
局部變量表所需內存空間在編譯期間完全分配,當進入一個方法需要在幀中分配多少局部變量空間完全確定,運行期間不會改變。
如果線程請求的棧深度大於虛擬機允許的深度,拋出StackOverflowError;
如果虛擬機棧可以動態分配(大部分都可以),但無法得到足夠的內存,拋出OutOfMemoryError;本地方法棧(線程私有)
與虛擬機棧一樣,區別在於。虛擬機棧是爲執行Java方法服務,本地方法棧是爲執行Native方法服務,沒有規定具體的語言,所以虛擬機可以自由實現。Java堆/Gc堆(線程共享)
是Java虛擬機所管理的內存最大的一塊。在虛擬機啓動的時候創建,唯一目的就是存放內存實例。Java堆是垃圾收集器管理的主要區域。因爲分代收集算法,所有可以劃分爲新生代和老年代。Java堆可擴展(通過-Xmx和-Xms控制)。如果堆內有內存完成實例分配,並且堆也無法擴展時,拋出OutOfMemoryError方法區(線程共享)
用於存儲已經被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。這個區域的內存回收目的主要是針對常量池的回收和對類型卸載,這個區域回收相當嚴苛,但是也有必要,未完全回收會導致內存泄露,當方法區無法滿足內存分配需求時,拋出OutOfMemoryError運行時常量池
是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息的常量池。用於存放編譯期生成的各種字面量和符號引用。這部分內容在類加載後進入方法區的運行時常量池存放。運行期間也可以產生常量,也可以放入常量池。當常量池無法滿足內存分配需求時,拋出OutOfMemoryError直接內存
並不是虛擬機運行時數據區的一部分,但是這部分內存也被頻繁使用,也會導致OutOfMemoryError。
Nio引入基於Channel和Buffer的I/O方法,使用Native函數庫直接分配堆外內存,通過存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。避免了Java堆和Navice堆中來回複製數據。
分配內存的時候需要注意這塊內存,不要出現哥哥內存區域總和大於物理內存,拋出OutOfMemoryError
對象的創建過程
- new
- 檢查參數是否能在常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否被加載、解析、初始化過,沒有,則進行類加載過程
- 爲新生對象分配內存,內存大小在類加載後完全確定。過程等於把一塊內存從Java堆中劃分出來
- 如果Java堆是規則的,則使用“指針碰撞”,空閒內存在一邊,用過的內存在另一邊,直接移動指針就行
- 如果不規則,使用“空閒列表”,從列表中找到一塊足夠大的內存分配出去
- 對象創建併發,一種是分配動作同步處理(CAS配上失敗重試保證原子性);另一種爲每個線程預先分配一小塊內存,叫做“本地線程分配緩衝”(TLAB),那個線程要分配就用那個內存區,只有在用完並分配新的時候才需要同步鎖,通過-XX:+/-UseTLAB來設定
- 分配內存後,需要初始零值(不包括對象頭)
- 執行方法,進行初始化。
對象的內存佈局
對象內存佈局分爲3塊區域
- 對象頭
包括兩部分信息,第一部分用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有鎖、偏向線程ID、偏向時間戳。32 64位虛擬機分別是32bit和64bit;第二部分是類型指針,即對象指向它的類元數據的指針,用於確定對象是那個類的實例。 - 實例數據
是對象真正存儲的有效信息,也是程序代碼裏面定義的字段內容。 - 對齊填充
佔位符,保證8字節的整數倍數,補齊。
對象訪問定位
Java程序需要通過棧上的reference數據來操作堆上的具體對象。
目前兩種主流的訪問方式
- 句柄訪問
需要Java堆劃分出一塊內存區域作爲句柄池,reference中存儲的就是對象的句柄地址,句柄中包含了對象實例數據指針和對象類型數據指針,分別指向Java堆中的對象實例數據和方法區的對象類型數據
優點:reference存儲的是穩定的句柄地址,由於垃圾收集時對象移動非常普遍,只要改變句柄的實例數據指針就行,reference本身不需要修改 直接指針訪問
reference中存儲的直接是對象地址,對象實例數據中存儲對象類型數據的指針,指向方法區的對象類型數據
優點:速度快,節省了一次指針定位開銷,HotSpot使用這種,但是從軟件開發範圍看,句柄比較常見
OutOfMemoryError異常實戰
堆溢出
設置VM參數
-verbose:gc //表示輸出虛擬機中GC的詳細情況
-Xms20M //最小Java堆內存20M
-Xmx20M //最大Java堆內存20M
-Xmn10M //最大Java堆內存新生代10M
-XX:+PrintGCDetails //打印GC詳細信息
-XX:SurvivorRatio=8 //Eden和Survivor空間佔比
運行方法
public class Test {
public static void main(String[] args){
List<String> list=new ArrayList<>();
while (true){
list.add("hello");
}
}
}
結果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.base/java.util.Arrays.copyOf(Arrays.java:3719)
at java.base/java.util.Arrays.copyOf(Arrays.java:3688)
at java.base/java.util.ArrayList.grow(ArrayList.java:237)
at java.base/java.util.ArrayList.grow(ArrayList.java:242)
at java.base/java.util.ArrayList.add(ArrayList.java:467)
at java.base/java.util.ArrayList.add(ArrayList.java:480)
at Test.main(Test.java:10)
遇到堆OutOfMemoryError先確定是內存泄露還是內存溢出
內存泄露:通過工具查看泄露對象到GC Roots的引用鏈,確定通過怎樣的路徑與GC Roots關聯導致無法回收
內存溢出:檢查-Xms -Xmx,是否可以調大
虛擬機棧和本地方法棧溢出
設置VM參數
-Xss20M //棧內存容量
運行方法
public class Test {
private int l=1;
public void stack(){
l++;
stack();
}
public static void main(String[] args){
Test test=new Test();
test.stack();
}
}
結果
Exception in thread "main" java.lang.StackOverflowError
默認的棧深度,在大多數情況下,達到1000-2000完全沒問題,對正常的方法調用(包括遞歸),這個深度是完全夠了,如果建立多線程導致內存溢出,在不能減少線程數的情況下,只能通過減少最大堆和減少棧容量來換取線程。
方法區和運行時常量池溢出
設置VM參數
-XX:PermSize=8M //最小方法區
-XX:MaxPermSize=16M //最大方法區
運行方法
public class Test {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
int i=0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
}
String.intern()是一個Native方法,作用:如果字符串常量池中已經包括一個等於此String對象的字符串,則返回常量池中的這個字符串String對象,否則,將此String對象添加到常量池,並返回引用。
1.6以前的版本,由於常量池分配在永久代,會出現OutOfMemoryError,跟隨“PermGen space”,
1.7會一直運行下去
本機直接內存溢出
設置VM參數
-Xmx20M //最大堆內存
-XX:MaxDirectMemorySize=10M //直接內存大小,默認是Xmx的大小一樣
由DirectMemory導致的內存溢出,特徵是Heap Dump文件中看不到明顯的異常,Dump文件很小,如果程序使用Nio考慮檢查是這個問題導致的。