JVM運行時資料區
簡介
JVM運行時資料區包括:JVM堆疊(虛擬機堆疊),堆,方法區,本地方法堆疊,PC暫存器,大概的劃分就是堆疊和堆,以及一些其他的結構,重點在JVM堆疊,堆,方法區,JVM規范指出:方法區在邏輯上屬于堆,但是實際的具體的JVM中并不屬于堆的一部分,
在JVM堆疊中會發生GC和Error,但是在其他的記憶體區域中,可能沒有GC或者Error,
有些區域的生命周期是跟隨著虛擬機的,當虛擬機被關閉時,這部分的記憶體也被釋放出來,有些是跟隨執行緒的,當執行緒結束時,這部分的記憶體也被釋放出來,
下圖展示了哪些區域是執行緒共享和執行緒私有的,
執行緒私有的:PC暫存器,堆疊,本地方法堆疊
執行緒間共享的:堆,堆外記憶體(永久代或元空間,代碼快取)
每個JVM對應Java的RunTime類的物件,RunTime實體在每個JVM中只有一個,
運行時資料區的結構圖解:

PC暫存器(程式計數器)
簡介
類似與計算機組成原理中提及的CPU的PC暫存器,但是CPU的PC暫存器是有實際硬體的,但是JVM的PC暫存器沒有實際的硬體部分,這個是每個執行緒私有的,
JVM的PC暫存器只是一塊很小很小的記憶體區域,
作用
- PC暫存器用來存盤指向下一條指令的地址,也就是即將執行的指令,由執行引擎讀取下一條指令,程式的分支,回圈,跳轉,例外處理,執行緒恢復等基礎功能都需要PC寄存去來完成,
- PC暫存器是唯一一個在JVM規范中沒有規定任何OutOfMemoryError情況的區域,
常見問題
為什么需要PC暫存器
- 因為CPU需要不停地切換執行緒,當切換回來的時候就需要知道該執行緒從哪里開始繼續執行,
- 如果沒有這個機制,那么切換執行緒應當不容易,有了這個機制之后,只需要讀取PC寄存去的指令就知道該執行哪一個指令,實作起來比較容易,
為什么PC暫存器是執行緒私有的
因為需要準確記錄各個執行緒正在執行的當前的位元組碼指令,必須是執行緒私有的,如果不是執行緒私有的,意味著任意執行緒可以隨意更改任意執行緒的位元組碼指令,這會導致程式的錯誤,
JVM堆疊(虛擬機堆疊) 重點
簡介
堆疊是運行時的單位,堆是存盤的單位,既:堆疊存放著程式如何執行的問題,堆解決的時候資料存盤的問題,
具體特性:
- JVM堆疊是每個執行緒私有的,
- 每創建一個執行緒就會創建一個虛擬機堆疊,虛擬機堆疊的基本單位是堆疊幀(Stack Frame),堆疊幀對應著方法呼叫,
- 生命周期和執行緒一致,
- 沒有GC,有錯誤(下面提及),
- 訪問速度僅次于PC暫存器
- JVM對虛擬機堆疊的操作只有兩個
- 方法執行,伴隨著進堆疊
- 方法執行結束后,伴隨著出堆疊
JVM規范中允許JVM堆疊的大小是動態的或者固定的,以下是JVM堆疊中會遇到的Error
-
JVM堆疊大小為動態,遇到以下情況會拋出OutOfMemoryError例外
- 嘗試拓展時不能申請到足夠的記憶體
- 創建新執行緒時沒有足夠的記憶體創建對應的JVM堆疊
-
大小固定,遇到以下情況會拋出StackOverflow例外
- 執行緒申請的JVM堆疊的容量超過虛擬機所允許的最大容量
JVM堆疊的作用
保存著方法的區域變數,部分結果,并參與方法的呼叫與回傳,
JVM堆疊的存盤單位
JVM堆疊存盤什么
-
每個執行緒都有自己的堆疊,堆疊的資料都是以堆疊幀(Stack Frame)的格式存在,
-
每個方法都對應一個堆疊幀
-
堆疊幀時一個記憶體塊,是資料的集合,保存著方法運行中的各種資料資訊,
堆疊運行原理
在一個運行中的執行緒,一個時間點上只有一個活動的堆疊幀,這個堆疊幀被稱為當前堆疊幀,對應的方法是當前方法,對應的類是當前類,
執行引擎運行的所有的位元組碼指令只針對當前堆疊幀來操作,如果當前堆疊幀呼叫了其他方法,那么對應的堆疊幀也會被創建出來,進入堆疊中,成為當前堆疊幀,


