執行時資料區內部結構
執行時資料區內部結構
Java,虛擬機器定義了若干種程式執行期間會使用到的執行時資料區,其中有一些會隨著虛擬機器啟動而建立(方法區和堆),隨著虛擬機器退出而銷燬。另外一些則是與執行緒一 一對應的,這些與執行緒對應的資料區域會隨著執行緒開始和結束而建立和銷燬。
- 一個JVM例項就對應一個唯一的Runtime例項
程式計數器
JVM中的程式計數暫存器(Program Counter Register) 中,Register 的命名源於CPU的暫存器,暫存器儲存指令相關的現場資訊。CPU只有把資料裝載到暫存器才能夠執行。
這裡,並非是廣義上所指的物理暫存器,或許將其翻譯為PC計數器(或指令計數器)會更加貼切(也稱為程式鉤子),並且也不容易引起一些不必要的誤會。JVM中的PC暫存器是對物理PC暫存器的一-種抽象模擬。
img/
PC暫存器用來儲存指向下一條指令的地址,也即將要執行的指令程式碼。由執行引擎讀取下一條指令。
- 它是一塊很小的記憶體空間,幾乎可以忽略不記。也是執行速度最快的儲存區域。
- 在JVM規範中,每個執行緒都有它自己的程式計數器,是執行緒私有的,生命週期與執行緒的生命週期保持--致。
- 任何時間一個執行緒都只有一個方法在執行,也就是所謂的當前方法。程式計數器會儲存當前執行緒正在執行的Java方法JVM指令地址;或者,如果是在執行native方法,則是未指定值(undefned) 。
- 它是程式控制流的指示器,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。
- 位元組碼直譯器工作時就是通過改變這個計數器的值來選取下--條需要執行的位元組碼指令。
- 它是唯一 一個在Java虛擬機器規範中沒有規定任何OutOtMemoryError情況的區域。
使用PC暫存器儲存位元組碼指令地址有什麼用呢?為什麼使用PC暫存器記錄當前執行緒的執行地址?
因為CPU需要不停的切換各個執行緒,這時候切換回來以後,就得知道接著從哪開始繼續執行。
JVM的位元組碼直譯器就需要通過改變PC暫存器的值來明確下一條應該執行什麼樣的位元組碼指令。
虛擬機器棧
-
Java虛擬機器棧是什麼?
Java虛擬機器棧(Java Virtual Machine Stack) ,早期也叫Java棧。每個執行緒在建立時都會建立-一個虛擬機器棧,其內部保一個個的棧幀(Stack Frame) ,對應著一次次的Java方法呼叫。
- 是執行緒私有的
-
生命週期
和執行緒保持一致
-
作用
主管Java程式的執行,它儲存方法的區域性變數(8種基本資料型別、物件的引用地址)、部分結果,並參與方法的呼叫和返回。
-
特點(優點)
- 棧是一種快速有效的分配儲存方式,訪問速度僅次於程式計數器。
- JVM直接對Java棧的操作只有兩個:
➢每個方法執行,伴隨著進棧(入棧、壓棧)
➢執行結束後的出棧工作. - 對於棧來說不存在垃圾回收問題。
如何設定棧大小
我們可以使用引數-Xss選項來設定執行緒的最大棧空間,棧的大小直接決定了函式呼叫的最大可達深度。
棧的儲存結構和執行原理
棧中儲存什麼?
- 每個執行緒都有自己的棧,棧中的資料都是以棧幀(stack Frame)的格式存在。
- 在這個執行緒上正在執行的每個方法都各自對應一個棧幀(stack Frame )。
- 棧幀是一個記憶體區塊,是一個數據集,維繫著方法執行過程中的各種資料資訊。
棧執行原理
- JVM直接對Java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循“先進後出”/“後進先出”原則。
- 在一條活動執行緒中,一一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀(Current Frame) ,與當前棧幀相對應的方法就是當前方法(CurrentMethod) ,定義這個方法的類就是當前類(Current Class)。
- 執行引擎執行的所有位元組碼指令只針對當前棧幀進行操作。
- 如果在該方法中呼叫了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前幀。
- 不同執行緒中所包含的棧幀是不允許存在相互引用的,即不可能在-一個棧幀之中引用另外一個執行緒的棧幀。
- 如果當前方法呼叫了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接著,虛擬機器會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀。
- Java方法有兩種返回函式的方式,一種是正常的函式返回,使用return指令;另外- -種是丟擲異常。不管使用哪種方式,都會導致棧幀被彈出。
棧幀的內部結構
-
區域性變量表(Local variables)
- 區域性變量表也被稱之為區域性變數陣列或本地變量表
- 區域性變量表中的變數也是重要的垃圾回收根節點,只要被區域性變量表中直接或間接引用的物件都不會被回收。
- 定義為一個數字陣列,主要用於儲存方法引數和定義在方法體內的區域性變數,這些資料型別包括各類基本資料型別、物件引用(reference) ,以及returnAddress型別。
- 引數值的存放總是在區域性變數陣列的index0開始,到陣列長度-1的索引結束。
- 區域性變量表,最基本的儲存單元是Slot (變數槽)
- 區域性變量表中存放編譯期可知的各種基本資料型別(8種),引用型別(reference),returnAddress型別的變數。
- 在區域性變量表裡,32位以內的型別只佔用一個slot (包括returnAddress型別),64位的型別(long和double)佔用兩個slot。
➢byte 、short 、char在儲存前被轉換為int,boolean也被轉換為int,0表示false ,非0表示true。
➢long和double則佔據兩個Slot。 - JVM會為區域性變量表中的每一個S1ot都分配一一個訪問索引,通過這個索引即可成功訪問到區域性變量表中指定的區域性變數值
- 當一個例項方法被呼叫的時候,它的方法引數和方法體內部定義的區域性變數將會按照順序被複制到區域性變量表中的每一個slot上
- 如果需要訪問區域性變量表中一個64bit的區域性變數值時,只需要使用前-一個索引即可。(比如:訪問long或double型別變數)
- 如果當前幀是由構造方法或者例項方法建立的,那麼該物件引用this將會存放在index為0的slot處,其餘的引數按照引數表順序繼續排列。
- 棧幀中的區域性變量表中的槽位是可以重用的,如果一個區域性變數過了其作用域(使用程式碼塊或for迴圈等),那麼在其作用域之後申明的新的區域性變數就很有可能會複用過期區域性變數的槽位,從而達到節省資源的目的。
- 由於區域性變量表是建立線上程的棧上,是執行緒的私有資料,因此不存在資料安全問題
- 區域性變量表所需的容量大小是在編譯期確定下來的,並儲存在方法的Code屬性的maximum local variables資料項中。 在方法執行期間是不會改變區域性變量表的大小的。
-
運算元棧(operand Stack) (或表示式棧)
-
在方法執行過程中,根據位元組碼指令,往棧中寫入資料或提取資料,即入棧(push) /出棧(pop)。
➢某些位元組碼指令將值壓入運算元棧,其餘的位元組碼指令將運算元取出棧。使用它們後再把結果壓入棧。
➢比如:執行復制、交換、求和等操作 -
運算元棧,主要用於儲存計算過程的中間結果,同時作為計算過程中變數臨時的儲存空間。
-
運算元棧就是JVM執行引擎的一一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的運算元棧是空的。
-
每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯期就定義好了,儲存在方法的Code屬性中,為max_ stack的值。
-
棧中的任何一個元素都是可以任意的Java資料型別
➢32bit的型別佔用一個棧單位深度
➢64bit的型別佔用兩個棧單位深度 -
運算元棧並非採用訪問索引的方式來進行資料訪問的,而是隻能通過標準的入棧(push) 和出棧(pop)操作來完成一次資料訪問。
-
如果被呼叫的方法帶有返回值的話,其返回值將會被壓入當前棧幀的運算元棧中,並更新PC暫存器中下一條需要執行的位元組碼指令。
-
運算元棧中元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類載入過程中的類檢驗階段的資料流分析階段要再次驗證。
-
另外,我們說Java虛擬機器的解釋引擎是基於棧的執行引擎,其中的棧指的就是運算元棧。
-
-
動態連結(Dynamic Linking) ( 或指向執行時常量池的方法引用)
- 每一個棧幀內部都包含一個指向執行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支援當前方法的程式碼能夠實現動態連結(Dynamic Linking)。比如: invokedynamic指令
- 在Java原始檔被編譯到位元組碼檔案中時,所有的變數和方法引用都作為符號引用(Symbolic Reference) 儲存在class檔案的常量池裡。比如:描述一一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那麼動態連結的作用就是為了將這些符號引用轉換為呼叫方法的直接引用。
-
方法返回地址(Return Address) (或方法正常退 出或者異常退出的定義)
- 存放呼叫該方法的pc暫存器的值
- 一個方法的結束,有兩種方式:
➢正常執行完成
➢出現未處理的異常,非正常退出 - 無論通過哪種方式退出,在方法退出後都返回到該方法被呼叫的位置。方法正常退出時,呼叫者的pc計數器的值作為返回地址,即呼叫該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會儲存這部分資訊。
- 本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的區域性變量表、運算元棧、將返回值壓入呼叫者棧幀的運算元棧、設定PC暫存器值等,讓呼叫者方法繼續執行下去。
- 正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他的上層呼叫者產生任何的返回值。
-
一些附加資訊
- 棧幀中還允許攜帶與Java虛擬機器實現相關的一些附加資訊。例如,對程式除錯提供支援的資訊。
方法的呼叫原理
在JVM中,將符號引用轉換為呼叫方法的直接引用與方法的繫結機制相關。
-
靜態連結:
當一個位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯期可知,且執行期保持不變時。這種情況下將呼叫方法的符號引用轉換為直接引用的過程稱之為靜態連結。
-
動態連結:
如果被呼叫的方法在編譯期無法被確定下來,也就是說,只能夠在程式執行期將呼叫方法的符號引用轉換為直接引用,由於這種引用轉換過程具備動態性,因此也就被稱之為動態連結。 -
對應的方法的繫結機制為:早期繫結(Early Binding)和晚期繫結(Late Binding) 。繫結是一個欄位,方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次。
-
早期繫結:
早期繫結就是指被呼叫的目標方法如果在編譯期可知,且執行期保持不變時,即可將這個方法與所屬的型別進行繫結,這樣一來,由於明確了被呼叫的目標方法究竟是哪一個,因此也就可以使用靜態連結的方式將符號引用轉換為直接引用。 -
晚期繫結:
如果被呼叫的方法在編譯期無法被確定下來,只能夠在程式執行期根據實際的型別繫結相關的方法,這種繫結方式也就被稱之為晚期繫結。
4種方法呼叫指令區分虛方法和非虛方法
- 如果方法在編譯期就確定了具體的呼叫版本,這個版本在執行時是不可變的。這樣的方法稱為非虛方法。
- 靜態方法、私有方法、final方法、例項構造器、父類方法都是非虛方法。其他方法稱為虛方法。
虛擬機器中提供了以下幾條方法呼叫指令:
普通呼叫指令:
- invokestatic:呼叫靜態方法,解析階段確定唯一方法版本
- invokespecial: 呼叫
方法、私有及父類方法,解析階段確定唯一方法版本 - invokevirtual: 呼叫所有虛方法(final修飾的方法除外)
- invokeinterface: 呼叫介面方法
動態呼叫指令:(JDK7+)
- invokedynamic: 動態解析出需要呼叫的方法,然後執行
- 前四條指令固化在虛擬機器內部,方法的呼叫執行不可人為干預,而invokedynamic指 令則支援由使用者確定方法版本。其中invokestatic指令和invokespecial指令呼叫的方法稱為非虛方法,其餘的(final修飾的除外)稱為虛方法。
- JVM位元組碼指令集一直比較穩定,一直到Java7中才增加了一個.invokedynamic指令,這是Java為了實現「動態型別語言」支援而做的一種改進。
- 但是在Java7中並沒有提供直接生成invokedynamic指令的方法,需要藉助ASM這種底層位元組碼工具來產生invokedynamic指令。直到Java8的Lambda表示式的出現,invokedynamic指 令的生成,在Java中才有 了直接的生成方式。
- Java7中增加的動態語言型別支援的本質是對Java虛擬機器規範的修改,而不.是對Java語言規則的修改,這一 塊相對來講比較複雜,增加了虛擬機器中的方法呼叫,最直接的受益者就是執行在Java平臺的動態語言的編譯器。
方法重寫的本質和虛方法表
Java 語言中方法重寫的本質:
- 找到運算元棧頂的第一個元素所執行的物件的實際型別,記作C
- 如果在型別C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問許可權校驗,如果通過則返回這個方法的直接引用,查詢不通過,則返回java. lang.IllegalAccessError 異常。
- 否則,按照繼承關係從下往上依次對C的各個父類進行第2步的搜尋和驗證過程。
- 如果始終沒有找到合適的方法,則丟擲java. lang .AbstractMethodError異常。
虛方法表:
- 在面向物件的程式設計中,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元資料中搜索合適的目標的話就可能影響到執行效率。因此,為了提高效能,JVM採用在類的方法區建立一個虛方法表(virtual method table) (非虛方法不會出現在表中)來實現。使用索引表來代替查詢。
- 每個類中都有一個虛方法表,表中存放著各個方法的實際入口。
- 虛方法表會在類載入的連結階段被建立並開始初始化,類的變數初始值準備完成之後,JVM會把該類的方法表也初始化完畢。
本地方法棧
- Java虛擬機器棧用於管理Java方法的呼叫,而本地方法棧用於管理本地方法的呼叫。
- 本地方法棧,也是執行緒私有的。.
- 允許被實現成固定或者是可動態擴充套件的記憶體大小。(在記憶體溢位方面是相同的)
➢如果執行緒請求分配的棧容量超過本地方法棧允許的最大容量,Java虛擬機器將會丟擲一個stackOverflowError 異常。
➢如果本地方法棧可以動態擴充套件,並且在嘗試擴充套件的時候無法申請到足夠的記憶體,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的本地方法棧,那麼Java虛擬機器將會丟擲一個OutOfMemoryError 異常。 - 本地方法是使用c語言實現的。
- 它的具體做法是Native Method Stack中 登記native方法,在Execution Engine執行時載入本地方法庫。
- 當某個執行緒呼叫一個本地方法時,它就進入了一個全新的並且不再受虛擬機器限制的世界。它和虛擬機器擁有同樣的許可權。
➢本地方法可以通過本地方法介面來訪問虛擬機器內部的執行時資料區。
➢它甚至可以直接使用本地處理器中的暫存器
➢直接從本地記憶體的堆中分配任意數量的記憶體。 - 並不是所有的JVM都支援本地方法。因為Java虛擬機器規範並沒有明確要求本地方法棧的使用語言、具體實現方式、資料結構等。如果JVM產品不打算支援native方法,也可以無需實現本地方法棧。
- 在Hotspot JVM中, 直接將本地方法棧和虛擬機器棧合二為一。
堆
- 一個JVM例項只存在一個堆記憶體,堆也是Java記憶體管理的核心區域。
- Java堆區在JVM啟動的時候即被建立,其空間大小也就確定了。是JVM管理的最大一塊記憶體空間。
➢堆記憶體的大小是可以調節的。 - 《Java虛擬機器規範》規定,堆可以處於物理.上不連續的記憶體空間中,但在邏輯.上它應該被視為連續的。
- 所有的執行緒共享Java堆,在這裡還可以劃分執行緒私有的緩衝區(Thread Local Al location Buffer, TLAB) 。
- 《Java虛擬機器規範》中對Java堆的描述是:所有的物件例項以及陣列都應當在執行耐分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated )
➢“幾乎”所有的物件例項都在這裡分配記憶體(特殊情況會在棧上分配)。一從實際使用角度看的。 - 陣列和物件可能永遠不會儲存在棧上,因為棧幀中儲存引用,這個引用指向物件或者陣列在堆中的位置。
- 在方法結束後,堆中的物件不會馬上被移除,僅僅在垃圾收集的時候才會被移除。
- 堆,是GC ( Garbage Collection, 垃圾收集器)執行垃圾回收的重點區域。
堆記憶體結構
現代垃圾收集器大部分都基於分代收集理論設計,堆空間細分為:
- Java 7及之前堆記憶體邏輯上分為三部分:新生區+養老區+永久區
➢Young Generation Space 新生區 Young/New
又被劃分為Eden區和Survivor區
➢Tenure generation space 養老區 old/ Tenure
➢Permanent Space 永久區 Perm - Java 8及之後堆記憶體邏輯上分為三部分:新生區+養老區+元空間
➢Young Generation Space 新生區 Young/New
又被劃分為Eden區和Survivor區
➢Tenure generation space 養老區 old/Tenure
➢Meta Space 元空間 Meta - 約定:新生區<=>新生代臺<=>年輕代 養老區<=>老年區<=>老年代 永久區<=>永久代
堆空間大小的設定
- Java堆區用於儲存Java物件例項,那麼堆的大小在JVM啟動時就已經設定好了,大家可以通過選項" -Xmx"和" -Xms”來進行設定。
➢“-Xms"用於表示堆區的起始記憶體,等價於-XX: InitialHeapSize
➢“-Xmx"則用於表示堆區的最大記憶體,等價於-XX :MaxHeapSize - 一旦堆區中的記憶體大小超過“-Xmx"所指定的最大記憶體時,將會丟擲OutOfMemoryError異常。
- 通常會將-Xms和- Xmx兩個引數配置相同的值,其目的是為了能夠在java垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,從而提高效能。
- 預設情況下,初始記憶體大小:物理電腦記憶體大小/ 64 最大記憶體大小:物理電腦記憶體大小/ 4
年輕代和老年代
- 儲存在JVM中的Java物件可以被劃分為兩類:
➢一類是生命週期較短的瞬時物件,這類物件的建立和消亡都非常迅速
➢另外一類物件的生命週期卻非常長,在某些極端的情況下還能夠與JVM的生命週期保持一致。 - Java堆區進一步細分的話, 可以劃分為年輕代(YoungGen)和老年代(OldGen)
- 其中年輕代又可以劃分為Eden空間、Survivor0空間和Survivor1空間(有時也叫做from區、to區)。
- 配置新生代與老年代在堆結構的佔比。
➢預設-XX: NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3
➢可以修改-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5 - 在Hotspot中,Eden空間和另外兩個Survivor空間預設所佔的比例是8:1:1
- 當然開發人員可以通過選項“-XX:SurvivorRatio”調整這個空間比例。比如-XX : SurvivorRatio=8
- 幾乎所有的Java物件都是在Eden區被new出來的。
- 絕大部分的Java物件的銷燬都在新生代進行了。
➢IBM公司的專門研究表明,新生代中80%的物件都是“朝生夕死”的。 - 可以使用選項"-Xmn"設定新生代最大記憶體大小
➢這個引數一般使用預設值就可以了。
垃圾回收概念
JVM在進行GC時,並非每次都對上面三個記憶體(新生代、老年代;方法區)區域一起回收的,大部分時候回收的都是指新生代。
針對Hotspot VM的實現,它裡面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Fu11 GC )
-
部分收集:不是完整收集整個Java堆的垃圾收集。 其中又分為:
➢新生代收集(Minor GC / Young GC) :只是新生代的垃圾收集
➢老年代收集(Major GC / 0ld GC) :只是老年代的垃圾收集。- 目前,只有CMS GC會有單獨收集老年代的行為。
- 注意,很多時候Major GC會和Fu1l GC混淆使用,需要具體分辨是老年代回收還是整堆回收。
➢混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC會有這種行為
-
整堆收集(Full GC):收集整個java堆和方法區的垃圾收集。
年輕代GC(Minor GC) 觸發機制:
- 當年輕代空間不足時,就會觸發Minor GC,這裡的年輕代滿指的是Eden代滿,Survivor滿不會引發GC。(每次 Minor GC會清理年輕代的記憶體。)
- 因為Java物件大多都具備朝生夕滅的特性,所以MinorGC非常頻繁,一般回收速度也比較快。這一定義既清晰又易於理解。
- Minor GC會引發STW,暫停其它使用者的執行緒,等垃圾回收結束,使用者執行緒才恢復執行。
老年代GC (Major GC/Full GC)觸發機制:
- 指發生在老年代的GC,物件從老年代消失時,我們說“Major GC" 或“Fu1l GC”發生了。
- 出現了Major GC,經常會伴隨至少一次的Minor GC (但非絕對的,在Parallel Scavenge收集器的收集策略裡就有直接進行Major GC的策略選擇過程)。
- 也就是在老年代空間不足時,會先嚐試觸發Minor GC。如果之後空間還不足,則觸發Major GC
- Major GC的速度一般會比Minor GC慢10倍以上,STW的時間更長。
- 如果Major GC後,記憶體還不足,就報OOM了。
- Major GC的速度一般會比Minor GC慢10倍以上。
Fu11 GC觸發機制
觸發Fu1l GC執行的情況有如下五種:
(1) 呼叫System.gc()時,系統建議執行Full GC,但是不必然執行
(2)老年代空間不足
(3)方法區空間不足
(4)通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體.
(5)由Eden區、survivor space0 (From Space)區向survivor space1 (To Space)區複製時,物件大小大於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小
說明: full gc是開發或調優中儘量要避免的。這樣暫停時間會短一些。
物件提升規則
針對不同年齡段的物件分配原則如下所示:
- 優先分配到Eden
- 大物件直接分配到老年代
➢儘量避免程式中出現過多的大物件 - 長期存活的物件分配到老年代
- 動態物件年齡判斷
➢如果Survivor 區中相同年齡的所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。 - 空間分配擔保
➢-XX: HandlePromotionFailure
物件分配和新生代垃圾回收(YGC/Minor GC)
新物件一般在Eden區分配,當Eden區滿了或是Eden區剩餘空間不足以放下新的物件時會進行YGC將垃圾回收,然後將剩下的物件放到倖存區(from),即清空Eden區。
- 針對倖存者s0,s1區的總結:複製之後有交換,誰空誰是to.
- 關於垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不在永久區/元空間收集。
TLAB(Thread Local Allocation Buffer)
為什麼有TLAB?
- 堆區是執行緒共享區域,任何執行緒都可以訪問到堆區中的共享資料
- 由於物件例項的建立在JVM中非常頻繁,因此在併發環境下從堆區中劃分記憶體空間是執行緒不安全的
- 為避免多個執行緒操作同一地址,需要使用加鎖等機制,進而影響分配速度。
什麼是TLAB?
- 從記憶體模型而不是垃圾收集的角度,對Eden區 域繼續進行劃分,JVM為每個執行緒分配了一個私有快取區域,它包含在Eden空間內。
- 多執行緒同時分配記憶體時,使用TLAB可以避免一系列的非執行緒安全問題,同時還能夠提升記憶體分配的吞吐量,因此我們可以將這種記憶體分配方式稱之為快速分配策略。
- 據我所知所有OpenJDK衍生出來的JVM都提供了TLAB的設計。
- 儘管不是所有的物件例項都能夠在TLAB中成功分配記憶體,但JVM確實是將TLAB作為記憶體分配的首選。
- 在程式中,開發人員可以通過選項“-XX:UseTLAB”設定是否開啟TLAB空間。
- 預設情況下,TLAB空 間的記憶體非常小,僅佔有整個Eden空間的1%,當然我們可以通過選項“-XX:TLABWasteTargetPercent"設定TLAB空間所佔用Eden空間的百分比大小。
- 一旦物件在TLAB空間分配記憶體失敗時,JVM就會嘗試著通過使用加鎖機制確保資料操作的原子性,從而直接在Eden空間中分配記憶體。
堆空間引數的設定
- 官網說明: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
- -XX:+PrintFlagsInitial :檢視所有的引數的預設初始值
- -XX:+PrintFlagsFinal : 檢視所有的引數的最終值(可能會存在修改,不再是初始值)
- -Xms:初始堆空間記憶體 (預設為實體記憶體的1/64)
- -Xmx:最大堆空間記憶體(預設為實體記憶體的1/4)
- -Xmn: 設定新生代的大小。(初始值及最大值)
- -XX: NewRatio:配置新生代與老年代在堆結構的佔比
- XX:NewRatio:配置新生代與老年代在堆結構的佔比
- -XX:SurvivorRatio:設定新生代中Eden和S0/S1空間的比例
- -XX:MaxTenuringThreshold:設定新生代垃圾的最大年齡
- -XX: +PrintGCDetails:輸出詳細的GC處理日誌
列印gc簡要資訊:①-XX:+PrintGC ② -verbose:gc - -XX:HandlePromotionFailure:是否設定空間分配擔保
在發生Minor GC之前,虛擬機器會檢查老年代最大可用的連續空間是否大於新生代所有物件的總空間。
-
如果大於, 則此次Minor GC是安全的
-
如果小於,則虛擬機器會檢視-XX:HandlePromotionFailure設定值是否允許擔保失敗。
➢如果HandlePromotionFailure=true, 那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的物件的平均大小。- 如果大於,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;
- 如果小於,則改為進行一次Full GC。
➢如果HandlePromotionFailure=false, 則改為進行一次Full GC。
在JDK6 Update24之 後,HandlePromotionFailure 引數不會再影響到虛擬機器的空間分配擔保策略,觀察OpenJDK中 的原始碼變化,雖然原始碼中還定義了HandlePromotionFailure引數,但是在程式碼中已經不會再使用它。JDK6 Update24之後的規則變為只要老年代的連續空間大於新生代物件總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。
逃逸分析
在Java虛擬機器中,物件是在Java堆中分配記憶體的,這是一個普遍的常識。但是,有一種特殊情況,那就是如果經過逃逸分析(Escape Analysis) 後發現,一個物件並沒有逃逸出方法的話,那麼就可能被優化成棧上分配。這樣就無需在堆上分配記憶體,也無須進行垃圾回收了。這也是最常見的堆外儲存技術。
-
逃逸分析是一種可以有效減少Java程式中同步負載和記憶體堆分配壓力的跨函式全域性資料流分析演算法。
-
通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的物件的引用的使用範圍從而決定是否要將這個物件分配到堆上。
-
逃逸分析的基本行為就是分析物件動態作用域
➢當一個物件在方法中被定義後,物件只在方法內部使用,則認為沒有發生逃逸。
➢當一個物件在方法中被定義後,它被外部方法所引用,則認為發生逃逸。例如作為呼叫引數傳遞到其他地方中。 -
引數設定:
- 在JDK 6u23版本之後,Hotspot中預設就已經開啟了逃逸分析。
- 如果使用的是較早的版本,開發人員則可以通過:
➢選項“-XX: +DoEscapeAnalysis"顯式開啟逃逸分析若JDK是32位還需加上-server引數,因為在server模式下才可以使用逃逸分析,而32位是預設client模式
➢通過選項“-XX: +PrintEscapeAnalysis"檢視逃逸分析的篩選結果。
-
開發中能使用區域性變數的,就不要使用在方法外定義。
-
使用逃逸分析,編譯器可以對程式碼做如下優化:
一、棧上分配。將堆分配轉化為棧分配。如果一個物件在子程式中被分配,要使指向該物件的指標永遠不會逃逸,物件可能是棧分配的候選,而不是堆分配。
據我所知 Oracle Hotspot JVM中(至JDK13)並沒有這麼做
二、同步省略。如果一個物件被發現只能從一個執行緒被訪問到,那麼對於這個物件的操作可以不考慮同步。
- 執行緒同步的代價是相當高的,同步的後果是降低併發性和效能。
- 在動態編譯同步塊的時候,JIT編譯器可以藉助逃逸分析來判斷同步塊所使用的鎖物件是否只能夠被一個執行緒訪問而沒有被髮布到其他執行緒。如果沒有,那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分程式碼的同步。這樣就能大大提高併發性和效能。這個取消同步的過程就叫同步省略,也叫鎖消除。
三、分離物件或標量替換。有的物件可能不需要作為一個連續的記憶體結構存在也可以被訪問到,那麼物件的部分(或全部)可以不儲存在記憶體,而是儲存在CPU暫存器中。.
- 標量(Scalar)是指一個無法再分解成更小的資料的資料。Java中的原始資料型別就是標量。
- 相對的,那些還可以分解的資料叫做聚合量(Aggregate),Java中的物件就是聚合量,因為他可以分解成其他聚合量和標量。
- 在JIT階段,如果經過逃逸分析,發現一個物件不會被外界訪問的話,那麼經過JIT優化,就會把這個物件拆解成若千個其中包含的若千個成員變數來代替。這個過程就是標量替換。.
- 標量替換引數設定:
引數-XX: +EliminateAllocations:開啟了標量替換(預設開啟),允許將物件打散分配在棧上。
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;
}
以上程式碼經過標量替換就會變成
public static void main(String[] args) {
private int X;
private int y;
System.out.println("point.x=" + x + "; point.y=" + y);
}
方法區/元空間
棧、堆方法區的互動關係
方法區的理解
《Java虛擬機器規範》中明確說明:“儘管所有的方法區在邏輯上是屬於堆的- - 部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。”但對於HotspotJVM而言,方法區還有一個別名叫做Non-Heap (非堆),目的就是要和堆分開。
- 方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域。
- 方法區在JVM啟動的時候被建立,並且它的實際的實體記憶體空間中和Java堆區一樣都可以是不連續的。
- 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴充套件。
- 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器同樣會丟擲記憶體溢位錯誤: java. lang . OutOfMemoryError:PermGen space---JDK7或者java. lang . OutOfMemoryError: Metaspace---JDK8
- 關閉JVM就會釋放這個區域的記憶體。
Hotspot 中方法區的演進
-
在jdk7及以前,習慣上把方法區,稱為永久代。jdk8開始,使用元空間取代了永久代。
-
本質上,方法區和永久代並不等價。僅是對hotspot而言的。《Java 虛擬機器規範》對如何實現方法區,不做統一要求。例如: BEA JRockit/ IBM J9中不存在永久代的概念。
➢現在來看,當年使用永久代,不是好的idea。導致Java程式更容易OOM (超過-XX :MaxPermSize上限)
img/
- 元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機器設定的記憶體中,而是使用本地記憶體。
- 永久代、元空間二者並不只是名字變了,內部結構也調整了。
- 根據《Java虛擬機器規範》的規定,如果方法區無法滿足新的記憶體分配需求時,將丟擲O0M異常。
設定方法區大小與OOM
-
方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。
-
jdk7及以前:
➢通過-XX: PermSize來設定永久代初始分配空間。預設值是20.75M
➢-XX:MaxPermSize來設定永久代最大可分配空間。32位機器預設是64M,64位機器模式是82M
➢當JVM載入的類資訊容量超過了這個值,會報異常OutOfMemoryError : PermGenspace。-XX:PermSize=100m -XX:MaxPermSize=100m
-
jdk8及以後:
➢元資料區大小可以使用引數-XX :MetaspaceSize和-XX : MaxMetaspaceSize指定,替代上述原有的兩個引數。
➢預設值依賴於平臺。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制。
➢與永久代不同, 如果不指定大小,預設情況下,虛擬機器會耗盡所有的可用系統記憶體。如果元資料區發生溢位,虛擬機器一樣會丟擲異常OutOfMemoryError: Metaspace
➢-XX :MetaspaceSize:設定初始的元空間大小。對於一個 64位的伺服器端JVM來說,其預設的-XX :Me taspaceSize值為21MB。這就是初始的高水位線,一旦觸及這個水位線,FullGC將會被觸發並解除安裝沒用的類(即這些類對應的類載入器不再存活),然後這個高水位線將會重置。新的高水位線的值取決於GC後釋放了多少元空間。如果釋放的空間不足,那麼在不超過MaxMetaspaceSize時,適當提高該值。如果釋放空間過多,則適當降低該值。
➢如果初始化的高水位線設定過低,上述高水位 線調整情況會發生很多次。通過垃圾回收器的日誌可以觀察到Full GC多次呼叫。為了避免頻繁地GC,建議將-XX :MetaspaceSize設定為一個相對較高的值。-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m
1、要解決OOM異常或heap space的異常,一般的手段是首先通過記憶體映像分析工具(如Eclipse Memory Analyzer) 對dump出來的堆轉儲快照進行分析,重點是確認記憶體中的物件是否是必要的,也就是要先分清楚到底是出現了記憶體洩漏(Memory Leak)還是記憶體溢位(Memory 0verflow)。
2、如果是記憶體洩漏,可進一步通過工具檢視洩漏物件到GC Roots的引用鏈。於是就能找到洩漏物件是通過怎樣的路徑與GCRoots相關聯並導致垃圾收集器無法自動回收它們的。掌握了洩漏物件的型別資訊,以及GCRoots引用鏈的資訊,就可以比較準確地定位出洩漏程式碼的位置。
3、如果不存在記憶體洩漏,換句話說就是記憶體中的物件確實都還必須存活著,那就應當檢查虛擬機器的堆引數( -Xmx與-Xms) ,與機器實體記憶體對比看是否還可以調大,從程式碼上檢查是否存在某些物件生命週期過長、持有狀態時間過長的情況,嘗試減少程式執行期的記憶體消耗。
方法區的內部結構
《深入理解Java虛擬機器》書中對方法區(Method Area)儲存內容描述如下:它用於儲存已被虛擬機器載入的型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等。
型別資訊
- 對每個載入的型別(類class、介面interface、列舉enum、註解annotation) ,JVM必須在方法區中儲存以下型別資訊:
①這個型別的完整有效名稱(全名=包名.類名)
②這個型別直接父類的完整有效名(對於interface或是java.lang.object,都沒有父類)
③這個型別的修飾符(public, abstract, final的某個子集)
④這個型別直接介面的一個有序列表
域(field)資訊---成員變數
- JVM必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序。
- 域的相關資訊包括:域名稱、域型別、域修飾符(public, private,protected, static, final, volatile, transient的某個子集)
方法資訊
- JVM必須儲存所有方法的以下資訊,同域資訊一樣包括宣告順序:
- 方法名稱
- 方法的返回型別(或void)
- 方法引數的數量和型別(按順序)
- 方法的修飾符 (public, private, protected, static, final,synchronized, native,abstract的一個子集)
- 方法的位元組碼(bytecodes)、運算元棧、區域性變量表及大小(abstract和native方法除外)
- 異常表( abstract和native方法除外)
➢每個異常處理的開始位置、結束位置、程式碼處理在程式計數器中的偏移地址、被捕獲的異常類的常量池索引
non-final型別的變數
- 靜態變數和類關聯在一起,隨著類的載入而載入,它們成為類資料在邏輯上的一部分。
- 類變數被類的所有例項共享,即使沒有類例項時你也可以訪問它。
全域性常量 static final
- 被宣告為final的類變數的處理方法則不同,每個全域性常量在編譯的時候就會被分配了。(編譯時就被賦值了而不是載入時)
執行時常量池
常量池
-
方法區,內部包含了執行時常量池。
-
位元組碼檔案,內部包含了常量池。
-
要弄清楚方法區,需要理解清楚ClassFile,因為載入類的資訊都在方法區。
-
要弄清楚方法區的執行時常量池,需要理解清楚ClassFile中的常量池。
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
如下:ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }
一個java原始檔中的類、介面,編譯後產生一個位元組碼檔案。而Java中的字 節碼需要資料支援,通常這種資料會很大以至於不能直接存到位元組碼裡,換另一種方式,可以存到常量池,這個位元組碼包含了指向常量池的引用。在動態連結的時候會用到執行時常量池,之前有介紹。
比如如下程式碼:
public class SimpleClass{
public void syaHello(){
System.out.println("Hello");
}
}
編譯後雖然只有194位元組,但是裡面卻使用了String、System、 PrintStream及object等結構。這裡程式碼量其實已經很小了。如果程式碼多,引用到的結構會更多!這裡就需要常量池了!
幾種在常量池記憶體儲的資料型別包括:
- 數量值
- 字串值
- 類引用
- 欄位引用
- 方法引用
常量池總結
常量池,可以看做是一張表,虛擬機器指令根據這張常量表找到要執行的類名、方法名、引數型別、字面量等型別。
執行時常量池
- 執行時常量池( Runtime Constant Pool)是方法區的一部分。
- 常量池表( Constant Pool Table) 是Class檔案的一部分,用於存放編譯期生成的各種字面量與符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。
- 執行時常量池,在載入類和介面到虛擬機器後,就會建立對應的執行時常量池。
- JVM為每個已載入的型別(類或介面)都維護一個常量池。池中的資料項像陣列項一樣,是通過索引訪問的。
- 執行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到執行期解析後才能夠獲得的方法或者欄位引用。此時不再是常量池中的符號地址了,這裡換為真實地址。
➢執行時常量池,相對於Class檔案常量池的另一重要特徵是:具備動態性。 - 執行時常量池類似於傳統程式語言中的符號表(symbol table) ,但是它所包含的資料卻比符號表要更加豐富一些。
- 當建立類或介面的執行時常量池時,如果構造執行時常量池所需的記憶體空間超過了方法區所能提供的最大值,則JVM會拋OutOfMemoryError異常。
方法區的演進細節
-
首先明確:只有Hotspot才 有永久代。BEA JRockit、 IBM J9等來說,是不存在永久代的概念的。原則上如何實現方
法區屬於虛擬機器實現細節,不受《Java虛擬機器規範》管束,並不要求統一。 -
Hotspot中方法區的變化:
-
jdk1.6及之前
有永久代(permanent generation), 靜態變數存放在永久代上
-
jdk1.7
有永久代,但已經逐步“去永久代”,字串常量池、靜態變數移除,儲存在堆中。
-
jdk1.8及以後
無永久代,型別資訊、欄位、方法、常量儲存在本地記憶體的元空間,但字串常量池、靜態變數仍在堆中。
-
注意!靜態變數物件本身始終在堆中,改變的是對物件的引用
永久代為什麼要被元空間替換
- 隨著Java8的到來,Hotspot VM中再也見不到永久代了。但是這並不意味著類的元資料資訊也消失了。這些資料被移到了一個與堆不相連的本地記憶體區域,這個區域叫做元空間( Metaspace )。
- 由於類的元資料分配在本地記憶體中,元空間的最大可分配空間就是系統可用記憶體空間。
- 這項改動是很有必要的,原因有:
- 為永久代設定空間大小是很難確定的。
在某些場景下,如果動態載入類過多,容易產生Perm區的OOM。比如某個實際web.工程中,因為功能點比較多,在執行過程中,要不斷動態載入很多類,經常出現致命錯誤。OutOfMemoryError: PermGen
而元空間和永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。 - 對永久代進行調優是很困難的。
- 為永久代設定空間大小是很難確定的。
String Table 為什麼要調整位置
jdk7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在full gc的時候才會觸發。而full gc是老年代的空間不足、永久代不足時才會觸發。這就導致StringTable回收效率不高。而我們開發中會有大量的字串被建立,回收效率低,導致永久代記憶體不足。放到堆裡,能及時回收記憶體。
方法區的垃圾回收
有些人認為方法區(如HotSpot虛擬機器中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機器規範》 對方法區的約束是非常寬鬆的,提到過可以不要求虛擬機器在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區型別解除安裝的收集器存在(如JDK11時期的ZGC收集器就不支援類解除安裝)。
一般來說這個區域的回收效果比較難令人滿意,尤其是型別的解除安裝,條件相當苛刻。但是這部分割槽域的回收有時又確實是必要的。以前Sun公司的Bug列表中,曾出現過的若干個嚴重的Bug就是由於低版本的HotSpot虛擬機器對此區域未完全回收而導致記憶體洩漏。
方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的型別。
- 先來說說方法區內常量池之中主要存放的兩大類常量:字面量和符號引用。字面量比較接近Java語言層次的常量概念,如文字字串、被宣告為final的常量值等。而符號引用則屬於編譯原理方面的概念,包括下面三類常量:
➢1、類和介面的全限定名
➢2、欄位的名稱和描述符
➢3、方法的名稱和描述符 - HotSpot虛擬機器對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。
- 回收廢棄常量與回收Java堆中的物件非常類似。
- 判定一個常量是否“廢棄”還是相對簡單,而要判定-一個型別是否屬於“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
➢該類所有的例項都已經被回收,也就是Java堆中不存在該類及其任何派生子類的例項。
➢載入該類的類載入器已經被回收,這個條件除非是經過精心設計的可替換類載入器的場景,如OSGi、JSP的重載入等,否則通常是很難達成的。
➢該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。 - Java虛擬機器被允許對滿足上述三個條件的無用類進行回收,這裡說的僅僅是“被允許”,而並不是和物件一樣,沒有引用了就必然會回收。關於是否要對型別進行回收,HotSpot虛擬機器提供了-Xnoclassgc引數進行控制,還可以使用-verbose: class以及-XX: +TraceClass-Loading、-XX:+TraceClassUnLoading檢視類載入和解除安裝資訊
- 在大量使用反射、動態代理、CGLib等 位元組碼框架,動態生成JSP以及OSGi這類頻繁自定義類載入器的場景中,通常都需要Java虛擬機器具備型別解除安裝的能力,以保證不會對方法區造成過大的記憶體壓力。
上一篇:JVM的生命週期和發展歷程
下一篇: