寫這篇的主要緣由呢,就是爲了能在簡歷上寫個「熟悉JVM底層結構」,另外一個緣由就是能讓讀我文章的你們也寫上這句話,真是個助人爲樂的帥小夥。。。。嗯,不僅僅只是面向面試學習哈,更重要的是構建本身的 JVM 知識體系,Javaer 們技術棧要有廣度,可是 JVM 的掌握必須有深度html
點贊+收藏 就學會系列,文章收錄在 GitHub JavaKeeper ,N線互聯網開發必備技能兵器譜,筆記自取java
直擊面試
反正我是帶着這些問題往下讀的git
- 說一下 JVM 運行時數據區吧,都有哪些區?分別是幹什麼的?
- Java 8 的內存分代改進
- 舉例棧溢出的狀況?
- 調整棧大小,就能保存不出現溢出嗎?
- 分配的棧內存越大越好嗎?
- 垃圾回收是否會涉及到虛擬機棧?
- 方法中定義的局部變量是否線程安全?
運行時數據區
內存是很是重要的系統資源,是硬盤和 CPU 的中間倉庫及橋樑,承載着操做系統和應用程序的實時運行。JVM 內存佈局規定了 Java 在運行過程當中內存申請、分配、管理的策略,保證了 JVM 的高效穩定運行。不一樣的 JVM 對於內存的劃分方式和管理機制存在着部分差別。github
下圖是 JVM 總體架構,中間部分就是 Java 虛擬機定義的各類運行時數據區域。面試
Java 虛擬機定義了若干種程序運行期間會使用到的運行時數據區,其中有一些會隨着虛擬機啓動而建立,隨着虛擬機退出而銷燬。另一些則是與線程一一對應的,這些與線程一一對應的數據區域會隨着線程開始和結束而建立和銷燬。算法
- 線程私有:程序計數器、棧、本地棧
- 線程共享:堆、堆外內存(永久代或元空間、代碼緩存)
下面咱們就來一一解毒下這些內存區域,先從最簡單的入手編程
1、程序計數器
程序計數寄存器(Program Counter Register),Register 的命名源於 CPU 的寄存器,寄存器存儲指令相關的線程信息,CPU 只有把數據裝載到寄存器纔可以運行。windows
這裏,並不是是廣義上所指的物理寄存器,叫程序計數器(或PC計數器或指令計數器)會更加貼切,而且也不容易引發一些沒必要要的誤會。JVM 中的 PC 寄存器是對物理 PC 寄存器的一種抽象模擬。數組
程序計數器是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。緩存
1.1 做用
PC 寄存器用來存儲指向下一條指令的地址,即將要執行的指令代碼。由執行引擎讀取下一條指令。
(分析:進入class文件所在目錄,執行 javap -v xx.class
反解析(或者經過 IDEA 插件 Jclasslib
直接查看,上圖),能夠看到當前類對應的Code區(彙編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等信息。)
1.2 概述
- 它是一塊很小的內存空間,幾乎能夠忽略不計。也是運行速度最快的存儲區域
- 在 JVM 規範中,每一個線程都有它本身的程序計數器,是線程私有的,生命週期與線程的生命週期一致
- 任什麼時候間一個線程都只有一個方法在執行,也就是所謂的當前方法。若是當前線程正在執行的是 Java 方法,程序計數器記錄的是 JVM 字節碼指令地址,若是是執行 natice 方法,則是未指定值(undefined)
- 它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成
- 字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令
- 它是惟一一個在 JVM 規範中沒有規定任何
OutOfMemoryError
狀況的區域
👨💻:使用PC寄存器存儲字節碼指令地址有什麼用呢?爲何使用PC寄存器記錄當前線程的執行地址呢?
🙋♂️:由於CPU須要不停的切換各個線程,這時候切換回來之後,就得知道接着從哪開始繼續執行。JVM的字節碼解釋器就須要經過改變PC寄存器的值來明確下一條應該執行什麼樣的字節碼指令。
👨💻:PC寄存器爲何會被設定爲線程私有的?
🙋♂️:多線程在一個特定的時間段內只會執行其中某一個線程方法,CPU會不停的作任務切換,這樣必然會致使常常中斷或恢復。爲了可以準確的記錄各個線程正在執行的當前字節碼指令地址,因此爲每一個線程都分配了一個PC寄存器,每一個線程都獨立計算,不會互相影響。
2、虛擬機棧
2.1 概述
Java 虛擬機棧(Java Virtual Machine Stacks),早期也叫 Java 棧。每一個線程在建立的時候都會建立一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次 Java 方法調用,是線程私有的,生命週期和線程一致。
做用:主管 Java 程序的運行,它保存方法的局部變量、部分結果,並參與方法的調用和返回。
特色:
- 棧是一種快速有效的分配存儲方式,訪問速度僅次於程序計數器
- JVM 直接對虛擬機棧的操做只有兩個:每一個方法執行,伴隨着入棧(進棧/壓棧),方法執行結束出棧
- 棧不存在垃圾回收問題
棧中可能出現的異常:
Java 虛擬機規範容許 Java虛擬機棧的大小是動態的或者是固定不變的
- 若是採用固定大小的 Java 虛擬機棧,那每一個線程的 Java 虛擬機棧容量能夠在線程建立的時候獨立選定。若是線程請求分配的棧容量超過 Java 虛擬機棧容許的最大容量,Java 虛擬機將會拋出一個 StackOverflowError 異常
- 若是 Java 虛擬機棧能夠動態擴展,而且在嘗試擴展的時候沒法申請到足夠的內存,或者在建立新的線程時沒有足夠的內存去建立對應的虛擬機棧,那 Java 虛擬機將會拋出一個OutOfMemoryError異常
能夠經過參數-Xss
來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。
官方提供的參考工具,可查一些參數和操做:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html#BGBCIEFC
2.2 棧的存儲單位
棧中存儲什麼?
- 每一個線程都有本身的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在
- 在這個線程上正在執行的每一個方法都各自有對應的一個棧幀
- 棧幀是一個內存區塊,是一個數據集,維繫着方法執行過程當中的各類數據信息
2.3 棧運行原理
-
JVM 直接對 Java 棧的操做只有兩個,對棧幀的壓棧和出棧,遵循「先進後出/後進先出」原則
-
在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱爲當前棧幀(Current Frame),與當前棧幀對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)
-
執行引擎運行的全部字節碼指令只針對當前棧幀進行操做
-
若是在該方法中調用了其餘方法,對應的新的棧幀會被建立出來,放在棧的頂端,稱爲新的當前棧幀
-
不一樣線程中所包含的棧幀是不容許存在相互引用的,即不可能在一個棧幀中引用另一個線程的棧幀
-
若是當前方法調用了其餘方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀從新成爲當前棧幀
-
Java 方法有兩種返回函數的方式,一種是正常的函數返回,使用 return 指令,另外一種是拋出異常,無論用哪一種方式,都會致使棧幀被彈出
IDEA 在 debug 時候,能夠在 debug 窗口看到 Frames 中各類方法的壓棧和出棧狀況
2.4 棧幀的內部結構
每一個棧幀(Stack Frame)中存儲着:
- 局部變量表(Local Variables)
- 操做數棧(Operand Stack)(或稱爲表達式棧)
- 動態連接(Dynamic Linking):指向運行時常量池的方法引用
- 方法返回地址(Return Address):方法正常退出或異常退出的地址
- 一些附加信息
繼續深拋棧幀中的五部分~~
2.4.1. 局部變量表
- 局部變量表也被稱爲局部變量數組或者本地變量表
- 是一組變量值存儲空間,主要用於存儲方法參數和定義在方法體內的局部變量,包括編譯器可知的各類 Java 虛擬機基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它並不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或其餘與此相關的位置)和 returnAddress 類型(指向了一條字節碼指令的地址,已被異常表取代)
- 因爲局部變量表是創建在線程的棧上,是線程的私有數據,所以不存在數據安全問題
- 局部變量表所須要的容量大小是編譯期肯定下來的,並保存在方法的 Code 屬性的
maximum local variables
數據項中。在方法運行期間是不會改變局部變量表的大小的 - 方法嵌套調用的次數由棧的大小決定。通常來講,棧越大,方法嵌套調用次數越多。對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以知足方法調用所需傳遞的信息增大的需求。進而函數調用就會佔用更多的棧空間,致使其嵌套調用次數就會減小。
- 局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機經過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束後,隨着方法棧幀的銷燬,局部變量表也會隨之銷燬。
- 參數值的存放老是在局部變量數組的 index0 開始,到數組長度 -1 的索引結束
槽 Slot
-
局部變量表最基本的存儲單元是 Slot(變量槽)
-
在局部變量表中,32 位之內的類型只佔用一個 Slot(包括returnAddress類型),64 位的類型(long和double)佔用兩個連續的 Slot
- byte、short、char 在存儲前被轉換爲int,boolean也被轉換爲int,0 表示 false,非 0 表示 true
- 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 的槽)
- 在棧幀中,與性能調優關係最爲密切的就是局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞
- 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收
2.4.2. 操做數棧
-
每一個獨立的棧幀中除了包含局部變量表以外,還包含一個後進先出(Last-In-First-Out)的操做數棧,也能夠稱爲表達式棧(Expression Stack)
-
操做數棧,在方法執行過程當中,根據字節碼指令,往操做數棧中寫入數據或提取數據,即入棧(push)、出棧(pop)
-
某些字節碼指令將值壓入操做數棧,其他的字節碼指令將操做數取出棧。使用它們後再把結果壓入棧。好比,執行復制、交換、求和等操做
概述
- <mark>操做數棧,主要用於保存計算過程的中間結果,同時做爲計算過程當中變量臨時的存儲空間</mark>
- 操做數棧就是 JVM 執行引擎的一個工做區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被建立出來,此時這個方法的操做數棧是空的
- 每個操做數棧都會擁有一個明確的棧深度用於存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的 Code 屬性的
max_stack
數據項中 - 棧中的任何一個元素均可以是任意的 Java 數據類型
- 32bit 的類型佔用一個棧單位深度
- 64bit 的類型佔用兩個棧單位深度
- 操做數棧並不是採用訪問索引的方式來進行數據訪問的,而是隻能經過標準的入棧和出棧操做來完成一次數據訪問
- 若是被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操做數棧中,並更新PC寄存器中下一條須要執行的字節碼指令
- 操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載過程當中的類檢驗階段的數據流分析階段要再次驗證
- 另外,咱們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操做數棧
棧頂緩存(Top-of-stack-Cashing)
HotSpot 的執行引擎採用的並不是是基於寄存器的架構,但這並不表明 HotSpot VM 的實現並無間接利用到寄存器資源。寄存器是物理 CPU 中的組成部分之一,它同時也是 CPU 中很是重要的高速存儲資源。通常來講,寄存器的讀/寫速度很是迅速,甚至能夠比內存的讀/寫速度快上幾十倍不止,不過寄存器資源卻很是有限,不一樣平臺下的CPU 寄存器數量是不一樣和不規律的。寄存器主要用於緩存本地機器指令、數值和下一條須要被執行的指令地址等數據。
基於棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操做的時候必然須要使用更多的入棧和出棧指令,這同時也就意味着將須要更多的指令分派(instruction dispatch)次數和內存讀/寫次數。因爲操做數是存儲在內存中的,所以頻繁的執行內存讀/寫操做必然會影響執行速度。爲了解決這個問題,HotSpot JVM 設計者們提出了棧頂緩存技術,將棧頂元素所有緩存在物理 CPU 的寄存器中,以此下降對內存的讀/寫次數,提高執行引擎的執行效率
2.4.3. 動態連接(指向運行時常量池的方法引用)
- 每個棧幀內部都包含一個指向運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是爲了支持當前方法的代碼可以實現動態連接(Dynamic Linking)。
- 在 Java 源文件被編譯到字節碼文件中時,全部的變量和方法引用都做爲<mark>符號引用</mark>(Symbolic Reference)保存在 Class 文件的常量池中。好比:描述一個方法調用了另外的其餘方法時,就是經過常量池中指向方法的符號引用來表示的,那麼動態連接的做用就是爲了將這些符號引用轉換爲調用方法的直接引用
JVM 是如何執行方法調用的
方法調用不一樣於方法執行,方法調用階段的惟一任務就是肯定被調用方法的版本(即調用哪個方法),暫時還不涉及方法內部的具體運行過程。Class 文件的編譯過程當中不包括傳統編譯器中的鏈接步驟,一切方法調用在 Class文件裏面存儲的都是符號引用,而不是方法在實際運行時內存佈局中的入口地址(直接引用)。也就是須要在類加載階段,甚至到運行期才能肯定目標方法的直接引用。
【這一塊內容,除了方法調用,還包括解析、分派(靜態分派、動態分派、單分派與多分派),這裏先不介紹,後續再挖】
在 JVM 中,將符號引用轉換爲調用方法的直接引用與方法的綁定機制有關
- 靜態連接:當一個字節碼文件被裝載進 JVM 內部時,若是被調用的目標方法在編譯期可知,且運行期保持不變時。這種狀況下將調用方法的符號引用轉換爲直接引用的過程稱之爲靜態連接
- 動態連接:若是被調用的方法在編譯期沒法被肯定下來,也就是說,只能在程序運行期將調用方法的符號引用轉換爲直接引用,因爲這種引用轉換過程具有動態性,所以也就被稱之爲動態連接
對應的方法的綁定機制爲:早期綁定(Early Binding)和晚期綁定(Late Binding)。綁定是一個字段、方法或者類在符號引用被替換爲直接引用的過程,這僅僅發生一次。
- 早期綁定:早期綁定就是指被調用的目標方法若是在編譯期可知,且運行期保持不變時,便可將這個方法與所屬的類型進行綁定,這樣一來,因爲明確了被調用的目標方法到底是哪個,所以也就可使用靜態連接的方式將符號引用轉換爲直接引用。
- 晚期綁定:若是被調用的方法在編譯器沒法被肯定下來,只可以在程序運行期根據實際的類型綁定相關的方法,這種綁定方式就被稱爲晚期綁定。
虛方法和非虛方法
- 若是方法在編譯器就肯定了具體的調用版本,這個版本在運行時是不可變的。這樣的方法稱爲非虛方法,好比靜態方法、私有方法、final 方法、實例構造器、父類方法都是非虛方法
- 其餘方法稱爲虛方法
虛方法表
在面向對象編程中,會頻繁的使用到動態分派,若是每次動態分派都要從新在類的方法元數據中搜索合適的目標有可能會影響到執行效率。爲了提升性能,JVM 採用在類的方法區創建一個虛方法表(virtual method table),使用索引表來代替查找。非虛方法不會出如今表中。
每一個類中都有一個虛方法表,表中存放着各個方法的實際入口。
虛方法表會在類加載的鏈接階段被建立並開始初始化,類的變量初始值準備完成以後,JVM 會把該類的方法表也初始化完畢。
2.4.4. 方法返回地址(return address)
用來存放調用該方法的 PC 寄存器的值。
一個方法的結束,有兩種方式
- 正常執行完成
- 出現未處理的異常,非正常退出
不管經過哪一種方式退出,在方法退出後都返回到該方法被調用的位置。方法正常退出時,調用者的 PC 計數器的值做爲返回地址,即調用該方法的指令的下一條指令的地址。而經過異常退出的,返回地址是要經過異常表來肯定的,棧幀中通常不會保存這部分信息。
當一個方法開始執行後,只有兩種方式能夠退出這個方法:
-
執行引擎遇到任意一個方法返回的字節碼指令,會有返回值傳遞給上層的方法調用者,簡稱正常完成出口
一個方法的正常調用完成以後究竟須要使用哪個返回指令還須要根據方法返回值的實際數據類型而定
在字節碼指令中,返回指令包含 ireturn(當返回值是 boolean、byte、char、short 和 int 類型時使用)、lreturn、freturn、dreturn 以及 areturn,另外還有一個 return 指令供聲明爲 void 的方法、實例初始化方法、類和接口的初始化方法使用。
-
在方法執行的過程當中遇到了異常,而且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出。簡稱異常完成出口
方法執行過程當中拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找處處理異常的代碼。
本質上,方法的退出就是當前棧幀出棧的過程。此時,須要恢復上層方法的局部變量表、操做數棧、將返回值壓入調用者棧幀的操做數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
正常完成出口和異常完成出口的區別在於:經過異常完成出口退出的不會給他的上層調用者產生任何的返回值
2.4.5. 附加信息
棧幀中還容許攜帶與 Java 虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息,但這些信息取決於具體的虛擬機實現。
3、本地方法棧
3.1 本地方法接口
簡單的講,一個 Native Method 就是一個 Java 調用非 Java 代碼的接口。咱們知道的 Unsafe 類就有不少本地方法。
爲何要使用本地方法(Native Method)?
Java 使用起來很是方便,然而有些層次的任務用 Java 實現起來也不容易,或者咱們對程序的效率很在乎時,問題就來了
- 與 Java 環境外交互:有時 Java 應用須要與 Java 外面的環境交互,這就是本地方法存在的緣由。
- 與操做系統交互:JVM 支持 Java 語言自己和運行時庫,可是有時仍須要依賴一些底層系統的支持。經過本地方法,咱們能夠實現用 Java 與實現了 jre 的底層系統交互, JVM 的一些部分就是 C 語言寫的。
- Sun's Java:Sun的解釋器就是C實現的,這使得它能像一些普通的C同樣與外部交互。jre大部分都是用 Java 實現的,它也經過一些本地方法與外界交互。好比,類
java.lang.Thread
的setPriority()
的方法是用Java 實現的,但它實現調用的是該類的本地方法setPrioruty()
,該方法是C實現的,並被植入 JVM 內部。
3.2 本地方法棧(Native Method Stack)
-
Java 虛擬機棧用於管理 Java 方法的調用,而本地方法棧用於管理本地方法的調用
-
本地方法棧也是線程私有的
-
容許線程固定或者可動態擴展的內存大小
- 若是線程請求分配的棧容量超過本地方法棧容許的最大容量,Java 虛擬機將會拋出一個
StackOverflowError
異常 - 若是本地方法棧能夠動態擴展,而且在嘗試擴展的時候沒法申請到足夠的內存,或者在建立新的線程時沒有足夠的內存去建立對應的本地方法棧,那麼 Java虛擬機將會拋出一個
OutofMemoryError
異常
- 若是線程請求分配的棧容量超過本地方法棧容許的最大容量,Java 虛擬機將會拋出一個
-
本地方法是使用 C 語言實現的
-
它的具體作法是
Mative Method Stack
中登記 native 方法,在Execution Engine
執行時加載本地方法庫當某個線程調用一個本地方法時,它就進入了一個全新的而且再也不受虛擬機限制的世界。它和虛擬機擁有一樣的權限。 -
本地方法能夠經過本地方法接口來訪問虛擬機內部的運行時數據區,它甚至能夠直接使用本地處理器中的寄存器,直接從本地內存的堆中分配任意數量的內存
-
並非全部 JVM 都支持本地方法。由於 Java 虛擬機規範並無明確要求本地方法棧的使用語言、具體實現方式、數據結構等。若是 JVM 產品不打算支持 native 方法,也能夠無需實現本地方法棧
-
<mark>在 Hotspot JVM 中,直接將本地方棧和虛擬機棧合二爲一</mark>
棧是運行時的單位,而堆是存儲的單位。
棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎麼放、放在哪。
4、堆內存
4.1 內存劃分
對於大多數應用,Java 堆是 Java 虛擬機管理的內存中最大的一塊,被全部線程共享。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例以及數據都在這裏分配內存。
爲了進行高效的垃圾回收,虛擬機把堆內存邏輯上劃分紅三塊區域(<mark>分代的惟一理由就是優化 GC 性能</mark>):
- 新生帶(年輕代):新對象和沒達到必定年齡的對象都在新生代
- 老年代(養老區):被長時間使用的對象,老年代的內存空間應該要比年輕代更大
- 元空間(JDK1.8 以前叫永久代):像一些方法中的操做臨時對象等,JDK1.8 以前是佔用 JVM 內存,JDK1.8 以後直接使用物理內存
Java 虛擬機規範規定,Java 堆能夠是處於物理上不連續的內存空間中,只要邏輯上是連續的便可,像磁盤空間同樣。實現時,既能夠是固定大小,也能夠是可擴展的,主流虛擬機都是可擴展的(經過 -Xmx
和 -Xms
控制),若是堆中沒有完成實例分配,而且堆沒法再擴展時,就會拋出 OutOfMemoryError
異常。
年輕代 (Young Generation)
年輕代是全部新對象建立的地方。當填充年輕代時,執行垃圾收集。這種垃圾收集稱爲 Minor GC。年輕一代被分爲三個部分——伊甸園(Eden Memory)和兩個倖存區(Survivor Memory,被稱爲from/to或s0/s1),默認比例是8:1:1
- 大多數新建立的對象都位於 Eden 內存空間中
- 當 Eden 空間被對象填充時,執行Minor GC,並將全部倖存者對象移動到一個倖存者空間中
- Minor GC 檢查倖存者對象,並將它們移動到另外一個倖存者空間。因此每次,一個倖存者空間老是空的
- 通過屢次 GC 循環後存活下來的對象被移動到老年代。一般,這是經過設置年輕一代對象的年齡閾值來實現的,而後他們纔有資格提高到老一代
老年代(Old Generation)
舊的一代內存包含那些通過許多輪小型 GC 後仍然存活的對象。一般,垃圾收集是在老年代內存滿時執行的。老年代垃圾收集稱爲 主GC(Major GC),一般須要更長的時間。
大對象直接進入老年代(大對象是指須要大量連續內存空間的對象)。這樣作的目的是避免在 Eden 區和兩個Survivor 區之間發生大量的內存拷貝
元空間
無論是 JDK8 以前的永久代,仍是 JDK8 及之後的元空間,均可以看做是 Java 虛擬機規範中方法區的實現。
雖然 Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫 Non-Heap(非堆),目的應該是與 Java 堆區分開。
因此元空間放在後邊的方法區再說。
4.2 設置堆內存大小和 OOM
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 堆內存大小的狀況下,JVM 根據默認值來配置當前內存大小
-
默認狀況下新生代和老年代的比例是 1:2,能夠經過
–XX:NewRatio
來配置- 新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1,能夠經過
-XX:SurvivorRatio
來配置
- 新生代中的 Eden:From Survivor:To Survivor 的比例是 8:1:1,能夠經過
-
若在 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 進程號
4.3 對象在堆中的生命週期
- 在 JVM 內存模型的堆中,堆被劃分爲新生代和老年代
- 新生代又被進一步劃分爲 Eden區 和 Survivor區,Survivor 區由 From Survivor 和 To Survivor 組成
- 當建立一個對象時,對象會被優先分配到新生代的 Eden 區
- 此時 JVM 會給對象定義一個對象年輕計數器(
-XX:MaxTenuringThreshold
)
- 此時 JVM 會給對象定義一個對象年輕計數器(
- 當 Eden 空間不足時,JVM 將執行新生代的垃圾回收(Minor GC)
- JVM 會把存活的對象轉移到 Survivor 中,而且對象年齡 +1
- 對象在 Survivor 中一樣也會經歷 Minor GC,每經歷一次 Minor GC,對象年齡都會+1
- 若是分配的對象超過了
-XX:PetenureSizeThreshold
,對象會直接被分配到老年代
4.4 對象的分配過程
爲對象分配內存是一件很是嚴謹和複雜的任務,JVM 的設計者們不只須要考慮內存如何分配、在哪裏分配等問題,而且因爲內存分配算法和內存回收算法密切相關,因此還須要考慮 GC 執行完內存回收後是否會在內存空間中產生內存碎片。
- new 的對象先放在伊甸園區,此區有大小限制
- 當伊甸園的空間填滿時,程序又須要建立對象,JVM 的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的再也不被其餘對象所引用的對象進行銷燬。再加載新的對象放到伊甸園區
- 而後將伊甸園中的剩餘對象移動到倖存者 0 區
- 若是再次觸發垃圾回收,此時上次倖存下來的放到倖存者 0 區,若是沒有回收,就會放到倖存者 1 區
- 若是再次經歷垃圾回收,此時會從新放回倖存者 0 區,接着再去倖存者 1 區
- 何時纔會去養老區呢? 默認是 15 次回收標記
- 在養老區,相對清閒。當養老區內存不足時,再次觸發 Major GC,進行養老區的內存清理
- 若養老區執行了 Major GC 以後發現依然沒法進行對象的保存,就會產生 OOM 異常
4.5 GC 垃圾回收簡介
Minor GC、Major GC、Full GC
JVM 在進行 GC 時,並不是每次都對堆內存(新生代、老年代;方法區)區域一塊兒回收的,大部分時候回收的都是指新生代。
針對 HotSpot VM 的實現,它裏面的 GC 按照回收區域又分爲兩大類:部分收集(Partial GC),整堆收集(Full GC)
- 部分收集:不是完整收集整個 Java 堆的垃圾收集。其中又分爲:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 目前,只有 CMS GC 會有單獨收集老年代的行爲
- 不少時候 Major GC 會和 Full GC 混合使用,須要具體分辨是老年代回收仍是整堆回收
- 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集
- 目前只有 G1 GC 會有這種行爲
- 整堆收集(Full GC):收集整個 Java 堆和方法區的垃圾
4.6 TLAB
什麼是 TLAB (Thread Local Allocation Buffer)?
- 從內存模型而不是垃圾回收的角度,對 Eden 區域繼續進行劃分,JVM 爲每一個線程分配了一個私有緩存區域,它包含在 Eden 空間內
- 多線程同時分配內存時,使用 TLAB 能夠避免一系列的非線程安全問題,同時還能提高內存分配的吞吐量,所以咱們能夠將這種內存分配方式稱爲快速分配策略
- OpenJDK 衍生出來的 JVM 大都提供了 TLAB 設計
爲何要有 TLAB ?
- 堆區是線程共享的,任何線程均可以訪問到堆區中的共享數據
- 因爲對象實例的建立在 JVM 中很是頻繁,所以在併發環境下從堆區中劃份內存空間是線程不安全的
- 爲避免多個線程操做同一地址,須要使用加鎖等機制,進而影響分配速度
儘管不是全部的對象實例都可以在 TLAB 中成功分配內存,但 JVM 確實是將 TLAB 做爲內存分配的首選。
在程序中,能夠經過 -XX:UseTLAB
設置是否開啓 TLAB 空間。
默認狀況下,TLAB 空間的內存很是小,僅佔有整個 Eden 空間的 1%,咱們能夠經過 -XX:TLABWasteTargetPercent
設置 TLAB 空間所佔用 Eden 空間的百分比大小。
一旦對象在 TLAB 空間分配內存失敗時,JVM 就會嘗試着經過使用加鎖機制確保數據操做的原子性,從而直接在 Eden 空間中分配內存。
4.7 堆是分配對象存儲的惟一選擇嗎
隨着 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 將不會逃逸出方法。
參數設置:
- 在 JDK 6u23 版本以後,HotSpot 中默認就已經開啓了逃逸分析
- 若是使用較早版本,能夠經過
-XX"+DoEscapeAnalysis
顯式開啓
開發中使用局部變量,就不要在方法外定義。
使用逃逸分析,編譯器能夠對代碼作優化:
- 棧上分配:將堆分配轉化爲棧分配。若是一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象多是棧分配的候選,而不是堆分配
- 同步省略:若是一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操做能夠不考慮同步
- 分離對象或標量替換:有的對象可能不須要做爲一個連續的內存結構存在也能夠被訪問到,那麼對象的部分(或所有)能夠不存儲在內存,而存儲在 CPU 寄存器
JIT 編譯器在編譯期間根據逃逸分析的結果,發現若是一個對象並無逃逸出方法的話,就可能被優化成棧上分配。分配完成後,繼續在調用棧內執行,最後線程結束,棧空間被回收,局部變量對象也被回收。這樣就無需進行垃圾回收了。
常見棧上分配的場景:成員變量賦值、方法返回值、實例引用傳遞
代碼優化之同步省略(消除)
- 線程同步的代價是至關高的,同步的後果是下降併發性和性能
- 在動態編譯同步塊的時候,JIT 編譯器能夠藉助逃逸分析來判斷同步塊所使用的鎖對象是否可以被一個線程訪問而沒有被髮布到其餘線程。若是沒有,那麼 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纔有實現,並且這項技術到現在也並非十分紅熟的。
其根本緣由就是沒法保證逃逸分析的性能消耗必定能高於他的消耗。雖然通過逃逸分析能夠作標量替換、棧上分配、和鎖消除。可是逃逸分析自身也是須要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。
一個極端的例子,就是通過逃逸分析以後,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
雖然這項技術並不十分紅熟,可是他也是即時編譯器優化技術中一個十分重要的手段。
5、方法區
- 方法區(Method Area)與 Java 堆同樣,是全部線程共享的內存區域。
- 雖然 Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫 Non-Heap(非堆),目的應該是與 Java 堆區分開。
- 運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本/字段/方法/接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將類在加載後進入方法區的運行時常量池中存放。運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的是
String.intern()
方法。受方法區內存的限制,當常量池沒法再申請到內存時會拋出OutOfMemoryErro
r 異常。 - 方法區的大小和堆空間同樣,能夠選擇固定大小也可選擇可擴展,方法區的大小決定了系統能夠放多少個類,若是系統類太多,致使方法區溢出,虛擬機一樣會拋出內存溢出錯誤
- JVM 關閉後方法區即被釋放
5.1 解惑
你是否也有看不一樣的參考資料,有的內存結構圖有方法區,有的又是永久代,元數據區,一臉懵逼的時候?
- 方法區(method area)只是 JVM 規範中定義的一個概念,用於存儲類信息、常量池、靜態變量、JIT編譯後的代碼等數據,並無規定如何去實現它,不一樣的廠商有不一樣的實現。而永久代(PermGen)是 Hotspot 虛擬機特有的概念, Java8 的時候又被元空間取代了,永久代和元空間均可以理解爲方法區的落地實現。
- 永久代物理是堆的一部分,和新生代,老年代地址是連續的(受垃圾回收器管理),而元空間存在於本地內存(咱們常說的堆外內存,不受垃圾回收器管理),這樣就不受 JVM 限制了,也比較難發生OOM(都會有溢出異常)
- Java7 中咱們經過
-XX:PermSize
和-xx:MaxPermSize
來設置永久代參數,Java8 以後,隨着永久代的取消,這些參數也就隨之失效了,改成經過-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
用來設置元空間參數 - 存儲內容不一樣,元空間存儲類的元信息,靜態變量和常量池等併入堆中。至關於永久代的數據被分到了堆和元空間中
- 若是方法區域中的內存不能用於知足分配請求,則 Java 虛擬機拋出
OutOfMemoryError
- JVM 規範說方法區在邏輯上是堆的一部分,但目前其實是與 Java 堆分開的(Non-Heap)
因此對於方法區,Java8 以後的變化:
- 移除了永久代(PermGen),替換爲元空間(Metaspace);
- 永久代中的 class metadata 轉移到了 native memory(本地內存,而不是虛擬機);
- 永久代中的 interned Strings 和 class static variables 轉移到了 Java heap;
- 永久代參數 (PermSize MaxPermSize) -> 元空間參數(MetaspaceSize MaxMetaspaceSize)
5.2 設置方法區內存的大小
JDK8 及之後:
- 元數據區大小可使用參數
-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
指定,替代上述原有的兩個參數 - 默認值依賴於平臺。Windows 下,
-XX:MetaspaceSize
是 21M,-XX:MaxMetaspacaSize
的值是 -1,即沒有限制 - 與永久代不一樣,若是不指定大小,默認狀況下,虛擬機會耗盡全部的可用系統內存。若是元數據發生溢出,虛擬機同樣會拋出異常
OutOfMemoryError:Metaspace
-XX:MetaspaceSize
:設置初始的元空間大小。對於一個 64 位的服務器端 JVM 來講,其默認的-XX:MetaspaceSize
的值爲20.75MB,這就是初始的高水位線,一旦觸及這個水位線,Full GC 將會被觸發並卸載沒用的類(即這些類對應的類加載器再也不存活),而後這個高水位線將會重置,新的高水位線的值取決於 GC 後釋放了多少元空間。若是釋放的空間不足,那麼在不超過MaxMetaspaceSize
時,適當提升該值。若是釋放空間過多,則適當下降該值- 若是初始化的高水位線設置太低,上述高水位線調整狀況會發生不少次,經過垃圾回收的日誌可觀察到 Full GC 屢次調用。爲了不頻繁 GC,建議將
-XX:MetaspaceSize
設置爲一個相對較高的值。
5.3 方法區內部結構
<mark>方法區用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等。</mark>
類型信息
對每一個加載的類型(類 class、接口 interface、枚舉 enum、註解 annotation),JVM 必須在方法區中存儲如下類型信息
- 這個類型的完整有效名稱(全名=包名.類名)
- 這個類型直接父類的完整有效名(對於 interface或是 java.lang.Object,都沒有父類)
- 這個類型的修飾符(public,abstract,final 的某個子集)
- 這個類型直接接口的一個有序列表
域(Field)信息
- JVM 必須在方法區中保存類型的全部域的相關信息以及域的聲明順序
- 域的相關信息包括:域名稱、域類型、域修飾符(public、private、protected、static、final、volatile、transient 的某個子集)
方法(Method)信息
JVM 必須保存全部方法的
- 方法名稱
- 方法的返回類型
- 方法參數的數量和類型
- 方法的修飾符(public,private,protected,static,final,synchronized,native,abstract 的一個子集)
- 方法的字符碼(bytecodes)、操做數棧、局部變量表及大小(abstract 和 native 方法除外)
- 異常表(abstract 和 native 方法除外)
- 每一個異常處理的開始位置、結束位置、代碼處理在程序計數器中的偏移地址、被捕獲的異常類的常量池索引
棧、堆、方法區的交互關係
5.4 運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分,理解運行時常量池的話,咱們先來講說字節碼文件(Class 文件)中的常量池(常量池表)
常量池
一個有效的字節碼文件中除了包含類的版本信息、字段、方法以及接口等描述信息外,還包含一項信息那就是常量池表(Constant Pool Table),包含各類字面量和對類型、域和方法的符號引用。
爲何須要常量池?
一個 Java 源文件中的類、接口,編譯後產生一個字節碼文件。而 Java 中的字節碼須要數據支持,一般這種數據會很大以致於不能直接存到字節碼裏,換另外一種方式,能夠存到常量池,這個字節碼包含了指向常量池的引用。在動態連接的時候用到的就是運行時常量池。
以下,咱們經過 jclasslib 查看一個只有 Main 方法的簡單類,字節碼中的 #2 指向的就是 Constant Pool
常量池能夠看做是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量等類型。
運行時常量池
- 在加載類和結構到虛擬機後,就會建立對應的運行時常量池
- 常量池表(Constant Pool Table)是 Class 文件的一部分,用於存儲編譯期生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中
- JVM 爲每一個已加載的類型(類或接口)都維護一個常量池。池中的數據項像數組項同樣,是經過索引訪問的
- 運行時常量池中包含各類不一樣的常量,包括編譯器就已經明確的數值字面量,也包括到運行期解析後纔可以得到的方法或字段引用。此時再也不是常量池中的符號地址了,這裏換爲真實地址
- 運行時常量池,相對於 Class 文件常量池的另外一個重要特徵是:動態性,Java 語言並不要求常量必定只有編譯期間才能產生,運行期間也能夠將新的常量放入池中,String 類的
intern()
方法就是這樣的
- 運行時常量池,相對於 Class 文件常量池的另外一個重要特徵是:動態性,Java 語言並不要求常量必定只有編譯期間才能產生,運行期間也能夠將新的常量放入池中,String 類的
- 當建立類或接口的運行時常量池時,若是構造運行時常量池所需的內存空間超過了方法區所能提供的最大值,則 JVM 會拋出 OutOfMemoryError 異常。
5.5 方法區在 JDK六、七、8中的演進細節
只有 HotSpot 纔有永久代的概念
jdk1.6及以前 | 有永久代,靜態變量存放在永久代上 |
---|---|
jdk1.7 | 有永久代,但已經逐步「去永久代」,字符串常量池、靜態變量移除,保存在堆中 |
jdk1.8及以後 | 取消永久代,類型信息、字段、方法、常量保存在本地內存的元空間,但字符串常量池、靜態變量仍在堆中 |
移除永久代緣由
http://openjdk.java.net/jeps/122
-
爲永久代設置空間大小是很難肯定的。
在某些場景下,若是動態加載類過多,容易產生 Perm 區的 OOM。若是某個實際 Web 工程中,由於功能點比較多,在運行過程當中,要不斷動態加載不少類,常常出現 OOM。而元空間和永久代最大的區別在於,元空間不在虛擬機中,而是使用本地內存,因此默認狀況下,元空間的大小僅受本地內存限制
-
對永久代進行調優較困難
5.6 方法區的垃圾回收
方法區的垃圾收集主要回收兩部份內容:常量池中廢棄的常量和再也不使用的類型。
先來講說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近 java 語言層次的常量概念,如文本字符串、被聲明爲 final 的常量值等。而符號引用則屬於編譯原理方面的概念,包括下面三類常量:
- 類和接口的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符
HotSpot 虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就能夠被回收
斷定一個類型是否屬於「再也不被使用的類」,須要同時知足三個條件:
- 該類全部的實例都已經被回收,也就是 Java 堆中不存在該類及其任何派生子類的實例
- 加載該類的類加載器已經被回收,這個條件除非是通過精心設計的可替換類加載器的場景,如 OSGi、JSP 的重加載等,不然一般很難達成
- 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法
Java 虛擬機被容許堆知足上述三個條件的無用類進行回收,這裏說的僅僅是「被容許」,而並非和對象同樣,不使用了就必然會回收。是否對類進行回收,HotSpot 虛擬機提供了 -Xnoclassgc
參數進行控制,還可使用 -verbose:class
以及 -XX:+TraceClassLoading
、-XX:+TraceClassUnLoading
查看類加載和卸載信息。
在大量使用反射、動態代理、CGLib 等 ByteCode 框架、動態生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader 的場景都須要虛擬機具有類卸載的功能,以保證永久代不會溢出。
參考與感謝
算是一篇學習筆記,共勉,主要來源:
《深刻理解 Java 虛擬機 第三版》
宋紅康老師的 JVM 教程
https://docs.oracle.com/javase/specs/index.html