堆疊幀的內部結構
JVM規范對應鏈接
存盤著以下資訊
- 區域變數表 Local Variables(重要)
- 運算元堆疊 Operand Stacks(重要)
- 方法回傳地址
- 動態連接 Dynamic Linking
- 其他資訊
區域變數表Local Variables
- 被稱為區域變數陣列或本地變數表,
- 是一個數字陣列,主要存盤方法引數和定義在方法內的區域變數,這些資料型別包括基本資料型別參考,物件參考,以及回傳地址型別,
- 沒有安全問題
- 區域變數表的大小是在編譯期確定下來的,并且保存在方法的Code屬性的maximum local variables,運行期間不會改變區域變數表的大小,
區域變數表的基本單位是Slot(插槽),32位以內的型別占用一個Slot,
64位的型別占用兩個Slot,只有long和double型別占用兩個插槽,其余都是占用一個插槽,非數值的型別,如boolean,則轉為int后再存盤,
形參和方法內區域變數會按照順序被復制到每一個Slot上,但是有一個變數特殊,就是this,
如果是非靜態方法和構造器,第一個變數將是this,如果是靜態方法,區域變數表沒有this這個變數,
可以使用jclasslib軟體或idea中的jclasslib插件查看具體的區域變數表,如下圖所示,
區域變數表的具體細節
-
序號:該變數在保存在第幾個slot中,從0號開始
-
起始PC:位元組碼的第幾行開始
-
長度:變數的有效范圍的長度
-
名字:對應的變數的名字
比如三號變數b,起始PC是30,長度是7,意味著變數b的作用范圍是30-37,查詢LineNumberTable可知,第30-37號指令對應的行號為42-44,
對應的原始碼
36 public static void testStatic() {
37 Integer integer = new Integer(1);
38 Date date = new Date();
39 int count = 18;
40 System.out.println(count);
41 {
42 int b = 10;
43 System.out.println(b);
44 }
45 int c = 8;
46 }
//下表為對應的LineNumberTable
//下表表明的是指令所對應的代碼行號,對應關系為:起始PC ------行號-1
//
Nr. 起始PC 行號
0 0 37
1 9 38
2 17 39
3 20 40
4 27 42
5 30 43
6 37 45
7 40 46
對應的位元組碼指令
0 new #10 <java/lang/Integer>
3 dup
4 iconst_1
5 invokespecial #11 <java/lang/Integer.<init> : (I)V>
8 astore_0
9 new #12 <java/util/Date>
12 dup
13 invokespecial #13 <java/util/Date.<init> : ()V>
16 astore_1
17 bipush 18
19 istore_2
20 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
23 iload_2
24 invokevirtual #9 <java/io/PrintStream.println : (I)V>
27 bipush 10
29 istore_3
30 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
33 iload_3
34 invokevirtual #9 <java/io/PrintStream.println : (I)V>
37 bipush 8
39 istore_3
40 return
Slot的重復利用
堆疊幀中的區域變數表中的slot是可以復用的,如果一個區域變數過了其作用域,那么在其作用域之后的宣告的新變數就有可能服用過期區域兩邊的槽位,達到節省資源的目的,
上面的例子剛好是一個slot重復利用的例子,變數b和c都是存放在序號為3的slot上,正是因為在b結束后有一個新的變數c宣告出來,
補充說明
在堆疊幀中,與調優最密切相關的部分就是區域變數表,
區域變數表的變數是重要的垃圾回收根節點,只要被區域變數表中直接或間接參考的物件都不會被回收,
運算元堆疊
運算元堆疊也位于堆疊幀當中,屬于執行緒私有的,也被稱為運算式堆疊,Expression Stack,
作用
- 在方法執行中,根據位元組碼指令,往運算元堆疊中寫入資料或提取資料,也就是入堆疊和出堆疊,
- 主要保存著計算程序的中間結果,也是計算程序中便變數的臨時存盤空間,
說明
-
當一個方法剛開始執行時,新的堆疊幀會被創建出來,這個堆疊幀的運算元堆疊時空的 ,
-
因為運算元堆疊是用陣列實作的,所以每一個運算元堆疊都會有一個明確的數值表示其所需的最大深度,保存在方法的Code屬性中,為max_stack的值,
-
運算元堆疊中元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行險證,同時在類加載程序中的類檢驗階段的資料流分析階段要再次驗證
-
和堆疊幀一樣,一個堆疊的單位深度是32位,
-
運算元堆疊的任意元素可以是任意的Java資料型別,
-
如果呼叫方法有回傳值的話,回傳值會被壓入當前堆疊幀的運算元堆疊中
堆疊頂快取技術
由于運算元是存盤在記憶體中的,因此頻繁地執行記憶體讀/寫操作必然會影響執行速度,為了解決這個問題,Hotspot JVM的設計者們提出了堆疊頂快取(ToS,Top-of-Stack Cashing)技術,將堆疊頂元素全部快取 在物理CPU的暫存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率,
動態鏈接(或指向運行時常量池的方法參考)
介紹
也是堆疊幀的一部分,每一個堆疊幀內部都包含一個指向運行時常量池中該堆疊幀所屬方法的參考,包含這個參考的目的就是為了支持當前方法的代碼能夠實作動態鏈接(Dynamic Linking),
比如:invokedynamic指令在Java源檔案被編譯到位元組碼檔案中時,所有的變數和方法參考都作為符號參考(Symbolic Reference)保存在class檔案的常量池里,
比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號參考來表示的,
動態鏈接的作用
就是為了將符號參考轉換為呼叫方法的直接參考,
符號參考
可以看作是一個字串,內容是這個方法的方法頭,拿上邊區域變數表的具體細節的例子來看,常量池如下方代碼塊所示,
Constant pool:
#1 = Methodref #15.#50 // java/lang/Object."<init>":()V
#2 = Fieldref #51.#52 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #53 // aaaa
#4 = Methodref #54.#55 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #56 // java/lang/Exception
#6 = Methodref #54.#57 // java/io/PrintStream.println:(Ljava/lang/Object;)V
省略剩下的,,,,,,,,
動態鏈接的理解
可以看到有一些是帶有Methodref 例如第五行的 #4 = Methodref,帶有Methodref 就是符號參考,但是這些只是一個字串,并不能直接運行, 所以就需要使用符號參考來將靜態的字串轉為可以運行的方法的參考,這些可運行方法的參考保存在方法區的運行時常量池中,
舉例,有個方法a,對應的運行時常量池的地址為0x00a13,那么就需要一個東西來將a和0x00a13對應起來,這個東西就是動態鏈接,
方法的呼叫
在JVM中,將符號參考轉換為呼叫方法的直接參考與方法的系結機制相關,
-
靜態鏈接:
- 當一個位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯期可知,且運行期保持不變時,這種情況下將呼叫方法的符號參考轉換為直接參考的程序稱之為靜態鏈接,
-
動態鏈接:
- 如果被呼叫的方法在編譯期無法被確定下來,也就是說,只能夠在程式運行期將呼叫方法的符號參考轉換為直接參考,由于這種參考轉換程序具備動態性,因此也就被稱之為動態鏈接,
對應的方法的系結機制為:早期系結(Early Binding)和晚期系結(Late Binding),系結是一個欄位、方法或者類在符號參考被替換為直接參考的程序,這僅僅發生一次,
-
早期系結:
- 早期系結就是指被呼叫的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的型別進行系結,這樣一來,由于明確了被呼叫的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號參考轉換為直接參考,
-
晚期系結:
- 如果被呼叫的方法在編譯期無法被確定下來,只能夠在程式運行期根據實際的型別系結相關的方法,這種系結方式也就被稱之為晚期系結,
虛方法和非虛方法
非虛方法:
-
如果方法在編譯期就確定了具體的呼叫版本,這個版本在運行時是不可變的,這樣的方法稱為非虛方法,
-
靜態方法、私有方法、final方法、實體構造器、父類方法都是非虛方法,
其他方法稱為虛方法,例如實體方法,
方法呼叫指令:
普通呼叫指令:
- invokestatic:呼叫靜態方法,決議階段確定唯一方法版本
- invokespecial:呼叫<init>方法、私有及父類方法,決議階段確定唯一方法版本
- invokevirtual:呼叫所有虛方法
- invokeinterface:呼叫介面方法
動態呼叫指令:
5. invokedynamic:動態決議出需要呼叫的方法,然后執行,
前四條指令固化在虛擬機內部,方法的呼叫執行不可人為干預,而invokedynamic指令則支持由用戶確定方法版本,其中invokestatic指令和invokespecial指令呼叫的方法稱為非虛方法,其余的(final修飾的除外)稱為虛方法,
代碼中使用lambda運算式對應到位元組碼指令就會使用到invokedynamic進行呼叫,
動態回傳地址(return address)
-
存放該呼叫方法的pc暫存器的值,例子:假如方法A呼叫方法B,動態回傳地址就是呼叫方法B指令的下一條指令,呼叫方法B指令是第五條指令,那么動態回傳地址應該是第六條指令,
-
一個方法的結束,有兩種方式
-
- 遇到return,將回傳值傳遞給上層方法呼叫者,簡稱正常完成出口(回傳指令包括ireturn(回傳值為boolean,byte,char,short,int),lreturn,freturn,dreturn,以及areturn,還有return 回傳為void、實體初始化方法,類和介面的初始化方法)
- 例外完成出口,即碰到了例外,并且沒有在方法內進行處理,就會退出方法,方法在執行程序總拋出例外時的例外處理,儲存在一個例外處理表,方法在發生例外時候找到處理例外的代碼
-
無論通過哪種方式退出,在方法退出后都回傳到該方法被呼叫的位置,方法正常退出時,呼叫者的pc計數器的值作為回傳地址,即呼叫該方法的指令的下一條指令的地址,而通過例外退出的,回傳地址是要通過例外表來確定,堆疊幀中一般不會保存這部分資訊
-
本質上,方法的退出就是當前堆疊幀出堆疊的程序,此時,需要恢復上層方法的資料區等資訊,讓呼叫者方法繼續執行下去
-
正常完成出口和例外完成出口的區別在于,通過例外完成出口推出的不會給他的上層呼叫者產生任何的回傳值,
本地方法堆疊
簡介
本地方法是指使用了native關鍵字修飾的方法,沒有方法體,是為了對接其他的編程語言,
普通的方法都是在JVM堆疊中進行管理,但是本地方法是在本地方法堆疊中進行管理,
- 當某個執行緒呼叫一個本地方法時,它就進入了一個全新的并且不再受虛擬機限制的世界,它和虛擬機擁有同樣的權限,
- 本地方法可以通過本地方法介面來訪問虛擬機內部的運行時資料區,它甚至可以直接使用本地處理器中的暫存器
- 直接從本地記憶體的堆中分配任意數量的記憶體,
- 并不是所有的JVM都支持本地方法,因為Java虛擬機規范并沒有明確要求本地方法堆疊的使用語言、具體實作方式、資料結構等,如果JVM產品不打算支持native方法,也可以無需實作本地方法堆疊,
- 在Hotspot JVM中,直接將本地方法堆疊和虛擬機堆疊合二為一
堆(重點)
簡介
- 每個JVM中只有一個堆,也是JVM最大記憶體區域
- 堆大小可調節也可固定
- 在物理記憶體上可以不連續,但是邏輯上是連續的
- 所有的執行緒共享堆,但是也可以劃出執行緒私有的緩沖區,稱為Thread Local Allocation Buffer,簡寫TLAB
- 《Java虛擬機規范》中對Java堆的描述是:所有的物件實體以及陣列都應當在運行時分配在堆上,從實際使用角度看,陣列和物件可能永遠不會存盤在堆疊上,因為堆疊幀中保存參考,這個參考指向物件或者陣列在堆中的位置,
- 在方法結束后,堆中的物件不會馬上被移除,僅僅在垃圾收集的時候才會被移除,
- 堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點 區域,
堆疊-堆-方法區三者聯系如圖
堆的記憶體細分
Java7之前有三部分:新生代 Young Generation Space, 老年代 Old Generation Space ,永久代 Perment Space
Java8及其之后---新生代, 老年代,元空間 Meta Space
具體叫法:
新生代-新生區-年輕代
養老區-老年區-老年代
永久代-永久區
堆大小設定
Java堆區用于存盤Java物件實體,那么堆的大小在JVM啟動時就已經設定好了,大家可以通過選項"—Xmx"和"—Xms"來進行設定,
“-Xms”用于表示堆區的起始記憶體,等價于-XX:InitialHeapSize
“-Xmx”則用于表示堆區的最大記憶體,等價于-XX:MaxHeapSize
一旦堆區中的記憶體大小超過“-Xmx”所指定的最大記憶體時,將會拋出OutOfMemoryError例外,
通常會將-Xms 和-Xmx兩個引數配置相同的值,其目的是為了能夠在java垃圾回識訓制清理完堆區后不需要重新分隔計算堆區的大小,從而提高性能,
默認情況下,初始記憶體大小:物理電腦記憶體大小/64,最大記憶體大小:物理電腦記憶體大小/4
堆空間的內部細節
堆分為兩個代際區域,新生代和老年代,新生代和老年代的大小比例為1:2,
新生代中各個區域的記憶體比例為Eden:s0:s1為8:1:1,
注意的是這只是JVM的默認比例,各個區域的比例和大小以及是否固定,都可以在VM Option中進行設定,但是大小會動態調整,所以需要手動設定引數,但一般不設定,
二者在邏輯上是連續的,但是實際記憶體中可以不連續,
新生代,新生代包括以下部分:
-
Eden區(伊甸園區)
-
Survivors0區(s0區)
-
Survivors1區(s1區)
可以使用visualvm來查看JVM堆疊的具體情況
物件的分配細節
為新物件分配記憶體是一件非常嚴謹和復雜的任務,JVM的設計者們不僅需要考慮記憶體如何分配、在哪里分配等問題,并且由于記憶體分配演算法與記憶體回收演算法密切相關,所以還需要考慮GC執行完記憶體回收后是否會在記憶體空間中產生記憶體碎片,
一般情況
- new的物件先放伊甸園區,此區有大小限制,
- 當伊甸園的空間填滿時,程式又需要創建物件,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不再被其他物件所參考的物件進行銷毀,再加載新的物件放到伊甸園區
- 然后將伊甸園中的剩余物件移動到幸存者0區,
- 如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者0區的,如果沒有回收,就會放到幸存者1區,
- 如果再次經歷垃圾回收,此時會重新放回幸存者0區,接著再去幸存者1區,6.啥時候能去養老區呢?可以設定次數,默認是15次,
特殊情況
如圖所示,小總結:
- 物件大于Eden的存放順序:Eden->old,物件不大于Survivor為:Eden->Survivor->old
- 放不下時會進行GC,至于是哪種GC要看哪個區域大小不夠,如果是Eden區,進行Minor GC,Old區進行Major GC,
- Survivor區滿了不會自動觸發垃圾回收,只有在Eden區不夠用了,進行 Minor GC的時候,對Eden和Survivor進行回收,只有Eden區才能自動觸發Minor GC總結,
頻繁在新生代進行回收,較少在老年代回收,幾乎不在元空間回收,
堆為什么要分代
研究發現,幾乎80%的物件的都是臨時物件,
為了優化GC性能,所以需要分代,將物件按照存活時間的不同進行劃分,那么在進行垃圾回收的時候就可以重點關注存活時間短的區域了,如果沒有分代,那么每次進行垃圾回收都會判斷全部物件是否為垃圾,這些判斷多了,性能也下降了,
TLAB
TLAB存在的原因
為了提高性能
解釋:
- 堆區是執行緒共享區域,任何執行緒都可以訪問到堆區中的共享資料
- 由于物件實體的創建在JVM中非常頻繁,因此在并發環境下從堆區中劃分記憶體空間是執行緒不安全的為避免多個執行緒操作同一地址,需要使用加鎖等機制,進而影響分配速度,
什么是TLAB
從記憶體的角度來看,是一塊執行緒私有的緩沖區域,位于Eden區,
解釋:
-
從記憶體模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM為每個執行緒分配了一個私有快取區域,它包含在Eden空間內,
-
多執行緒同時分配記憶體時,使用TLAB可以避免一系列的非執行緒安全問題,同時還能夠提升記憶體分配的吞吐量,因此我們可以將這種記憶體分配方式稱之為快速分配策略,
-
據我所知所有OpenJDK衍生出來的JVM都提供了TLAB的設計,
圖示
進一步說明
盡管不是所有的物件實體都能夠在TLAB中成功分配記憶體,但JVM確實是將TLAB作為記憶體分配的首選
在程式中,開發人員可以通過選項“-XXUseTLAB”設定是否開啟TLAB空間,
默認情況下,TLAB空間的記憶體非常小,僅占有整個Eden空間的1%,當然我們可以通過選項
“-XX:TLABWasteTargetPercent”設定TLAB空間所占用Eden空間的百 分比大小,
一旦物件在TLAB空間分配記憶體失敗時,JVM就會嘗試著通過使用加鎖機制確保資料操作的原子性,從而直接在Eden空間中分配記憶體,
JVM引數總結
官網說明
JVM引數官網
具體的引數
-XX:+PrintGCDetails
? Enables printing of detailed messages at every GC. By default, this option is disabled.
? 列印每次GC的細節,默認關閉
-Xms:初始堆空間記憶體(默認為物理記憶體的1/64)
-Xmx:最大堆空間記憶體(默認為物理記憶體的1/4)
-Xmn:設定新生代的大小,(初始值及最大值)
-XX:NewRatio:配置新生代與老年代在堆結構的占比
-XX:SurvivorRatio:設定新生代中Eden和SO/S1空間的比例
-XX:MaxTenuringThreshold:設定新生代垃圾的最大年齡
其他說明
在JDK6之后,只要老年代的連續空間大于新生代物件總大小或者歷次晉升的平均大小就會進行Minor GC,否則就進行Full GC,
逃逸分析技術
概述
堆是否是物件分配的唯一選擇:是,但是有逃逸分析技術,但是HotSpot并沒有使用基于逃逸分析技術的代碼優化
如何將堆上的物件分配到堆疊,需要使用逃逸分析手段,
這是一種可以有效減少Java 程式中同步負載和記憶體堆分配壓力的演算法,
通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的物件的參考的使用范圍從而決定是否要將這個物件分配到堆上,
逃逸分析的基本行為就是分析物件動態作用域:
- 當一個物件在方法中被定義后,物件只在方法內部使用,則認為沒有發生逃逸,
- 當一個物件在方法中被定義后,它被外部方法所參考,則認為發生逃逸,例如作為呼叫引數傳遞到其他地方中,
總結:能使用區域變數的就不要在方法外定義,
具體引數
-XX:+DoEscapeAnalysis 顯式開啟逃逸分析,在JDK6后默認開啟
-XX:+PrintEscapeAnalysis 查看逃逸分析的篩選結果,
基于逃逸分析技術的代碼優化
堆疊上分配
將堆分配轉化為堆疊分配,
JIT編譯器在編譯期間根據逃逸分析的結果,發現如果一個物件并沒有逃逸出方法的話,就可能被優化成堆疊上分配,分配完成后,繼續在呼叫堆疊內執行,I最后執行緒結束,堆疊空間被回收,區域變數物件也被回收,這樣就無須進行垃圾回收了,
常見的堆疊上分配的場景:
給成員變數賦值、方法回傳值、實體參考傳遞,
同步省略
如果一個物件被發現只能從一個執行緒被訪問到,那么對于這個物件的操作可以不考慮同步,即使代碼中有同步塊,但是如果該代碼只能被一個執行緒發現,那么在運行階段此同步代碼塊會被取出,
在動態編譯同步塊的時候,JIT編譯器可以借助逃逸分析來判斷同步塊所使用的鎖物件是否只能夠被一個執行緒訪問而沒有被發布到其他執行緒,如果沒有,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步,這樣就能大大提高并發性和性能,這個取消同步的程序就叫同步省略,也叫鎖消除,
以下代碼便是同步省略的例子,因為變數hollis是在方法f中宣告的,那么只有宣告該方法的執行緒才能訪問,因為hollis是區域變數,區域變數保存在堆疊幀中,堆疊幀是執行緒私有的,
public void f(){
Object hollis = new Object();
synchronized(hollis){
System.out.println(hollis);
}
}
分離物件或標量替換
聚合量:可以被分解為更小的資料和其他的聚合量,例如物件
標量:不能被拆分的資料,例如基本資料型別
有的物件可能不需要作為一個連續的記憶體結構存在也可以被訪問到,那么物件的部分(或全部)可以不存盤在記憶體,而是存盤在堆疊中,
標量(Scalar)是指一個無法再分解成更小的資料的資料,Java中的原始資料型別就是標量,相對的,那些還可以分解的資料叫做聚合量(Aggregate),Java中的物件就是聚合量,因為他可以分解成其他聚合量和標量,
在JIT階段,如果經過逃逸分析,發現一個物件不會被外界訪問的話,那么經過JIT優化,就會把這個物件拆解成若干個其中包含的若干個成員變數來代替,這個程序就是標量替換,
經過標量替換前的alloc方法
public static void main(String[] args) { alloc();
}
private static void alloc(){
Point point = new Point (1,2);
System.out.printin("point.x="+point.x+"; point.y="+point.y);
}
class Point{
private int x;
private int y;
}
經過標量替換后的alloc方法
private static void alloc(){
int x=1;
int y=2;
System.out.printin("point.x="+x+"; point.y="+y);
}
可以看到,Point這個聚合量經過逃逸分析后,發現他并沒有逃逸,就被替換成兩個聚合量了,那么標量替換有什么好處呢?就是可以大大減少堆記憶體的占用,因為一旦不需要創建物件了,那么就不再需要分配堆記憶體了,
標量替換為堆疊上分配提供了很好的基礎,
具體引數
-XX:+EliminateAllocations:開啟了標量替換(默認打 開),允許將物件打散分配在堆疊上,
逃逸分析技術總結
該技術到目前為止沒有特別成熟,
其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗,雖然經過逃逸分析可以做標量替換、堆疊上分配、和鎖消除,但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的程序,
一個極端的例子,就是經過逃逸分析之后,發現沒有一個物件是不逃逸的,那這個逃逸分析的程序就白白浪費掉了,
雖然這項技術并不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段,
注意到有一些觀點,認為通過逃逸分析,JVM會在堆疊上分配那些不會逃逸的物件,這在理論上是可行的,但是取決于JVM設計者的選擇,
Oracle HotspotJVM中并未這么做,這一點在逃逸分析相關的檔案里已經說明,所以可以明確所有的物件實體都是創建在堆上,
目前很多書籍還是基于JDK 7以前的版本,JDK已經發生了很大變化,intern字串的快取和靜態變數曾經都被分配在永久代上,而永久代已經被元資料區取代,但是,intern字串快取和靜態變數并不是被轉移到元資料區,而是直接在堆上分配,
方法區
官方檔案
下圖是堆疊、堆、方法區的關系,
堆疊保存著的是區域變數
堆保存著變數對應的資料
方法區保存著變數型別的資訊

