1 java虛擬機的基本結構如圖:java
1)類加載子系統負責從文件系統或者網絡中加載Class信息,加載的類信息存放於一塊稱爲方法區的內存空間。除了類的信息外,方法區中可能還會存放運行時常量池信息,包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)。算法
2)java堆在虛擬機啓動的時候創建,它是java程序最主要的內存工做區域。幾乎全部的java對象實例都存放在java堆中。堆空間是全部線程共享的,這是一塊與java應用密切相關的內存空間。數組
3)java的NIO庫容許java程序使用直接內存。直接內存是在java堆外的、直接向系統申請的內存空間。一般訪問直接內存的速度會優於java堆。所以出於性能的考慮,讀寫頻繁的場合可能會考慮使用直接內存。因爲直接內存在java堆外,所以它的大小不會直接受限於Xmx指定的最大堆大小,可是系統內存是有限的,java堆和直接內存的總和依然受限於操做系統能給出的最大內存。網絡
4)垃圾回收系統是java虛擬機的重要組成部分,垃圾回收器能夠對方法區、java堆和直接內存進行回收。其中,java堆是垃圾收集器的工做重點。和C/C++不一樣,java中全部的對象空間釋放都是隱式的,也就是說,java中沒有相似free()或者delete()這樣的函數釋放指定的內存區域。對於再也不使用的垃圾對象,垃圾回收系統會在後臺默默工做,默默查找、標識並釋放垃圾對象,完成包括java堆、方法區和直接內存中的全自動化管理。數據結構
5)每個java虛擬機線程都有一個私有的java棧,一個線程的java棧在線程建立的時候被建立,java棧中保存着幀信息,java棧中保存着局部變量、方法參數,同時和java方法的調用、返回密切相關。eclipse
6)本地方法棧和java棧很是相似,最大的不一樣在於java棧用於方法的調用,而本地方法棧則用於本地方法的調用,做爲對java虛擬機的重要擴展,java虛擬機容許java直接調用本地方法(一般使用C編寫)jvm
7)PC(Program Counter)寄存器也是每個線程私有的空間,java虛擬機會爲每個java線程建立PC寄存器。在任意時刻,一個java線程老是在執行一個方法,這個正在被執行的方法稱爲當前方法。若是當前方法不是本地方法,PC寄存器就會指向當前正在被執行的指令。若是當前方法是本地方法,那麼PC寄存器的值就是undefined函數
8)執行引擎是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棧上,並指向堆中兩個實例。
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棧溢出錯誤。
例2 使用遞歸,因爲遞歸沒有出口,這段代碼可能會拋出棧溢出錯誤,在拋出棧溢出錯誤時,打印最大的調用深度
package com.jvm;
public class TestStackDeep {
private static int count =0;
public static void recursion(){
count ++;
recursion();
}
public static void main(String[] args) {
try{
recursion();
}catch(Throwable e){
System.out.println("deep of calling ="+count);
e.printStackTrace();
}
}
}
使用參數-Xss128K執行上面代碼(在eclipse中右鍵選擇Run As-->run Configurations....設置Vm arguments),部分結果如圖:
能夠看出,在進行大約1079次調用以後,發生了棧溢出錯誤,經過增大-Xss的值,能夠得到更深的層次調用,嘗試使用參數-Xss256K執行上述代碼,可能產生以下輸出,很明顯,調用層次有明顯的增長:
注意:函數嵌套調用的層次在很大程度上由棧的大小決定,棧越大,函數支持的嵌套調用次數就越多。
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數組已經被回收釋放。