JVM ≠ Japanese Video's Manhtml
寫這篇的主要緣由呢,就是爲了能在簡歷上寫個「熟悉JVM底層結構」,另外一個緣由就是能讓讀我文章的你們也寫上這句話,真是個助人爲樂的帥小夥。。。。嗯,不僅僅只是面向面試學習哈,更重要的是構建本身的 JVM 知識體系,Javaer 們技術棧要有廣度,可是 JVM 的掌握必須有深度java
點贊+收藏 就學會系列,文章收錄在 GitHub JavaKeeper ,N線互聯網開發必備技能兵器譜,筆記自取git
反正我是帶着這些問題往下讀的github
內存是很是重要的系統資源,是硬盤和 CPU 的中間倉庫及橋樑,承載着操做系統和應用程序的實時運行。JVM 內存佈局規定了 Java 在運行過程當中內存申請、分配、管理的策略,保證了 JVM 的高效穩定運行。不一樣的 JVM 對於內存的劃分方式和管理機制存在着部分差別。面試
下圖是 JVM 總體架構,中間部分就是 Java 虛擬機定義的各類運行時數據區域。算法
Java 虛擬機定義了若干種程序運行期間會使用到的運行時數據區,其中有一些會隨着虛擬機啓動而建立,隨着虛擬機退出而銷燬。另一些則是與線程一一對應的,這些與線程一一對應的數據區域會隨着線程開始和結束而建立和銷燬。編程
下面咱們就來一一解毒下這些內存區域,先從最簡單的入手windows
程序計數寄存器(Program Counter Register),Register 的命名源於 CPU 的寄存器,寄存器存儲指令相關的線程信息,CPU 只有把數據裝載到寄存器纔可以運行。數組
這裏,並不是是廣義上所指的物理寄存器,叫程序計數器(或PC計數器或指令計數器)會更加貼切,而且也不容易引發一些沒必要要的誤會。JVM 中的 PC 寄存器是對物理 PC 寄存器的一種抽象模擬。緩存
程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。
PC 寄存器用來存儲指向下一條指令的地址,即將要執行的指令代碼。由執行引擎讀取下一條指令。
(分析:進入class文件所在目錄,執行 javap -v xx.class
反解析(或者經過 IDEA 插件 Jclasslib
直接查看,上圖),能夠看到當前類對應的Code區(彙編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等信息。)
OutOfMemoryError
狀況的區域👨💻:使用PC寄存器存儲字節碼指令地址有什麼用呢?爲何使用PC寄存器記錄當前線程的執行地址呢?
🙋♂️:由於CPU須要不停的切換各個線程,這時候切換回來之後,就得知道接着從哪開始繼續執行。JVM的字節碼解釋器就須要經過改變PC寄存器的值來明確下一條應該執行什麼樣的字節碼指令。
👨💻:PC寄存器爲何會被設定爲線程私有的?
🙋♂️:多線程在一個特定的時間段內只會執行其中某一個線程方法,CPU會不停的作任務切換,這樣必然會致使常常中斷或恢復。爲了可以準確的記錄各個線程正在執行的當前字節碼指令地址,因此爲每一個線程都分配了一個PC寄存器,每一個線程都獨立計算,不會互相影響。
Java 虛擬機棧(Java Virtual Machine Stacks),早期也叫 Java 棧。每一個線程在建立的時候都會建立一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次 Java 方法調用,是線程私有的,生命週期和線程一致。
做用:主管 Java 程序的運行,它保存方法的局部變量、部分結果,並參與方法的調用和返回。
特色:
棧中可能出現的異常:
Java 虛擬機規範容許 Java虛擬機棧的大小是動態的或者是固定不變的
能夠經過參數-Xss
來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。
官方提供的參考工具,可查一些參數和操做:docs.oracle.com/javase/8/do…
棧中存儲什麼?
JVM 直接對 Java 棧的操做只有兩個,對棧幀的壓棧和出棧,遵循「先進後出/後進先出」原則
在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱爲當前棧幀(Current Frame),與當前棧幀對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)
執行引擎運行的全部字節碼指令只針對當前棧幀進行操做
若是在該方法中調用了其餘方法,對應的新的棧幀會被建立出來,放在棧的頂端,稱爲新的當前棧幀
不一樣線程中所包含的棧幀是不容許存在相互引用的,即不可能在一個棧幀中引用另一個線程的棧幀
若是當前方法調用了其餘方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀從新成爲當前棧幀
Java 方法有兩種返回函數的方式,一種是正常的函數返回,使用 return 指令,另外一種是拋出異常,無論用哪一種方式,都會致使棧幀被彈出
IDEA 在 debug 時候,能夠在 debug 窗口看到 Frames 中各類方法的壓棧和出棧狀況
每一個棧幀(Stack Frame)中存儲着:
繼續深拋棧幀中的五部分~~
maximum local variables
數據項中。在方法運行期間是不會改變局部變量表的大小的局部變量表最基本的存儲單元是 Slot(變量槽)
在局部變量表中,32 位之內的類型只佔用一個 Slot(包括returnAddress類型),64 位的類型(long和double)佔用兩個連續的 Slot
JVM 會爲局部變量表中的每個 Slot 都分配一個訪問索引,經過這個索引便可成功訪問到局部變量表中指定的局部變量值,索引值的範圍從 0 開始到局部變量表最大的 Slot 數量
當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照順序被複制到局部變量表中的每個 Slot 上
若是須要訪問局部變量表中一個 64bit 的局部變量值時,只須要使用前一個索引便可。(好比:訪問 long 或double 類型變量,不容許採用任何方式單獨訪問其中的某一個 Slot)
若是當前幀是由構造方法或實例方法建立的,那麼該對象引用 this 將會存放在 index 爲 0 的 Slot 處,其他的參數按照參數表順序繼續排列(這裏就引出一個問題:靜態方法中爲何不能夠引用 this,就是由於this 變量不存在於當前方法的局部變量表中)
棧幀中的局部變量表中的槽位是能夠重用的,若是一個局部變量過了其做用域,那麼在其做用域以後申明的新的局部變量就頗有可能會複用過時局部變量的槽位,從而達到節省資源的目的。(下圖中,this、a、b、c 理論上應該有 4 個變量,c 複用了 b 的槽)
每一個獨立的棧幀中除了包含局部變量表以外,還包含一個後進先出(Last-In-First-Out)的操做數棧,也能夠稱爲表達式棧(Expression Stack)
操做數棧,在方法執行過程當中,根據字節碼指令,往操做數棧中寫入數據或提取數據,即入棧(push)、出棧(pop)
某些字節碼指令將值壓入操做數棧,其他的字節碼指令將操做數取出棧。使用它們後再把結果壓入棧。好比,執行復制、交換、求和等操做
max_stack
數據項中HotSpot 的執行引擎採用的並不是是基於寄存器的架構,但這並不表明 HotSpot VM 的實現並無間接利用到寄存器資源。寄存器是物理 CPU 中的組成部分之一,它同時也是 CPU 中很是重要的高速存儲資源。通常來講,寄存器的讀/寫速度很是迅速,甚至能夠比內存的讀/寫速度快上幾十倍不止,不過寄存器資源卻很是有限,不一樣平臺下的CPU 寄存器數量是不一樣和不規律的。寄存器主要用於緩存本地機器指令、數值和下一條須要被執行的指令地址等數據。
基於棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操做的時候必然須要使用更多的入棧和出棧指令,這同時也就意味着將須要更多的指令分派(instruction dispatch)次數和內存讀/寫次數。因爲操做數是存儲在內存中的,所以頻繁的執行內存讀/寫操做必然會影響執行速度。爲了解決這個問題,HotSpot JVM 設計者們提出了棧頂緩存技術,將棧頂元素所有緩存在物理 CPU 的寄存器中,以此下降對內存的讀/寫次數,提高執行引擎的執行效率
方法調用不一樣於方法執行,方法調用階段的惟一任務就是肯定被調用方法的版本(即調用哪個方法),暫時還不涉及方法內部的具體運行過程。Class 文件的編譯過程當中不包括傳統編譯器中的鏈接步驟,一切方法調用在 Class文件裏面存儲的都是符號引用,而不是方法在實際運行時內存佈局中的入口地址(直接引用)。也就是須要在類加載階段,甚至到運行期才能肯定目標方法的直接引用。
【這一塊內容,除了方法調用,還包括解析、分派(靜態分派、動態分派、單分派與多分派),這裏先不介紹,後續再挖】
在 JVM 中,將符號引用轉換爲調用方法的直接引用與方法的綁定機制有關
對應的方法的綁定機制爲:早期綁定(Early Binding)和晚期綁定(Late Binding)。綁定是一個字段、方法或者類在符號引用被替換爲直接引用的過程,這僅僅發生一次。
在面向對象編程中,會頻繁的使用到動態分派,若是每次動態分派都要從新在類的方法元數據中搜索合適的目標有可能會影響到執行效率。爲了提升性能,JVM 採用在類的方法區創建一個虛方法表(virtual method table),使用索引表來代替查找。非虛方法不會出如今表中。
每一個類中都有一個虛方法表,表中存放着各個方法的實際入口。
虛方法表會在類加載的鏈接階段被建立並開始初始化,類的變量初始值準備完成以後,JVM 會把該類的方法表也初始化完畢。
用來存放調用該方法的 PC 寄存器的值。
一個方法的結束,有兩種方式
不管經過哪一種方式退出,在方法退出後都返回到該方法被調用的位置。方法正常退出時,調用者的 PC 計數器的值做爲返回地址,即調用該方法的指令的下一條指令的地址。而經過異常退出的,返回地址是要經過異常表來肯定的,棧幀中通常不會保存這部分信息。
當一個方法開始執行後,只有兩種方式能夠退出這個方法:
執行引擎遇到任意一個方法返回的字節碼指令,會有返回值傳遞給上層的方法調用者,簡稱正常完成出口
一個方法的正常調用完成以後究竟須要使用哪個返回指令還須要根據方法返回值的實際數據類型而定
在字節碼指令中,返回指令包含 ireturn(當返回值是 boolean、byte、char、short 和 int 類型時使用)、lreturn、freturn、dreturn 以及 areturn,另外還有一個 return 指令供聲明爲 void 的方法、實例初始化方法、類和接口的初始化方法使用。
在方法執行的過程當中遇到了異常,而且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出。簡稱異常完成出口
方法執行過程當中拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找處處理異常的代碼。
本質上,方法的退出就是當前棧幀出棧的過程。此時,須要恢復上層方法的局部變量表、操做數棧、將返回值壓入調用者棧幀的操做數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
正常完成出口和異常完成出口的區別在於:經過異常完成出口退出的不會給他的上層調用者產生任何的返回值
棧幀中還容許攜帶與 Java 虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息,但這些信息取決於具體的虛擬機實現。
簡單的講,一個 Native Method 就是一個 Java 調用非 Java 代碼的接口。咱們知道的 Unsafe 類就有不少本地方法。
爲何要使用本地方法(Native Method)?
Java 使用起來很是方便,然而有些層次的任務用 Java 實現起來也不容易,或者咱們對程序的效率很在乎時,問題就來了
java.lang.Thread
的 setPriority()
的方法是用Java 實現的,但它實現調用的是該類的本地方法 setPrioruty()
,該方法是C實現的,並被植入 JVM 內部。Java 虛擬機棧用於管理 Java 方法的調用,而本地方法棧用於管理本地方法的調用
本地方法棧也是線程私有的
容許線程固定或者可動態擴展的內存大小
StackOverflowError
異常OutofMemoryError
異常本地方法是使用 C 語言實現的
它的具體作法是 Mative Method Stack
中登記 native 方法,在 Execution Engine
執行時加載本地方法庫當某個線程調用一個本地方法時,它就進入了一個全新的而且再也不受虛擬機限制的世界。它和虛擬機擁有一樣的權限。
本地方法能夠經過本地方法接口來訪問虛擬機內部的運行時數據區,它甚至能夠直接使用本地處理器中的寄存器,直接從本地內存的堆中分配任意數量的內存
並非全部 JVM 都支持本地方法。由於 Java 虛擬機規範並無明確要求本地方法棧的使用語言、具體實現方式、數據結構等。若是 JVM 產品不打算支持 native 方法,也能夠無需實現本地方法棧
在 Hotspot JVM 中,直接將本地方棧和虛擬機棧合二爲一
棧是運行時的單位,而堆是存儲的單位。
棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎麼放、放在哪。
對於大多數應用,Java 堆是 Java 虛擬機管理的內存中最大的一塊,被全部線程共享。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例以及數據都在這裏分配內存。
爲了進行高效的垃圾回收,虛擬機把堆內存邏輯上劃分紅三塊區域(分代的惟一理由就是優化 GC 性能):
Java 虛擬機規範規定,Java 堆能夠是處於物理上不連續的內存空間中,只要邏輯上是連續的便可,像磁盤空間同樣。實現時,既能夠是固定大小,也能夠是可擴展的,主流虛擬機都是可擴展的(經過 -Xmx
和 -Xms
控制),若是堆中沒有完成實例分配,而且堆沒法再擴展時,就會拋出 OutOfMemoryError
異常。
年輕代是全部新對象建立的地方。當填充年輕代時,執行垃圾收集。這種垃圾收集稱爲 Minor GC。年輕一代被分爲三個部分——伊甸園(Eden Memory)和兩個倖存區(Survivor Memory,被稱爲from/to或s0/s1),默認比例是8:1:1
舊的一代內存包含那些通過許多輪小型 GC 後仍然存活的對象。一般,垃圾收集是在老年代內存滿時執行的。老年代垃圾收集稱爲 主GC(Major GC),一般須要更長的時間。
大對象直接進入老年代(大對象是指須要大量連續內存空間的對象)。這樣作的目的是避免在 Eden 區和兩個Survivor 區之間發生大量的內存拷貝
無論是 JDK8 以前的永久代,仍是 JDK8 及之後的元空間,均可以看做是 Java 虛擬機規範中方法區的實現。
雖然 Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫 Non-Heap(非堆),目的應該是與 Java 堆區分開。
因此元空間放在後邊的方法區再說。
Java 堆用於存儲 Java 對象實例,那麼堆的大小在 JVM 啓動的時候就肯定了,咱們能夠經過 -Xmx
和 -Xms
來設定
-Xmx
用來表示堆的起始內存,等價於 -XX:InitialHeapSize
-Xms
用來表示堆的最大內存,等價於 -XX:MaxHeapSize
若是堆的內存大小超過 -Xms
設定的最大內存, 就會拋出 OutOfMemoryError
異常。
咱們一般會將 -Xmx
和 -Xms
兩個參數配置爲相同的值,其目的是爲了可以在垃圾回收機制清理完堆區後再也不須要從新分隔計算堆的大小,從而提升性能
默認狀況下,初始堆內存大小爲:電腦內存大小/64
默認狀況下,最大堆內存大小爲:電腦內存大小/4
能夠經過代碼獲取到咱們的設置值,固然也能夠模擬 OOM:
public static void main(String[] args) {
//返回 JVM 堆大小
long initalMemory = Runtime.getRuntime().totalMemory() / 1024 /1024;
//返回 JVM 堆的最大內存
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 /1024;
System.out.println("-Xms : "+initalMemory + "M");
System.out.println("-Xmx : "+maxMemory + "M");
System.out.println("系統內存大小:" + initalMemory * 64 / 1024 + "G");
System.out.println("系統內存大小:" + maxMemory * 4 / 1024 + "G");
}
複製代碼
在默認不配置 JVM 堆內存大小的狀況下,JVM 根據默認值來配置當前內存大小
默認狀況下新生代和老年代的比例是 1:2,能夠經過 –XX:NewRatio
來配置
-XX:SurvivorRatio
來配置若在 JDK 7 中開啓了 -XX:+UseAdaptiveSizePolicy
,JVM 會動態調整 JVM 堆中各個區域的大小以及進入老年代的年齡
此時 –XX:NewRatio
和 -XX:SurvivorRatio
將會失效,而 JDK 8 是默認開啓-XX:+UseAdaptiveSizePolicy
在 JDK 8中,不要隨意關閉-XX:+UseAdaptiveSizePolicy
,除非對堆內存的劃分有明確的規劃
每次 GC 後都會從新計算 Eden、From Survivor、To Survivor 的大小
計算依據是GC過程中統計的GC時間、吞吐量、內存佔用量
java -XX:+PrintFlagsFinal -version | grep HeapSize
uintx ErgoHeapSizeLimit = 0 {product}
uintx HeapSizePerGCThread = 87241520 {product}
uintx InitialHeapSize := 134217728 {product}
uintx LargePageHeapSizeThreshold = 134217728 {product}
uintx MaxHeapSize := 2147483648 {product}
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
複製代碼
$ jmap -heap 進程號
複製代碼
-XX:MaxTenuringThreshold
)-XX:PetenureSizeThreshold
,對象會直接被分配到老年代爲對象分配內存是一件很是嚴謹和複雜的任務,JVM 的設計者們不只須要考慮內存如何分配、在哪裏分配等問題,而且因爲內存分配算法和內存回收算法密切相關,因此還須要考慮 GC 執行完內存回收後是否會在內存空間中產生內存碎片。
JVM 在進行 GC 時,並不是每次都對堆內存(新生代、老年代;方法區)區域一塊兒回收的,大部分時候回收的都是指新生代。
針對 HotSpot VM 的實現,它裏面的 GC 按照回收區域又分爲兩大類:部分收集(Partial GC),整堆收集(Full GC)
儘管不是全部的對象實例都可以在 TLAB 中成功分配內存,但 JVM 確實是將 TLAB 做爲內存分配的首選。
在程序中,能夠經過 -XX:UseTLAB
設置是否開啓 TLAB 空間。
默認狀況下,TLAB 空間的內存很是小,僅佔有整個 Eden 空間的 1%,咱們能夠經過 -XX:TLABWasteTargetPercent
設置 TLAB 空間所佔用 Eden 空間的百分比大小。
一旦對象在 TLAB 空間分配內存失敗時,JVM 就會嘗試着經過使用加鎖機制確保數據操做的原子性,從而直接在 Eden 空間中分配內存。
隨着 JIT 編譯期的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化,全部的對象都分配到堆上也漸漸變得不那麼「絕對」了。 ——《深刻理解 Java 虛擬機》
逃逸分析(Escape Analysis)是目前 Java 虛擬機中比較前沿的優化技術。這是一種能夠有效減小 Java 程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。經過逃逸分析,Java Hotspot 編譯器可以分析出一個新的對象的引用的使用範圍從而決定是否要將這個對象分配到堆上。
逃逸分析的基本行爲就是分析對象動態做用域:
例如:
public static StringBuffer craeteStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
複製代碼
StringBuffer sb
是一個方法內部變量,上述代碼中直接將sb返回,這樣這個 StringBuffer 有可能被其餘方法所改變,這樣它的做用域就不僅是在方法內部,雖然它是一個局部變量,稱其逃逸到了方法外部。甚至還有可能被外部線程訪問到,譬如賦值給類變量或能夠在其餘線程中訪問的實例變量,稱爲線程逃逸。
上述代碼若是想要 StringBuffer sb
不逃出方法,能夠這樣寫:
public static String createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
複製代碼
不直接返回 StringBuffer,那麼 StringBuffer 將不會逃逸出方法。
參數設置:
-XX"+DoEscapeAnalysis
顯式開啓開發中使用局部變量,就不要在方法外定義。
使用逃逸分析,編譯器能夠對代碼作優化:
JIT 編譯器在編譯期間根據逃逸分析的結果,發現若是一個對象並無逃逸出方法的話,就可能被優化成棧上分配。分配完成後,繼續在調用棧內執行,最後線程結束,棧空間被回收,局部變量對象也被回收。這樣就無需進行垃圾回收了。
常見棧上分配的場景:成員變量賦值、方法返回值、實例引用傳遞
public void keep() {
Object keeper = new Object();
synchronized(keeper) {
System.out.println(keeper);
}
}
複製代碼
如上代碼,代碼中對 keeper 這個對象進行加鎖,可是 keeper 對象的生命週期只在 keep()
方法中,並不會被其餘線程所訪問到,因此在 JIT編譯階段就會被優化掉。優化成:
public void keep() {
Object keeper = new Object();
System.out.println(keeper);
}
複製代碼
標量(Scalar)是指一個沒法再分解成更小的數據的數據。Java 中的原始數據類型就是標量。
相對的,那些的還能夠分解的數據叫作聚合量(Aggregate),Java 中的對象就是聚合量,由於其還能夠分解成其餘聚合量和標量。
在 JIT 階段,經過逃逸分析肯定該對象不會被外部訪問,而且對象能夠被進一步分解時,JVM 不會建立該對象,而會將該對象成員變量分解若干個被這個方法使用的成員變量所代替。這些代替的成員變量在棧幀或寄存器上分配空間。這個過程就是標量替換。
經過 -XX:+EliminateAllocations
能夠開啓標量替換,-XX:+PrintEliminateAllocations
查看標量替換狀況。
public static void main(String[] args) {
alloc();
}
private static void alloc() {
Point point = new Point(1,2);
System.out.println("point.x="+point.x+"; point.y="+point.y);
}
class Point{
private int x;
private int y;
}
複製代碼
以上代碼中,point 對象並無逃逸出 alloc()
方法,而且 point 對象是能夠拆解成標量的。那麼,JIT 就不會直接建立 Point 對象,而是直接使用兩個標量 int x ,int y 來替代 Point 對象。
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x="+x+"; point.y="+y);
}
複製代碼
咱們經過 JVM 內存分配能夠知道 JAVA 中的對象都是在堆上進行分配,當對象沒有被引用的時候,須要依靠 GC 進行回收內存,若是對象數量較多的時候,會給 GC 帶來較大壓力,也間接影響了應用的性能。爲了減小臨時對象在堆內分配的數量,JVM 經過逃逸分析肯定該對象不會被外部訪問。那就經過標量替換將該對象分解在棧上分配內存,這樣該對象所佔用的內存空間就能夠隨棧幀出棧而銷燬,就減輕了垃圾回收的壓力。
總結:
關於逃逸分析的論文在1999年就已經發表了,但直到JDK 1.6纔有實現,並且這項技術到現在也並非十分紅熟的。
其根本緣由就是沒法保證逃逸分析的性能消耗必定能高於他的消耗。雖然通過逃逸分析能夠作標量替換、棧上分配、和鎖消除。可是逃逸分析自身也是須要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。
一個極端的例子,就是通過逃逸分析以後,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
雖然這項技術並不十分紅熟,可是他也是即時編譯器優化技術中一個十分重要的手段。
String.intern()
方法。受方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryErro
r 異常。你是否也有看不一樣的參考資料,有的內存結構圖有方法區,有的又是永久代,元數據區,一臉懵逼的時候?
-XX:PermSize
和 -xx:MaxPermSize
來設置永久代參數,Java8 以後,隨着永久代的取消,這些參數也就隨之失效了,改成經過-XX:MetaspaceSize
和 -XX:MaxMetaspaceSize
用來設置元空間參數OutOfMemoryError
因此對於方法區,Java8 以後的變化:
JDK8 及之後:
-XX:MetaspaceSize
和 -XX:MaxMetaspaceSize
指定,替代上述原有的兩個參數-XX:MetaspaceSize
是 21M,-XX:MaxMetaspacaSize
的值是 -1,即沒有限制OutOfMemoryError:Metaspace
-XX:MetaspaceSize
:設置初始的元空間大小。對於一個 64 位的服務器端 JVM 來講,其默認的 -XX:MetaspaceSize
的值爲20.75MB,這就是初始的高水位線,一旦觸及這個水位線,Full GC 將會被觸發並卸載沒用的類(即這些類對應的類加載器再也不存活),而後這個高水位線將會重置,新的高水位線的值取決於 GC 後釋放了多少元空間。若是釋放的空間不足,那麼在不超過 MaxMetaspaceSize
時,適當提升該值。若是釋放空間過多,則適當下降該值-XX:MetaspaceSize
設置爲一個相對較高的值。方法區用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等。
對每一個加載的類型(類 class、接口 interface、枚舉 enum、註解 annotation),JVM 必須在方法區中存儲如下類型信息
JVM 必須保存全部方法的
棧、堆、方法區的交互關係
運行時常量池(Runtime Constant Pool)是方法區的一部分,理解運行時常量池的話,咱們先來講說字節碼文件(Class 文件)中的常量池(常量池表)
一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(Constant Pool Table),包含各類字面量和對類型、域和方法的符號引用。
一個 Java 源文件中的類、接口,編譯後產生一個字節碼文件。而 Java 中的字節碼須要數據支持,一般這種數據會很大以致於不能直接存到字節碼裏,換另外一種方式,能夠存到常量池,這個字節碼包含了指向常量池的引用。在動態連接的時候用到的就是運行時常量池。
以下,咱們經過 jclasslib 查看一個只有 Main 方法的簡單類,字節碼中的 #2 指向的就是 Constant Pool
常量池能夠看做是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型。
intern()
方法就是這樣的只有 HotSpot 纔有永久代的概念
jdk1.6及以前 | 有永久代,靜態變量存放在永久代上 |
---|---|
jdk1.7 | 有永久代,但已經逐步「去永久代」,字符串常量池、靜態變量移除,保存在堆中 |
jdk1.8及以後 | 取消永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍在堆中 |
爲永久代設置空間大小是很難肯定的。
在某些場景下,若是動態加載類過多,容易產生 Perm 區的 OOM。若是某個實際 Web 工程中,由於功能點比較多,在運行過程當中,要不斷動態加載不少類,常常出現 OOM。而元空間和永久代最大的區別在於,元空間不在虛擬機中,而是使用本地內存,因此默認狀況下,元空間的大小僅受本地內存限制
對永久代進行調優較困難
方法區的垃圾收集主要回收兩部份內容:常量池中廢棄的常量和再也不使用的類型。
先來講說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近 java 語言層次的常量概念,如文本字符串、被聲明爲 final 的常量值等。而符號引用則屬於編譯原理方面的概念,包括下面三類常量:
HotSpot 虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就能夠被回收
斷定一個類型是否屬於「再也不被使用的類」,須要同時知足三個條件:
Java 虛擬機被容許堆知足上述三個條件的無用類進行回收,這裏說的僅僅是「被容許」,而並非和對象同樣,不使用了就必然會回收。是否對類進行回收,HotSpot 虛擬機提供了 -Xnoclassgc
參數進行控制,還可使用 -verbose:class
以及 -XX:+TraceClassLoading
、-XX:+TraceClassUnLoading
查看類加載和卸載信息。
在大量使用反射、動態代理、CGLib 等 ByteCode 框架、動態生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader 的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。
算是一篇學習筆記,共勉,主要來源:
《深刻理解 Java 虛擬機 第三版》
宋紅康老師的 JVM 教程