您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關如何理解java虛擬機的基本結構,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結了以下內容,希望大家根據這篇文章可以有所收獲。
類加載子系統:負責從文件系統或者網絡中加載class信息,加載的類信息存放于一塊稱為方法區的內存空間中。除了類的信息,方法區中可能還會存放運行時常量池信息,包括字符串字面量和數字常量(這部分常量信息是class文件中常量池部分的內存映射)。
java堆:java堆在虛擬機啟動的時候建立,它是java程序最主要的內存工作區域。幾乎所有的java對象實例都存放于java堆中。堆空間是所有線程共享的,這是一塊與java應用密切相關的內存區域。
直接內存:java的NIO庫允許程序使用直接內存。直接內存是在java堆外的、直接向系統申請的內存區域。通常,訪問直接內存的速度會優于java堆。因此,出于性能考慮,讀寫頻繁的場合可能會考慮使用直接內存。由于直接內存在java堆外,因此,它的大小不會直接受限于Xmx指定的最大堆大小,但是系統內存是有限的,java堆和直接內存的總和依然受限于操作系統的最大內存。
垃圾回收系統:垃圾回收系統是java虛擬機有重要組成部分,垃圾回收器可以對方法區、java堆和直接內存進行回收。其中,java堆是垃圾收集器的工作重點。和 C/C++
不同,java中所有的對象空間釋放都是隱式的。也就是說,java中沒有類似 free()
或者 delete()
這樣的函數釋放指定的內存區域。對于不再使用的垃圾對象,垃圾回收系統會在后臺默默工作,默默查找、標識并釋放垃圾對象,完成包括java堆、方法區和直接內存中的全自動化管理。
每一個java虛擬機線程都有一個私有的java棧,一個線程的java棧在線程創建的時候被創建,java棧中保存著幀信息,java棧中保存著局部變量、方法參數,同時和java方法的調用、返回密切相關。
本地方法棧和java棧非常類似,最大的不同在于java棧用于方法的調用,而本地方法棧則用于本地方法的調用,作為對java虛擬機的重要擴展,java虛擬機允許java直接調用本地方法(通常使用C編寫)
PC(Program Counter
)寄存器也是每一個線程私有的空間,java虛擬機會為每一個java線程創建PC寄存器。在任意時刻,一個java線程總是在執行一個方法,這個正在被執行的方法稱為當前方法。如果當前方法不是本地方法,PC寄存器就會指向當前正在被執行的指令。如果當前方法是本地方法,那么PC寄存器的值就是undefined
執行引擎是java虛擬機的最核心組件之一,它負責執行虛擬機的字節碼,現代虛擬機為了提高執行效率,會使用即時編譯技術將方法編譯成機器碼后再執行。
根據java回收機制的不同,java堆有可能擁有不同的結構。最為常見的一種構成是將整個java堆分為新生代和老年代。其中新生代存放新生對象或者年齡不大的對象,老年代則存放老年對象。新生代有可能分為eden區、s0區、s1區,s0區和s1區也被稱為from和to區,他們是兩塊大小相同、可以互換角色的內存空間。
java棧是一塊線程私有的內存空間。如果說,java堆和程序數據密切相關,那么java棧就是和線程執行密切相關。線程執行的基本行為是函數調用,每次函數調用的數據都是通過java棧傳遞的。
在java棧中保存的主要內容為棧幀。每一次函數調用,都會有一個對應的棧幀被壓入java棧,每一個函數調用結束,都會有一個棧幀被彈出java棧。如下圖:
函數1對應棧幀1,函數2對應棧幀2,依次類推。當前正在執行的函數所對應的幀就是當前幀(位于棧頂),它保存著當前函數的局部變量、中間計算結果等數據。
當函數返回時,棧幀從java棧中被彈出,java方法區有兩種返回函數的方式,一種是正常的函數返回,使用return指令,另一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。
java虛擬機提供了參數-Xss
來指定線程的最大棧空間,這個參數也直接決定了函數調用的最大深度:
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(); } }
使用-Xss256K
參數,結果如下:
可以看到,在進行大約2900次調用后,發生了棧溢出錯誤,通過增大-Xss
的值,可以獲得更深的調用層次,嘗試使用參數-Xss512K
,可以看到調用次數明顯增加:
在一個棧幀中,至少包含局部變量表、操作數棧和幀數據區幾個部分。
局部變量表用于保存函數的參數以及局部變量。局部亦是表中的變量只在當前函數調用中有效,當函數調用結束后,函數棧幀銷毀,局部變量表也會隨之銷毀。
由于局部變量表在棧幀之中,因此,如果函數的參數和局部變量較多,會使局部變量表膨脹,從而每一次函數調用就會占用更多的棧空間,最終導致函數的嵌套調用次數減少。
下面這段代碼,第一個recursion()
函數有3個參數和10個局部變量,因此,其局部變量表含有13個變量,而第2個recursion()
函數不含有任何參數和局部變量。當這兩個函數被嵌套調用時,第2個rescursion()
函數可以擁有更深的調用層次。
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; recursion(a, b, c); } 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(); } }
使用-Xss256k
執行上述代碼中的第1個rescursion()
函數,結果如下:
使用-Xss256k
執行上述代碼中的第2個rescursion()
函數,結果如下:
可以看到,在相同的棧容量下,局部變量少的函數可以支持更深層次的函數調用。
棧楨中的局部變量表中的槽位是可以重用的,如果局部變量的作用域范圍超過了其作用域,那么在其作用域之后聲明的新的局部變量就很有可能會復用局部變量a的槽位,從而達到節省資源的目的。局部變量表中的變量也是重要的垃圾回收根節點,被局部變量表中直接或間接引用的對象都是不會回收的。
如以下代碼:
public void localVarGc1() { byte[] a = new byte[6 * 1024 * 1024]; 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) { Demo05 d = new Demo05(); d.localVarGc1(); //d.localVarGc2(); //d.localVarGc3(); //d.localVarGc4(); //d.localVarGc5(); }
上述代碼中,每一個localVarGcN()
函數都分配了一塊6MB的堆內存,并使用局部變量引用這塊空間。可以使用參數-XX:+PrintGC
分別執行上述函數,在輸出的日志中,可以看到垃圾回收前后堆的大小,進而推斷byte數組是否被回收。
在localVarGc1()
中,在申請空間后,立即進行垃圾回收,很多明顯,由于byte數組被變量a引用,因此無法回收這塊空間。執行結果如下:
[GC (System.gc()) 8765K->6664K(251392K), 0.0041586 secs] [Full GC (System.gc()) 6664K->6515K(251392K), 0.0039022 secs]
在localVarGc2()
中,在垃圾回收前,先將變量a置為null,使用byte數組失去強引用,故垃圾回收可以順利回收byte數組。執行結果如下:
[GC (System.gc()) 8765K->568K(251392K), 0.0012696 secs] [Full GC (System.gc()) 568K->395K(251392K), 0.0039405 secs]
對于localVarGc3()
,在垃圾回收前,先使用局部變量a失效,雖然變量a已經離開了作用域,但是變量a依然存在于局部變量表中,并且也指向這塊byte數組,故byte數組依然無法被回收。執行結果如下:
[GC (System.gc()) 8765K->6696K(251392K), 0.0039619 secs] [Full GC (System.gc()) 6696K->6515K(251392K), 0.0039020 secs]
對于localVarGc4()
,在垃圾回收前,不僅使用變量a失效,更是聲明了變量c,使變量c復用了變量a的字,由于變量a此時被銷毀,故垃圾回收器可以順利回收byte數組。執行結果如下:
[GC (System.gc()) 8765K->536K(251392K), 0.0010555 secs] [Full GC (System.gc()) 536K->370K(251392K), 0.0033685 secs]
對于localVarGc5()
,它首先調用了localVarGC1()
,很明顯,在localVarGc1()
中并沒有釋放byte數組,但在localVarGc1()
返回后,它的棧楨被銷毀,自然也包含了棧幀中的所有局部變量,故byte數組失去引用,在localVarGc5()
的垃圾回收中被回收。執行結果如下:
[GC (System.gc()) 8765K->6744K(251392K), 0.0034826 secs] [Full GC (System.gc()) 6744K->6539K(251392K), 0.0045563 secs] [GC (System.gc()) 6539K->6539K(251392K), 0.0007713 secs] [Full GC (System.gc()) 6539K->395K(251392K), 0.0032212 secs]
操作數棧主要用于保存計算過程的中間結果,同事作為計算過程中變量臨時的存儲空間。操作數棧也是一個先進后出的數據結構,只支持入棧和出棧兩種操作。
幀數據區時候為了支持常量池解析、正常方法返回和異常處理等。大部分Java字節碼指令需要進行常量池訪問,在幀數據區中保存著訪問常量池的指針,方便程序訪問常量池。
提示:由于每次函數調用都會產生對應的棧幀,從而占用一定的棧空間,因此,如果棧空間不足,那么函數調用自然無法繼續進行下去。當請求的棧深度大于最大可用棧深度時,系統會拋出StackOverflowError棧溢出錯誤。 舉個例子:
棧上分配是Java虛擬機提供的一項優化技術,它的基本思想是:對于那些線程私有的對象(這里指不可能被其他線程訪問的對象),可以將它們打散分配在棧上,而不是分配在堆上。分配在棧上的好處是可以在函數調用結束后自行銷毀,而不需要垃圾回收器的介入,從而提高系統的性能。
棧上分配的以及技術基礎是進行逃逸分析。逃逸分析的目的是判斷對象的作用域是否有可能逃逸出函數體。
下面這個簡單示例顯示了對非逃逸對象的棧上分配:
public static class User { public int id; public String name = ""; } public static void alloc() { User u = new User(); u.id = 5; u.name = "geym0909"; } public static void main(String[] args) { long b = System.currentTimeMillis(); for(int i = 0; i < 10_0000_0000; i++) { alloc(); } long e = System.currentTimeMillis(); System.out.println(e - b); }
上述代碼在主函數中進行了1億次alloc()
調用來創建對象,由于User
對象實例需要占用約16字節的空間,因此累計分配空間將近1.5GB。如果堆空間小于這個值,就必然會發生GC。使用如下參數運行上述代碼:
-server -Xmx10m -Xms10m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:-UseTLAB -XX:+EliminateAllocations
這里使用參數-server
執行程序,因為在Server模式下,才可以啟用逃逸分析。
參數-XX:+DoEscapeAnalysis
啟用逃逸分析。
-Xms10m
、-Xmx10m
指定了最大與最小堆空間都是10m
-XX:+PrintGC
將打印GC日志
-XX:+EliminateAllocations
開啟了標量替換(默認打開),允許將對象打散分配在棧上,比如對象擁有id與name兩個字段,那么這兩個字段將會被視為兩個獨立的局部變量進行分配。
-XX:-UseTLAB
關閉TLAB
程序執行后,結果如下:
注:在本人機器上,使用如下參數(即不指定任何棧上分配相關的參數),結果依然無大量gc日志:
-server -Xmx10m -Xms10m -XX:+PrintGC
再關閉逃逸分析,則結果如下:
-server -Xmx10m -Xms10m -XX:+PrintGC -XX:-DoEscapeAnalysis
可見,在本人機器上逃逸分析、棧上分配是默認開啟的。
對于大量的零散小對象,棧上分配提供了一種很好的對象分配優化策略,棧上分配速度快,并且可以有效避免垃圾回收帶來的負面影響,但由于和堆空間相比,棧空間較小,因此,大對象無法也不適用在棧上分配。
和堆一樣,方法區是一塊所有線程共享的內存區域,它用于保存系統的類信息,比如類的字段、方法、常量池等。方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區的溢出,虛擬機同樣會拋出內存溢出錯誤。
在JDK1.6、JDK1.7中,方法區可以理解為永久區(Perm)。永久區可以使用參數 -XX:PermSize
和 -XX:MaxPermSize
指定,默認情況下,-XX:MaxPermSize
為64M。一個大的永久區可以保存更多的類信息。如果系統使用了一些動態代理,那么有可能會在運行時生成大量的類,如果這樣,就需要設置一個合理的永久區大小,確保不發生永久區內存溢出。
在JDK1.8中,永久區已經被徹底移除,取而代之的是元數據區,元數據區大小可以使用參數 -XX:MaxMetaspaceSize
指定(一個大的元數據區可以使系統支持更多的類),這是一塊堆外的直接內存。與永久區不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統內存。
如果元數據區發生異常,虛擬機一樣會拋出異常。
-server
:使用server
模式啟動jvm,對應也有-client
,使用client
模式啟動jvm。對于server
模式,jvm啟動較慢,因為jvm會收集系統信息并進行優化在提高程序的運行效率;對于client
模式,jvm啟動較快,但由于沒有收集運行時的信息導致優化不足,后期運行效率可能會降低。
參數-XX:+DoEscapeAnalysis
啟用逃逸分析。
-Xms10m
、-Xmx10m
指定了最大與最小堆空間都是10m
-Xss256k
:指定棧大小為256k
-XX:+PrintGC
將打印GC日志
-XX:+EliminateAllocations
開啟了標量替換(默認打開),允許將對象打散分配在棧上,比如對象擁有id與name兩個字段,那么這兩個字段將會被視為兩個獨立的局部變量進行分配。
-XX:-UseTLAB
關閉TLAB
-XX:PermSize
和 -XX:MaxPermSize
:在JDK1.6、JDK1.7中,方法區可以理解為永久區(Perm)。永久區可以使用參數 -XX:PermSize
和 -XX:MaxPermSize
指定。默認情況下,-XX:MaxPermSize
為64M。
-XX:MaxMetaspaceSize
:在JDK1.8中,永久區已經被徹底移除,取而代之的是元數據區,元數據區大小可以使用參數 -XX:MaxMetaspaceSize
指定。這是一塊堆外的直接內存。與永久區不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統內存。
看完上述內容,你們對如何理解java虛擬機的基本結構有進一步的了解嗎?如果還想了解更多知識或者相關內容,請關注億速云行業資訊頻道,感謝大家的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。