標籤:blog http io os 使用 java ar strong 檔案
其實對於我們一般理解的電腦記憶體,它算是CPU與電腦打交道最頻繁的地區,所有資料都是先經過硬碟至記憶體,然後由CPU再從記憶體中擷取資料進行處理,又將資料儲存到記憶體,通過分頁或分區技術將記憶體中的資料再flush至硬碟。那JVM的記憶體結構到底是如何呢?JVM做為一個運行在作業系統上,但又獨立於os啟動並執行平台,它的記憶體至少應該包括象寄存器、堆棧等地區。
JVM在運行時將資料劃分為了6個地區來儲存,而不僅僅是大家熟知的Heap地區,這6個地區圖示如下:
JVM記憶體的分配結構
下面將逐一介紹下各個地區所做的工作及其充當的功能。
1 PC Register(PC寄存器)
PC寄存器是一塊很小的記憶體地區,主要作用是記錄當前線程所執行的位元組碼的行號。位元組碼解譯器工作時就是通過改變當前線程的程式計數器選取下一條位元組碼指令來工作的。任何分支,迴圈,方法調用,判斷,異常處理,線程等待以及恢複線程,遞迴等等都是通過這個計數器來完成的。
由於Java多線程是通過交替線程輪流切換並分配處理器時間的方式來實現的,在任何一個確定的時間裡,在處理器的一個核心只會執行一條線程中的指令。因此為了線程等待結束需要恢複到正確的位置執行,每條線程都會有一個獨立的程式計數器來記錄當前指令的行號。計數器之間相互獨立互不影響,我們稱這塊記憶體為“線程私人”的記憶體。
如果所調用的方法為native的,則PC寄存器中不儲存任何資訊。
2 JVM棧
JVM棧是線程私人的,每個線程建立的同時都會建立JVM棧,JVM棧中存放的為當前線程中局部基本類型的變數(java中定義的八種基本類型:boolean、char、byte、short、int、long、float、double)、部分的返回結果以及Stack Frame,非基本類型的對象在JVM棧上僅存放一個指向堆上的地址,因此Java中基本類型的變數是值傳遞,而非基本類型的變數是引用傳遞,Sun JDK的實現中JVM棧的空間是在實體記憶體上分配的,而不是從堆上分配。
由於JVM棧是線程私人的,因此其在記憶體配置上非常高效,並且當線程運行完畢後,這些記憶體也就被自動回收。
當JVM棧的空間不足時,會拋出StackOverflowError的錯誤,在Sun JDK中可以通過-Xss來指定棧的大小,例如如下代碼:
new Thread(new Runnable(){
public void run() {
loop(0);
}
private void loop (int i){
if(i!=1000){
i++;
loop (i);
}
else{
return;
}
}
}).start();
當JVM參數設定為-Xss1K,運行後會報出類似下面的錯誤:
Exceptionin thread "Thread-0"java.lang.StackOverflowError
3 堆(Heap)
Heap是大家最為熟悉的地區,它是JVM用來儲存物件執行個體以及數組值的地區,可以認為Java中所有通過new建立的對象的記憶體都在此分配,Heap中的對象的記憶體需要等待GC進行回收,Heap在32位的作業系統上最大為2G,在64位的作業系統上則沒有限制,其大小通過-Xms和-Xmx來控制,-Xms為JVM啟動時申請的最小Heap記憶體,預設為實體記憶體的1/64但小於1G,-Xmx為JVM可申請的最大Heap記憶體,預設為實體記憶體的1/4,預設當空餘堆記憶體小於40%時,JVM會增大Heap的大小到-Xmx指定的大小,可通過-XX:MinHeapFreeRatio=來指定這個比例,當空餘堆記憶體大於70%時,JVM會將Heap的大小往-Xms指定的大小調整,可通過-XX:MaxHeapFreeRatio=來指定這個比例,但對於運行系統而言,為了避免頻繁的Heap Size的大小,通常都會將-Xms和-Xmx的值設成一樣,因此這兩個用於調整比例的參數通常是沒用的。其實jvm中對於堆記憶體的分配、使用、管理、收集等有更為精巧的設計,具體可以在JVM堆記憶體分析中進行詳細介紹。
當堆中需要使用的記憶體超過其允許的大小時,會拋出OutOfMemory的錯誤資訊。
4 方法地區(MethodArea)
方法地區存放了所載入的類的資訊(名稱、修飾符等)、類中的靜態變數、類中定義為final類型的常量、類中的Field資訊、類中的方法資訊,當開發人員在程式中通過Class對象中的getName、isInterface等方法來擷取資訊時,這些資料都來源於方法地區,可見方法地區的重要性。同樣,方法地區也是全域共用的,它在虛擬機器啟動時在一定的條件下它也會被GC,當方法地區需要使用的記憶體超過其允許的大小時,會拋出OutOfMemory的錯誤資訊。
在Sun JDK中這塊地區對應的為PermanetGeneration,又稱為持久代,預設為64M,可通過-XX:PermSize以及-XX:MaxPermSize來指定其大小。
5 運行時常量池(RuntimeConstant Pool)
類似C中的符號表,存放的為類中的固定的常量資訊、方法和Field的引用資訊等,其空間從方法地區中分配。類或介面的常量池在該類的class檔案被java虛擬機器成功裝載時分配。
6 本地方法堆棧(NativeMethod Stacks)
JVM採用本地方法堆棧來支援native方法的執行,此地區用於儲存每個native方法調用的狀態。
例如有這麼一段代碼:
public class A{
public static void main(String[] args) {
String a = "a";
String b = "b";
String ab = "ab";
System.out.println((a + b) == ab); //false
System.out.println(("a" +"b") == ab); // true
final String afinal = "a";
String result = afinal + "b";
System.out.println(result == ab); //true
String plus = a + "b";
System.out.println(plus == ab); //false
System.out.println(plus.intern() ==ab); // true
}
}
分析下上面代碼執行的結果,可通過javap –verbose A來輔助理解分析。
(a+b)==ab
a+b是兩個變數相加,需要到運行時才能確定其值,到運行時後JVM會為兩者相加後產生一個新的對象,因此a+b==ab的結果為false。
(“a”+”b”)==ab
“a”+”b”是常量,在編譯時間JVM已經將其變為”ab”字串了,而ab=”ab”也是常量,這兩者在常量池即為同一地址,因此(“a”+”b”)==ab為true。
result==ab
result=afinal+”b”,afinal是個final的變數, result在編譯時間也已經被轉變為了”ab”,和”ab”在常量池中同樣為同一地址,因此result==ab為true。
plus=ab
plus和a+b的情況是相同的,因此plus==ab為false。
plus.intern()==ab
這裡的不同點在於調用了plus.intern()方法,這個方法的作用是擷取plus指向的常量池地址,因此plus.intern()==ab為true。
在掌握了JVM對象記憶體配置的機制後,接下來看看JVM是如何做到自動的對象記憶體回收的,這裡指的的是Heap以及Method Area的回收,其他幾個地區的回收都由JVM簡單的按生命週期來進行管理
JVM為運行一個程式定義了幾種資料區(DataArea),包括:pc寄存器、JVM堆棧、堆、方法區(Method Area)、運行時常量池(Runtime Constant Pool)以及本機方法堆棧(Native Method Stacks),這些資料區根據其生存期可以分為兩種,一種就是和JVM的生存期相同(包括堆和方法區),一種和線程的生存期相同(其它的),和JVM生存期相同的資料區在JVM啟動的時候被建立並在JVM退出的時候被銷毀,而和線程生存期相同的資料區是每個線程一個的,他們線上程建立的時候被建立,線上程被銷毀的時候被銷毀。
JVM記憶體結構和6大地區