jvm的基本結構以及各部分詳解(轉)
原文鏈接:https://www.cnblogs.com/zwbg/p/6194470.html
1、java虛擬機的基本結構
圖:
1、類加載器子系統從文件系統或者網絡中加載Class信息,類信息(字段、方法)存放於方法區,方法區中還存在常量池(字符串常量和數字常量)信息
2、方法區,存放類方法信息以及常量池
3、java堆在虛擬機啟動時候建立,java程序最主要的內存工作區域。幾乎所有的java對象實例都存放在java堆中。
4、棧,每一個java虛擬機都有一個私有的java棧,一個線程的java棧在線程創建的時候被創建,java棧中保存著幀信息,java棧中保存著局部變量、方法參數,同時和java的方法調用、返回密切相關,內容指的是局部變量表部分,局部變量表存放了編譯器可以知道的基本數據類型,對象引用和返回後所指向的字節碼的地址。
5、本地方法棧和java棧非常類似,最大的不同在於java棧用於方法的調用,而本地方法棧用於本地方法的調用,作為java虛擬機的重要拓展,java虛擬機允許java直接調用本地方法
6、java的NIO允許java程序使用直接內存。直接內存是在java堆外的、直接向系統申請的內存空間。通常訪問直接內存的速度回優於java堆。因此出於性能的考慮,讀寫頻繁的場合可能會考慮使用直接內存。直接內存在堆外,因此Xmx指定的最大堆大小不會影響到它,但是系統內存是有限的,java堆和直接內存的總和依然受限於操作系統能給出的最大內存。
7、垃圾回收系統是java虛擬機的重要組成部分,垃圾回收器可以對方法區、java堆和直接內存進行回收。其中,java堆是垃圾收集器的工作重點。java中的所有對象空間的釋放都是隱式的,也就是說,java中沒有類似free()或者delete()這樣的函數釋放指定的內存區域。
8、PC(Progam Counter)寄存器也是每一個線程私有的空間,java虛擬機會為每一個java線程創建PC寄存器。在任意時刻,一個java線程總是在執行一個方法,這個正在被執行的方法被稱為當前方法。如果當前方法不是本地方法,PC寄存器就會指向當前正在被執行的指令。如果當前方法是本地方法,那麽在PC寄存器的值就是undefined。寄存器是一個概念模型,各種虛擬機可能會通過一些更高效的方式去實現。在概念模型中,通過寄存器的值來選取需要執行的指令,分支,循環,跳轉,異常處理,線程恢復等基礎功能。
9、執行引擎是java虛擬機的最核心的組件之一,它負責執行虛擬機的字節碼,現代虛擬機為了提高執行效率,會使用即時編譯技術獎方法編譯成機器碼後再執行
2、java堆
java堆是和應用程序關系最為密切的內存空間,幾乎所有的對象都存放在堆上。並且java堆是完全自動化管理的,通過垃圾回收機制,垃圾對象會被自動清理,而不需要顯示的釋放
根據java回收機制的不同,java堆有可能擁有不同的結構。最常見的一種構成是將整個java堆分為新生代和老生代。其中新生代存放新生對象或者年齡不大的對象,老生代則存放老生對象。新生代有可能分為eden區、s0區、s1區,s0和s1區也被稱為from和to區,他們是兩塊大小相同、可以互換角色的內存空間。
在絕大多數數情況下,對象首先分配在eden區,在一次新生代回收之後,如果對象還存活,則進入s0或者s1,每經過一次新生代回收,對象如果存活,它的年齡就會加1.當對象的年齡達到一定條件後,就會被認為是老年對象,進入老年代。
例1 :通過簡單的示例,展示java堆、方法區和java棧之間的關系
package com.jvm;
public class SimpleHeap {
private int id;
public SimpleHeap(int id){
this.id = id;
}
public void show(){
System.out.println("My id is "+id);
}
public static void main(String[] args) {
SimpleHeap s1 = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
s1.show();
s2.show();
}
}
該代碼聲明了一個類,並在main函數中創建了兩個SimpleHeap實例。此時,各對象和局部變量的存放情況如圖:
SimpleHeap實例本身分配在堆中,描述SimpleHeap類的信息存放在方法區,main函數中的s1 s2局部變量存放在java棧上,並指向堆中兩個實例。
java堆可以通過-Xmx和-Xms控制
3、java棧
java棧是一塊線程私有的內存空間。如果說,java堆和程序數據密切相關,那麽java棧就是和線程執行密切相關。線程執行的基本行為是函數調用,每次函數調用的數據都是通過java棧傳遞的。
java棧與數據結構上的棧有著類似的含義,它是一個先進後出的數據結構,只支持出棧和入棧兩種操作,在java棧中保存的主要內容為棧幀。每一次函數調用,都會有一個對應的棧幀被壓入java棧,每一個函數調用結束,都會有一個棧幀被彈出java棧。如下圖:棧幀和函數調用。函數1對應棧幀1,函數2對應棧幀2,依次類推。函數1中調用函數2,函數2中調用函數3,函數3調用函數4.當函數1被調用時,棧幀1入棧,當函數2調用時,棧幀2入棧,當函數3被調用時,棧幀3入棧,當函數4被調用時,棧幀4入棧。當前正在執行的函數所對應的幀就是當前幀(位於棧頂),它保存著當前函數的局部變量、中間計算結果等數據。回字形,類似過濾器。
當函數返回時,棧幀從java棧中被彈出,java方法區有兩種返回函數的方式,一種是正常的函數返回,使用return指令,另一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。
在一個棧幀中,至少包含局部變量表、操作數棧和幀數據區幾個部分。
提示:由於每次函數調用都會產生對應的棧幀,從而占用一定的棧空間,因此,如果棧空間不足,那麽函數調用自然無法繼續進行下去。當請求的棧深度大於最大可用棧深度時,系統會拋出StackOverflowError棧溢出錯誤。
運行時可以設置-Xss設置棧的大小,從而提高棧可用深度。
3.1、棧幀組成之局部變量表
局部變量表示棧幀的重要組成部分之一。它用於保存函數的參數以及局部變量,局部變量表中的變量只在當前函數調用中有效,當函數調用結束,隨著函數棧幀的彈出而銷毀,局部變量表也會隨之銷毀。
由於局部變量表在棧幀之中,因此,如果函數的參數和局部變量很多,會使得局部變量表膨脹,從而每一次函數調用就會占用更多的棧空間,最終導致函數的嵌套調用次數減少。
示例3:一個recursion函數含有3個參數和10個局部變量,因此,其局部變量表含有13個變量,而第二個recursion函數不再含有任何參數和局部變量,當這兩個函數被嵌套調用時,第二個recursion函數可以擁有更深的調用層次。
package com.jvm;
public class TestStackDeep2 {
private static int count = 0;
public static void recursion(long a,long b,long c){
long e=1,f=2,g=3,h=4,i=5,k=6,q=7,x=8,y=9,z=10;
count ++;
recursion(a,b,c);
}
public static void recursion(){
count++;
recursion();
}
public static void main(String[] args) {
try{
recursion(0L,0L,0L);
//recursion();
}catch(Throwable e){
System.out.println("deep of calling = "+count);
e.printStackTrace();
}
}
}
使用參數-Xss128K執行上述代碼中的第一個帶參recursion(long a,long b,long c)函數,輸出結果為:
使用虛擬機參數-Xss128K執行上述代碼中第二個不帶參數的recursion()函數(當然需要把第一個函數註釋掉),輸出結果為:
可以看出,在相同的棧容量下,局部變量少的函數可以支持更深的函數調用。
使用jclasslib工具可以查看函數的局部變量表,如下圖:最大局部變量表大小
該圖顯示了第一個帶參recursion(long a,long b,long c)的最大局部變量表的大小為26個字,因為該函數包含總共13個參數和局部變量,且都為long型,long和double在局部變量表中需要占用2個字,其他如int short byte 對象引用等占用一個字。
說明:字(word)指的是計算機內存中占據一個單獨的內存單元編號的一組二進制串,一般32位計算機上一個字為4個字節長度。
通過jclasslib工具查看該類的Class文件中局部變量表的內容,(這裏說的局部變量表和上述說的局部變量表不同,這裏指Class文件的一個屬性,而上述的局部變量表指java棧空間的一部分)
可以看到,在Class文件的局部變量表中,顯示了每個局部變量的作用域範圍、所在槽位的索引(index列)、變量名(name列)和數據類型(J表示long型)
棧幀中局部變量表中的槽位是可以重用的,如果一個局部變量過了其作用域,那麽在其作用域之後申明的新的局部變量就很有可能會復用過期局部變量的槽位,從而達到節省資源的目的。
示例4 :顯示局部變量表的復用,在localvar1函數中,局部變量a和b都作用到了函數的末尾,故b無法復用a所在的位置。而在localvar2()函數中,局部變量a在第?行不再有效,故局部變量b可以復用a的槽位(1個字)
package com.jvm;
public class TestReuse {
public static void localvar1(){
int a=0;
System.out.println(a);
int b=0;
}
public static void localvar2(){
{
int a=0;
System.out.println(a);
}
int b=0;
}
}
如圖顯示localvar1()函數的局部變量表,該函數局部變量大小為2個字,(最大局部變量表中一般第一個局部變量槽位是this引用)第一個槽位是變量a,第二個槽位是變量b,每個變量占一個字。
而localvar2()函數的局部變量表信息如下圖,雖然和localvar1()一樣,但是b復用了a的槽位,(從他們都占用同一個槽位index都是0可以看出),因此在整個函數執行中,同時存在的局部變量為1字。
局部變量表中的變量也是垃圾回收根節點,只要被局部變量表中直接或者間接引用的對象都是不會被回收的。
示例5:通過一個簡單示例,展示局部變量對垃圾回收的影響
package com.jvm;
public class LocalvarGC {
public void localvarGc1(){
byte[] a = new byte[6*1024*1024];//6M
System.gc();
}
public void localvarGc2(){
byte[] a = new byte[6*1024*1024];
a = null;
System.gc();
}
public void localvarGc3(){
{
byte[] a = new byte[6*1024*1024];
}
System.gc();
}
public void localvarGc4(){
{
byte[] a = new byte[6*1024*1024];
}
int c = 10;
System.gc();
}
public void localvarGc5(){
localvarGc1();
System.gc();
}
public static void main(String[] args) {
LocalvarGC ins = new LocalvarGC();
ins.localvarGc1();
}
}
每一個localvarGcN()函數都分配了一塊6M的堆內存,並使用局部變量引用這塊空間。
在localvarGc1()中,在申請空間後,立即進行垃圾回收,很明顯由於byte數組被變量a引用,因此無法回收這塊空間。
在localvarGc2()中,在垃圾回收前,先將變量a置為null,使得byte數組失去強引用,故垃圾回收可以順利回收byte數組。
在localvarGc3()中,在進行垃圾回收前,先使局部變量a失效,雖然變量a已經離開了作用域,但是變量a依然存在於局部變量表中,並且也指向這塊byte數組,故byte數組依然無法被回收。
對於localvarGc4(),在垃圾回收之前,不僅使變量a失效,更是聲明了變量c,使變量c復用了變量a的字,由於變量a此時被銷毀,故垃圾回收器可以順利回收數組byte
對於localvarGc5(),它首先調用了localvarGc1(),很明顯,在localvarGc1()中並沒有釋放byte數組,但在localvarGc1()返回後,它的棧幀被銷毀,自然也包含了棧幀中的所有局部變量,故byte數組失去了引用,在localvarGc5()的垃圾回收中被回收。
可以使用-XX:+printGC執行上述幾個函數,在輸出日誌裏,可以看到垃圾回收前後堆的大小,進而推斷出byte數組是否被回收。
下面的輸出是函數localvarGc4()的運行結果:
[GC (System.gc()) 7618K->624K(94208K), 0.0015613 secs]
[Full GC (System.gc()) 624K->526K(94208K), 0.0070718 secs]
從日誌中可以看出,堆空間從回收前的7618K變為回收後的624K,釋放了>6M的空間,byte數組已經被回收釋放。
其他圖:
有關的比較好的文章鏈接:https://my.oschina.net/hosee/blog/638753?nocache=1536564436445
jvm的基本結構以及各部分詳解(轉)