Java記憶體區域和記憶體溢位異常
Java執行時記憶體區域
Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。如圖2-1所示
程式計數器(Program Counter Register):程式計數器是一塊較小的記憶體空間。它可以看作是當前執行緒所執行的位元組碼的行號指示器。系統執行時,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響。執行緒執行Java方法時,程式計數器記錄的是虛擬機器正在執行的位元組碼的指令地址,如果正在執行的是Native方法,這個計數器值則為空(Undefined)。此區域是唯一沒有OutOfMemoryError異常的區域。
Java虛擬機器棧(
本地方法棧(Native Method Stack):與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。本地方法棧區域也會丟擲StackOverflowError
堆(Heap):對於大多數應用來說,Java堆(Java Heap)是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。幾乎所有的物件例項和陣列都是在堆上分配的。Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”。從記憶體分配角度看:堆記憶體可以分為新生代和老年代。
Java堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可,就像我們的磁碟空間一樣。在實現時,堆既可以實現成固定大小的,也可以是可擴充套件的,不過當前主流的虛擬機器都是按照可擴充套件來實現的(通過-Xmx和
方法區(Method Area):方法區與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
總結:Java堆和方法區是執行緒共享的,Java虛擬機器棧、本地方法棧、程式計數器是執行緒私有的
執行時常量池
執行時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。
執行時常量池相對於Class檔案常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入Class檔案中常量池的內容才能進入方 法區執行時常量池,執行期間也可能將新的常量放入池中,如String類的intern()方法(可以看看intern在JDK1.6和JDK1.6後的區別)。
物件的建立
Java虛擬機器是如何建立一個物件的呢?(HotSpot為例)
第一步:檢查類是否被載入過。虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒 有,那必須先執行相應的類載入過程。
第二步:在堆中找一塊足夠大的記憶體空間,分配給物件。在劃分空閒記憶體空間的時候,在併發情況下可能出現執行緒安全問題。有兩種方式來解決這個問題:
(1)使用CAS操作和失敗重試方式保證更新操作的原子性
(2)把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer,TLAB)。哪個執行緒要分配記憶體,就在哪個執行緒的TLAB上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定。
第三步:記憶體空間初始化零值。這一步操作保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
第四步:初始化。從虛擬機器的角度看,三面三個步驟執行完,物件就創建出來了。從Java程式的角度看,還需要一個<init>方法執行,這個方法執行後,欄位零值就被初始化為程式中定義的值。這時一個可用的物件才被創建出來。
物件的記憶體佈局
在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭(Header)、 例項資料(Instance Data)和對齊填充(Padding)。
物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時 間戳等,這部分資料的長度在32位和64位的虛擬機器(未開啟壓縮指標)中分別為32bit和 64bit,官方稱它為“Mark Word”。物件頭的另外一部分是型別指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。
例項資料部分是物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。
對齊填充並不是必然存在的,也沒有特別的含義,它僅僅起著佔位符的作用。
由於HotSpot VM的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,換句話說, 就是物件的大小必須是8位元組的整數倍。而物件頭部分正好是8位元組的倍數(1倍或者2倍),因此,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。
物件的訪問定位
Java程式需要通過棧上的reference資料來操作堆上的具體物件。主流的訪問方式有兩種:
(1)控制代碼:如果使用控制代碼訪問的話,那麼Java堆中將會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊,如圖2-2所示。
(2)直接指標:如果使用直接指標訪問,那麼Java堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而reference中儲存的直接就是物件地址,如圖2-3所示。
兩種方式訪問物件方式的比較:在物件移動時,reference本身方式不需要頻繁的修改,而只需要改變控制代碼中物件例項資料的指標。指標方式的優點就是速度快,少一次指標定位的開銷。HotSpot虛擬機器是使用直接指標方式的。
記憶體溢位
記憶體溢位(OutOfMemoryError):也叫OOM,記憶體不足導致無法滿足程式執行的需要。
(1)模擬堆記憶體溢位實驗:
/**
*VM Args:-Xms20m-Xmx20m-XX:+HeapDumpOnOutOfMemoryError
*@author zzm
*/
public class HeapOOM{
static class OOMObject{
}
public static void main(String[]args){
List<OOMObject>list=new ArrayList<OOMObject>();
while(true){
list.add(new OOMObject());
}
}
}
上面的程式碼執行使用了引數:-Xms 20m -Xms 20m的意思是堆記憶體設定為最大最小都是20m。這樣就避免的堆記憶體自動拓展而方便程式模擬記憶體溢位效果。
上面程式碼的執行結果:=
java.lang.OutOfMemoryError:Java heap space
Dumping heap to java_pid3404.hprof……
Heap dump file created[22045981 bytes in 0.663 secs]
(3)棧記憶體溢位實驗:
/**
*VM Args:-Xss128k
*@author zzm
*/
public class JavaVMStackSOF{
private int stackLength=1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[]args)throws Throwable{
JavaVMStackSOF oom=new JavaVMStackSOF();
try{
oom.stackLeak();
}catch(Throwable e){
System.out.println("stack length:"+oom.stackLength);
throw e;
}
}
-Xss 128k是限制棧大小為128k。然後不停的遞迴導致棧深度超虛擬機器的最大深度。
執行結果:
stack length:2402
Exception in thread"main"java.lang.StackOverflowError
at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:20)
at org.fenixsoft.oom.VMStackSOF.leak(VMStackSOF.java:21)
at org.fenixsoft.oom.VMS
如果執行緒請求棧深度超過虛擬機器允許的最大深度,丟擲
StackOverflowError異常。虛擬機器在拓展棧時候沒有足夠的記憶體則丟擲OOM異常。
方法區和執行時常量池溢位。(下面這個兩種溢位書中給出介紹很少)
本機直接記憶體溢位。