方法區 在哪個部分
JVM規范指出:方法區在邏輯上屬于堆,但是實際的具體的JVM中并不屬于堆的一部分,
基本理解
- 方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,
- 元空間,永久代是方法區的一個實作
- 方法區在JVM啟動的時候被創建,并且它的實際的物理記憶體空間中和Java堆區一樣都可以是不連續的,
- 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴展,
- 方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機同樣會拋出記憶體溢位錯誤:java.lang.OutofMemoryError: PermGen space 或者 java.lang.OutOfMemoryError: Metaspace
- 例如加載大量第三方的jar包,Tomcat部署過多的工程(30-50),大量的動態生成反射類,
- 關閉JVM就會釋放這個區域的記憶體,
HotSpot中方法區的演進
在jdk7及以前,習慣上把方法區,稱為永久代,jdk8開始,使用元空間取代了永久代,
In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.
本質上,方法區和永久代并不等價,僅是對hotspot而言的,《Java虛擬機規范》
對如何實作方法區,不做統一要求,例如:BEA JRockit/ IBM J9中不存在永久代的概念,
元空間的本質和永久代類似,都是對JVM規范中方法區的實作,
元空間和永久代區別
元空間不在虛擬機設定的記憶體中,而是使用本地記憶體,
永久代、元空間二者并不只是名字變了,內部結構也調整了,
根據《Java虛擬機規范》的規定,如果方法區無法滿足新的記憶體分配需求時,將拋出OOM例外,
設定方法區的大小與OOM
方法區大小可以動態調整,JDK8之前和之后有所不同,這里只介紹JDK8及其之后的引數設定
元資料區大小可以使用引數-XX:MetaspaceSize和-XX:MaxMetaspaceSize指定,設定方法區大小,
默認值依賴于平臺,windows下,-XX:MetaspaceSize是21M,如果- XX:MaxMetaspaceSize 的值是-1,即沒有限制,
方法區存盤內容
《深入理解Java 虛擬機》書中對方法區(Method Area)存盤內容描述如下:
它用于存盤已被虛擬機加載的型別資訊、常量、靜態變數、即時編譯器編譯后的代碼快取等,
型別資訊
對每個加載的型別(類class、介面interface、列舉enum、注解annotation),JVM必須在方法區中存盤以下型別資訊:
①這個型別的完整有效名稱(全名=包名.類名)
②這個型別直接父類的完整有效名(對于interface或是java.lang.object,都沒有父類)
③這個型別的修飾符(public,abstract,final的某個子集)
④這個型別直接介面的一個有序串列
可以看作是型別所有靜態資訊
域(Field)資訊
JVM必須在方法區中保存型別的所有域的相關資訊以及域的宣告順序,
域的相關資訊包括:域名稱、域型別、域修飾符(public, private,
protected,static,final,volatile, transient的某個子集)
方法(Method)資訊
JVM必須保存所有方法的以下資訊,同域資訊一樣包括宣告順序:
- 方法名稱
- 方法的回傳型別(或 void)
- 方法引數的數量和型別(按順序)
- 方法的修飾符(public,private, protected, static等)
- 方法的位元組碼(bytecodes)、運算元堆疊、區域變數表及大小(abstract和native方法除外)
- 例外表 abstract和native方法除外)
每個例外處理的開始位置,結束位置,等資訊
non-final的類變數
-
靜態變數和類關聯在一起,隨著類的加載而加載,它們成為類資料在邏輯上的一部分,
-
類變數被類的所有實體共享,即使沒有類實體時你也可以訪問它,
運行時常量池(重要)
簡介
方法區,內部包含了運行時常量池, 位元組碼檔案,內部包含了常量池,
要弄清楚方法區,需要理解清楚ClassFile,因為加載類的資訊都在方法區, 要弄清楚方法區的運行時常量池,需要理解清楚ClassFile中的常量池,
ClassFile中的常量池是保存著靜態的資訊,包括數值,方法的符號參考,類參考,字串值,欄位參考,ClassFile中的常量池可以看作一張表,虛擬機指令通過這張表找到要執行的類名,方法名,引數名,引數型別,字面量等資訊,
常量池表(Constant Pool Table)是Class檔案的一部分,用于存放編譯期生成的各種字面量與符號參考,這部分內容將在類加載后存放到方法區的運行時常量池中,
運行時常量池,在加載類和介面到虛擬機后,就會創建對應的運行時常量池,
JVM為每個已加載的型別(類或介面)都維護一個常量池,池中的資料項像陣列項一樣,是通過索引訪問的,
運行時常量池,相對于Class檔案常量池的另一重要特征是:具備動態性,
java規范并不要求常量只能在運行時才產生,也就是說運行時常量池的內容并不全部來自class常量池,在運行時可以通過代碼生成常量并將其放入運行時常量池中,這種特性被用的最多的就是String.intern(),
運行時常量池類似于傳統編程語言中的符號表(symbol table),但是它所包含的資料卻比符號表要更加豐富一些,
總結:常量池和運行時常量池就像是類和實體的關系,常量池是類,運行時常量池是物件,運行時常量池存盤著一些真正的可以執行的東西
方法區的演變細節
| jdk1.6及之前 | 有永久代,靜態變數放在永久代上 |
|---|---|
| jdk1.7 | 有永久代,但在去永久代化,字串常量池,靜態變數被移除,保存到堆中 |
| jdk1.8及之后 | 無永久代,型別資訊,欄位,方法,常量保存在本地記憶體的元空間,但字串常量池,靜態變數還是在堆 |
為什么要將永久代替換為元空間
-
為永久代設定大小是很難確定的,如果類很多,過小會OOM,過大會導致浪費
-
很難對永久代進行調優
StringTable(字串常量池)為什么要放到堆
jdk7將StringTable放到堆中,因為永久代的垃圾回收效率低,就導致StringTable回收效率低,就會占用很多永久代的記憶體,如果放到堆中,就可以即使回收了
方法區的GC
方法區的垃圾回收主要回收兩部分:常量池廢棄的常量和不再使用的型別,
判定一個常量是否“廢棄”還是相對簡單,
而要判定一個型別是否屬于“不再被使用的類”的條件就比較苛刻了,需要同時滿足下面三個條件:
-
該類所有的實體都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實體,
-
加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的,
-
該類對應的java.lang.Class物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554479.html
標籤:其他
下一篇:返回列表
