主頁 > 後端開發 > JVM詳記

JVM詳記

2023-02-01 06:53:21 後端開發

JVM

在這里插入圖片描述

1 運行時資料區域

從概念上Java虛擬機在執行Java程式的程序中會把它所管理的記憶體劃分為若干個不同的資料區域,
在這里插入圖片描述
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ioqwiGjl-1675133822187)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129154559939.png)]

在Java8中,元空間(Metaspace)登上舞臺,方法區存在于元空間(Metaspace),同時,元空間不再與堆連續,而且是存在于本地記憶體(Native memory),

方法區Java8之后的變化:

  • 移除了永久代(PermGen),替換為元空間(Metaspace)
  • 永久代中的class metadata(類元資訊)轉移到了native memory(本地記憶體,而不是虛擬機)
  • 永久代中的interned Strings(字串常量池) 和 class static variables(類靜態變數)轉移到了Java heap
  • 永久代引數(PermSize、MaxPermSize)-> 元空間引數(MetaspaceSize、MaxMetaspaceSize)

Java8為什么要將永久代替換成Metaspace?

  • 字串存在永久代中,容易出現性能問題和記憶體溢位,
  • 類及方法的資訊等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現永久代溢位,太大則容易導致老年代溢位,
  • 永久代會為 GC 帶來不必要的復雜度,并且回收效率偏低,
  • Oracle 可能會將HotSpot 與 JRockit 合二為一,JRockit沒有所謂的永久代,

1.1 程式計數器

程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器,位元組碼解釋器作業時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的指示器,分支、回圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成,為了執行緒切換后能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器, 這類記憶體區域稱為“執行緒私有”的記憶體,

如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機位元組碼指令的地址;如果正在執行的是本地(Native)方法,這個計數器值則應為空(Undefined), 此記憶體區域是唯一在《Java虛擬機規范》中沒有規定任何OutOfMemoryError情況的區域,

1.2 Java虛擬機堆疊

Java虛擬機堆疊(Java Virtual Machine Stack)也是執行緒私有的,它的生命周期與執行緒相同 , 虛擬機堆疊描述的是Java方法執行的執行緒記憶體模型:每個方法被執行的時候,Java虛擬機都會同步創建一個堆疊幀(Stack Frame)用于存盤區域變數表[1]、運算元堆疊、動態連接、方法出口等資訊,每一個方法被呼叫直至執行完畢的程序,就對應著一個堆疊幀在虛擬機堆疊中從入堆疊到出堆疊的程序,

-Xss 為jvm啟動的每個執行緒分配的記憶體大小,默認JDK1.4中是256K,JDK1.5+中是1M
在這里插入圖片描述

運行時堆疊幀結構

Java虛擬機以方法作為最基本的執行單元,“堆疊幀”(Stack Frame)則是用于支持虛擬機進行方法呼叫方法執行背后的資料結構,它也是虛擬機運行時資料區中的虛擬機堆疊(Virtual Machine Stack)的堆疊元素,

在編譯Java程式原始碼的時候,堆疊幀中區域變數表大小及運算元堆疊深度,就已經被分析計算出來,并且寫入到方法表的Code屬性之中,

對于執行引擎來講,在活動執行緒中,只有位于堆疊頂的方法才是在運行的,只有位于堆疊頂的堆疊幀才是生效的,其被稱為“當前堆疊幀”(Current Stack Frame),與這個堆疊幀所關聯的方法被稱為“當前方法”(Current Method),執行引擎所運行的所有位元組碼指令都只 針對當前堆疊幀進行操作,

區域變數表

區域變數表(Local Variables Table)是一組變數值的存盤空間,用于存放方法引數和方法內部定義的區域變數,在Java程式被編譯為Class檔案時,就在方法的Code屬性的max_locals資料項中確定了該方法所需分配的區域變數表的最大容量,

區域變數表的容量以變數槽(Variable Slot)為最小單位,《Java虛擬機規范》中很有導向性地說到每個變數槽都應該能存放一個boolean、 byte、char、short、int、float、reference[2]或returnAddress[3]型別的資料,這8種資料型別,都可以使用32位或更小的物理記憶體來存盤,在64位虛擬機中使用了64位的物理記憶體空間去實作一個變數槽,虛擬機仍要使用對齊和補白的手段讓變數槽在外觀上看起來與32位虛擬機中的一致,Java語言中明確的64位的資料型別只有long和double兩種,由于區域變數表是建立在執行緒堆疊中的,屬于執行緒私有的資料,無論讀寫兩個連續的變數槽是否為原子操作,都不會引起資料競爭和執行緒安全問題,

Java虛擬機通過索引定位的方式使用區域變數表,索引值的范圍是從0開始至區域變數表最大的變數槽數量,如果訪問的是32位資料型別的變數,索引N就代表了使用第N個變數槽,如果訪問的是64位 資料型別的變數,則說明會同時使用第N和N+1兩個變數槽,虛擬機不允許采用任何方式單獨訪問其中的某一個,

當一個方法被呼叫時,Java虛擬機會使用區域變數表來完成引數值到引數變數串列的傳遞程序, 即實參到形參的傳遞,如果執行的是實體方法(沒有被static修飾的方法),那區域變數表中第0位索引的變數槽默認是用于傳遞方法所屬物件實體的參考,在方法中可以通過關鍵字“this”來訪問到這個隱含的引數,其余引數則按照引數表順序排列,占用從1開始的區域變數槽,引數表分配完畢后,再根據方法體內部定義的變數順序和作用域分配其余的變數槽,

變數槽是可以重用的,方法體中定義的變數,其作用域并不一定會覆寫整個方法體,如果當前位元組碼PC計數器的值已經超出了某個變數的作用域,那這個變數對應的變數槽就可以交給其他變數來重用,這樣的設計可以節省堆疊幀空間,但在某些情況下變數槽的復用會直接影響到系統的垃圾收集行為,

運算元堆疊

運算元堆疊(Operand Stack)也常被稱為操作堆疊,它是一個后入先出(Last In First Out,LIFO)堆疊,運算元堆疊的最大深度也在編譯的時候被寫入到Code屬性的max_stacks資料項之中,運算元堆疊的每一個元素都可以是包括long和double在內的任意Java資料型別,32位資料型別所占的堆疊容量為1,64位資料型別所占的堆疊容量為2,Javac編譯器的資料流分析作業保證了在方法執行的任何時候,運算元堆疊的深度都不會超過在max_stacks資料項中設定的最大值,

隨著方法執行和位元組碼指令的執行,會從區域變數表或物件實體的欄位中復制常量或變數寫入到運算元堆疊,再隨著計算的進行將堆疊中元素出堆疊到區域變數表或者 回傳給方法呼叫者,也就是出堆疊/入堆疊操作,

大多虛擬機的實作里都會令兩個堆疊幀出現一部分重疊,讓下面堆疊幀的部分運算元堆疊與上面堆疊幀的部分區域變數表重疊在一起,這樣做不僅節約了一些空間,更重要的是在進行方法呼叫時就可以直接共用一部分資料,無須進行額外的引數復制傳遞了,

動態連接

每個堆疊幀都包含一個指向運行時常量池中該堆疊幀所屬方法的參考,持有這個參考是為了支持方法呼叫程序中的動態連接(Dynamic Linking),Class檔案的常量池中存有大量的符號參考,位元組碼中的方法呼叫指令就以常量池里指向方法的符號參考作為引數,這些符號 參考一部分會在類加載階段或者第一次使用的時候就被轉化為直接參考,這種轉化被稱為靜態決議, 另外一部分將在每一次運行期間都轉化為直接參考,這部分就稱為動態連接,

方法回傳地址

在方法退出之后,都必須回傳到最初方法被呼叫時的位置,程式才能繼續執行,方法回傳時可能需要在堆疊幀中保存一些資訊,用來幫助恢復它的上層主調方法的執行狀態, 一般來說,方法正常退出時,主調方法的PC計數器的值就可以作為回傳地址,堆疊幀中很可能會保存這 個計數器值,而方法例外退出時,回傳地址是要通過例外處理器表來確定的,堆疊幀中就一般不會保存這部分資訊,

當一個方法開始執行后,只有兩種方式退出這個方法,

  • 執行引擎遇到任意一個方法回傳的位元組碼指令,這時候可能會有回傳值傳遞給上層的方法呼叫者,方法是否有回傳值以及回傳值的型別將根據遇到何種方法回傳指令來決定,稱為“正常呼叫完成”(Normal Method Invocation Completion),
  • 方法執行的程序中遇到了例外,并且這個例外沒有在方法體內得到妥善處理,只要在本方法的例外表中沒有搜索到匹配的例外處理器,就會導致方法退出,稱為“例外呼叫完成(Abrupt Method Invocation Completion)”,一個方法使用例外完成出口的方式退出,是不會給它的上層呼叫者提供任何回傳值的,

在《Java虛擬機規范》中,對這個記憶體區域規定了兩類例外狀況:如果執行緒請求的堆疊深度大于虛擬機所允許的深度,將拋出StackOverflowError例外;如果Java虛擬機堆疊容量可以動態擴展,當堆疊擴展時無法申請到足夠的記憶體會拋出OutOfMemoryError例外,

1.3 本地方法堆疊

本地方法堆疊(Native Method Stacks)與虛擬機堆疊所發揮的作用是非常相似的,其區別只是虛擬機堆疊為虛擬機執行Java方法(也就是位元組碼)服務,而本地方法堆疊則是為虛擬機使用到的本地(Native) 方法服務,

1.4 Java堆

Java堆(Java Heap)是虛擬機所管理的記憶體中最大的一塊,Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機啟動時創建, 唯一目的就是存放物件實體, 根據《Java虛擬機規范》的規定,Java堆可以處于物理上不連續的記憶體空間中,但在邏輯上它應該被視為連續的,如果在Java堆中沒有記憶體完成實體分配,并且堆也無法再擴展時,Java虛擬機將會拋出OutOfMemoryError例外,

設定堆空間大小: 初始分配-Xms指定,默認是物理記憶體的1/64;最大分配由-Xmx指定,默認是物理記憶體的1/4

從記憶體回收的角度來看,由于現在收集器基本都采用分代收集演算法,所以Java堆還可以細分為:新生代(青年代)和老年代,

針對具備“朝生夕滅”特點的物件,把新生代分為一塊較大的Eden空間和兩塊較小的 Survivor空間(From Survivor空間、To Survivor空間),每次分配記憶體只使用Eden和其中一塊Survivor,發生垃圾收集時,將Eden和Survivor中仍 然存活的物件一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空 間,HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,也即每次新生代中可用記憶體空間為整個新 生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會被“浪費”的,當Survivor空間不足以容納一次新生代Minor GC之后存活的物件時,就需要依賴其他記憶體區域(實際上大多就是老年代)進行分配擔保(Handle Promotion),

在Java7 Hotspot虛擬機中將Java堆記憶體分為3個部分: 青年代Young Generation、老年代Old Generation、永久代Permanent Generation,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-xU5k0WF5-1675133822188)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129163606426.png)]

在Java8以后,由于方法區的記憶體不在分配在Java堆上,而是存盤于本地記憶體元空間Metaspace中,所以永久代就不存在了,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-f5GnNU3Z-1675133822188)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129163655080.png)]

配置新生代和老年代堆結構占比:

  • 默認 -XX:NewRatio=2 , 標識新生代占1 , 老年代占2 ,新生代占整個堆的1/3,修改占比 -XX:NewPatio=4 , 標識新生代占1 , 老年代占4 , 新生代占整個堆的1/5
  • 默認 -XX:SurvivorRatio=8,標識Eden空間和另外兩個Survivor空間占比分別為8:1:1

分配物件策略

  1. 物件優先在Eden分配

    大多數情況下,物件在新生代中 Eden 區分配,當 Eden 區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC,引數-XX:+UseAdaptiveSizePolicy( Parallel Scavenge收集器,默認開啟),會導致Eden與Survivor區默認8:1:1比例自動變化,

  2. 大物件直接進入老年代

    大物件就是需要大量連續記憶體空間的物件(比如:字串、陣列),如果物件超過 -XX:PretenureSizeThreshold 設定的大小,會直接進入老年代,這個引數只在 Serial 和ParNew兩個收集器下有效,

  3. 長期存活的物件將進入老年代

    物件的GC年齡增加到一定程度 (默認為15歲,CMS收集器默認6歲,不同的垃圾收集器會略微有點不同)就會被晉升到老年代中,可以通過引數 -XX:MaxTenuringThreshold 來設定,

  4. 動態物件年齡判定

    當前放物件的Survivor區域里,一批物件的總大小大于這塊Survivor區域記憶體大小的 50%(-XX:TargetSurvivorRatio可以指定),那么此時大于等于這批物件年齡最大值的物件,就可以直接進入老年代了, 例如Survivor區域里現在有一批物件,年齡1+年齡2+年齡n的多個年齡物件總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的物件都放入老年代,這個規則其實是希望那些可能是長期存活的物件,盡早進入老年代,物件動態年齡判斷機制一般是在minor gc之后觸發的,

  5. 空間分配擔保機制

    年輕代每次minor gc之前,JVM都會檢查下老年代最大可用的連續空間是否大于年輕代里現有的所有物件大小之和(包括垃圾物件) ,如果這個條件成立,那這一次Minor GC可以確保是安全的,如果不成立,則虛擬機會先查看XX:HandlePromotionFailure引數的設定值是否允許擔保失敗(Handle Promotion Failure),如果允許:那會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代物件的平均大小,如果大于,將嘗試進行一次Minor GC;如果小于,那就要改為進行一次Full GC,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4iazVDYT-1675133822188)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129184955524.png)]

1.5 方法區

方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用于存盤已被虛擬機加載的型別資訊、常量、靜態變數、即時編譯器編譯后的代碼快取等資料,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-RYK1vWb3-1675133822189)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129164350780.png)]

方法區結構

在這里插入圖片描述

  • 型別資訊 :對每個加載的型別(類Class、介面 interface、列舉enum、注解 annotation),JVM必須在方法區中存盤以下型別信 息:
    1. 這個型別的完整有效名稱(全名 = 包名.類名)
    2. 這個型別直接父類的完整有效名(對于 interface或是java.lang. Object,都沒有父類)
    3. 這個型別的修飾符( public, abstract,final的某個子集)
    4. 這個型別直接介面的一個有序串列 域資訊
  • 域資訊:即為類的屬性,成員變數, JVM必須在方法區中保存類所有的成員變數相關資訊及宣告順序, 域的相關資訊包括:域名稱、域型別、域修飾符(pυblic、private、protected、static、final、volatile、transient的某個子集)
  • 方法資訊:JVM必須保存所有方法的以下資訊,同域資訊一樣包括宣告順序:
    1. 方法名稱方法的回傳型別(或void)
    2. 方法引數的數量和型別(按順序)
    3. 方法的修飾符public、private、protected、static、final、synchronized、native,、abstract的一個子集
    4. 方法的位元組碼bytecodes、運算元堆疊、區域變數表及大小( abstract和native方法除外)
    5. 例外表( abstract和 native方法除外),每個例外處理的開始位置、結束位置、代碼處理在程式計數器中的偏移地址、被捕獲的例外類的常量池索引,

方法區設定:

jdk7及以前通過-xx:Permsize來設定永久代初始分配空間,默認值是20.75M ;-XX:MaxPermsize來設定永久代最大可分配空間,32位機器默認是64M,64位機器模式是82M,當JVM加載的類資訊容量超過了這個值,會報例外OutofMemoryError:PermGen space,

JDK8以后元資料區大小可以使用引數 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定 默認值依賴于平臺,windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,即沒有限制,

在JDK7的HotSpot,已經把原本放在永久代的字串常量池、靜態變數等移至Java堆中,而到了JDK 8,完全廢棄了永久代的概念,改用在本地記憶體中實作的元空間(Meta-space)來代替,把JDK7中永久代還剩余的內容(主要是型別資訊)全部移到元空間中,

如果方法區無法滿足新的記憶體分配需求時,將拋出 OutOfMemoryError例外,

運行時常量池

常量池:存放編譯期間生成的各種字面量與符號參考

運行時常量池:常量池表在運行時的表現形式

運行時常量池(Runtime Constant Pool)是方法區的一部分,Class檔案中除了有類的版本、字 段、方法、介面等描述資訊外,還有一項資訊是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量[4](Literal)與符號參考[5](Symbolic References),這部分內容將在類加載后存放到方法區的運行時常量池中,這些常量池現在是靜態資訊,只有到運行時被加載到記憶體后,這些符號才有對應的記憶體地址資訊,這些常量池一旦被裝入記憶體就變成運行時常量池,對應的符號參考在程式加載或運行時會被轉變為被加載到記憶體區域的代碼的直接參考,也 就是我們說的動態鏈接了,例如,compute()這個符號參考在運行時就會被轉變為compute()方法具體代碼在記憶體中的地址,主要通過物件頭里的型別指標去轉換直接參考,

但對于運行時常量池,《Java虛擬機規范》并沒有做任何細節的要求,一般來說,除了保存Class檔案中描述的符號參考外,還會把由符號參考翻譯出來的直接參考也存盤在運行時常量池中,

運行時常量池相對于Class檔案常量池的另外一個重要特征是具備動態性,Java語言并不要求常量一定只有編譯期才能產生,也就是說,并非預置入Class檔案中常量池的內容才能進入方法區運行時常量池,運行期間也可以將新的常量放入池中,這種特性利用得比較多的便是String類的 intern()方法,

字串常量池

字串的分配,和其他的物件分配一樣,耗費高昂的時間與空間代價,作為最基礎的資料型別,大量頻繁的創建字串,會極大程度地影響程式的性能,

JVM為了提高性能和減少記憶體開銷,在實體化字串常量的時候進行了一些優化:

  • 為字串開辟一個字串常量池,類似于快取區
  • 創建字串常量時,首先查詢字串常量池是否存在該字串
  • 存在該字串,回傳參考實體,不存在,實體化該字串并放入池中

三種字串操作(Jdk1.7 及以上版本):

  • 直接賦值字串

    String s = "z"; // s指向常量池中的參考

    這種方式創建的字串物件,只會在常量池中, 因為有"z"這個字面量,創建物件s的時候,JVM會先去常量池中通過 equals(key) 方法,判斷是否有相同的物件 如果有,則直接回傳該物件在常量池中的參考; 如果沒有,則會在常量池中創建一個新物件,再回傳參考,

  • new String();

    String s1 = new String("z"); // s1指向記憶體中的物件參考

    這種方式會保證字串常量池和堆中都有這個物件,沒有就創建,最后回傳堆記憶體中的物件參考, 步驟大致如下: 因為有"z"這個字面量,所以會先檢查字串常量池中是否存在字串"z" 不存在,先在字串常量池里創建一個字串物件;再去記憶體中創建一個字串物件"z"; 存在的話,就直接去堆記憶體中創建一個字串物件"z"; 最后,將記憶體中的參考回傳,

  • intern方法

    String s1 = new String("zhuge");

    String s2 = s1.intern();

    System.out.println(s1 == s2); //false

    String中的intern方法是一個 native 的方法,當呼叫 intern方法時,如果池已經包含一個等于此String物件的字串 (用equals(oject)方法確定),則回傳池中的字串,否則,將intern回傳的參考指向當前字串 s1(jdk1.6版本需要將 s1 復制到字串常量池里),

字串常量池位置 :

  • Jdk1.6及之前: 有永久代, 運行時常量池在永久代,運行時常量池包含字串常量池
  • Jdk1.7:有永久代,但已經逐步“去永久代”,字串常量池從永久代里的運行時常量池分離到堆里
  • Jdk1.8及之后: 無永久代,運行時常量池在元空間,字串常量池里依然在堆里

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fiKtzzqk-1675133822189)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129173011658.png)]

2 類加載機制

Java虛擬機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換決議和初始化,最終形成可以被虛擬機直接使用的Java型別,這個程序被稱作虛擬機的類加載機制,型別的加載連接初始化程序都是在程式運行期間完成的,這種策略為Java應用提供了極高的擴展性和靈活性,Java動態擴展的語言特性就是依賴運行期動態加載和動態連接這個特點實作的,

類被加載到方法區中后主要包含運行時常量池、型別資訊、欄位資訊、方法資訊、類加載器的參考、對應class實體的參考等資訊,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-YKyy8hq9-1675133822190)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129172822997.png)]

2.1類加載的時機

一個型別從被加載到虛擬機記憶體中開始,到卸載出記憶體為止,它的整個生命周期將會經歷加載(Loading)、驗證(Verification)、準備(Preparation)、決議(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準備、決議三個部分統稱為連接(Linking),

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CDtKPuXe-1675133822190)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230123121329269.png)]

加載、驗證、準備、初始化、卸載這五個階段的順序是確定的,而決議階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時系結特性(也稱為動態系結或晚期系結),

有且只有六種情況必須立即對類進行“初始化”(加載、驗證、準備自然需要在此之前開始):

  1. new物件;讀取或設定靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外);呼叫靜態方法
  2. 對型別進行反射呼叫的時候,如果型別沒有進行過初始化,則需要先觸發其初始化,
  3. 當初始化類的時候,其父類還沒有進行過初始化,需要先觸發父類的初始化,
  4. 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類,
  5. 當使用JDK7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實體最后的決議結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種型別的方法句柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化,
  6. 介面中定義了JDK8新加入的默認方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實作類發生了初始化,那該介面要在其之前被初始化,

這六種場景中的行為稱為對一個型別進行主動參考,除此之外,所有參考型別的方式都不會觸發初始化,稱為被動參考

介面與類真正有區別的是一個介面在初始化時,并不要求其父介面全部都完成了初始化,只有在真正使用到父介面的時候(如參考介面中定義的常量)才會初始化,

2.2 類加載的程序

2.2.1 加載

在加載階段,Java虛擬機需要完成以下三件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進制位元組流
  2. 將這個位元組流所代表的靜態存盤結構轉化為方法區的運行時資料結構
  3. 在記憶體中生成一個代表這個java.lang.Class物件,作為方法區這個類的各種資料的訪問入口

2.2.2 驗證

驗證是連接階段的第一步,目的是確保Class檔案的位元組流中包含的資訊符合《Java虛擬機規范》的全部約束要求,保證這些資訊被當作代碼運行后不會危害虛擬機自身的安全,

驗證階段大致上會完成下面四個階段的檢驗動作:檔案格式驗證[6]、元資料驗證[7]、位元組碼驗證[8]、符號參考驗證[9]

可以使用-Xverify:none引數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間,

2.2.3 準備

準備階段正式為類變數(即靜態變數,被static修飾的變數)分配記憶體并設定類變數初始值(資料型別的默認零值,如果類欄位的欄位屬性表中存在ConstantValue屬性(final),就會初始化final所指定的值,),在JDK 7及之前,HotSpot使用永久代來實作方法區時,這些變數所使用的記憶體都應當在方法區中進行分配,JDK7及之后,類變數則會隨著Class物件一起存放在Java中,

這時候進行記憶體分配的僅包括類變數,而不包括實體變數,實體變數將會在物件實體化時隨著一起分配在Java堆中,

2.2.4 決議

決議階段是Java虛擬機將常量池內的符號參考[10](Symbolic References)替換為直接參考[11](Direct References)的程序,決議動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法句柄和呼叫點限定符這7類符號參考進行,在決議階段也對方法或者欄位的可訪問性(public、protected、private、<package>)進行檢查,

在JDK 9之前,Java介面中的所有方法都默認是public的,也沒有模塊化的訪問約束,所以不存在訪問權限的問題,在JDK 9中增加了介面的靜態私有方法,也有了模塊化的訪問約束,所以從JDK 9起,介面方法的訪問也完全有可能因訪問權限控制而出現java.lang.IllegalAccessError例外,

2.2.5 初始化

在初始化階段會根據程式員通程序式編碼制定的主觀計劃去初始化類變數和其他資源,我們也可以從另外一種更直接的形式來表達:初始化階段就是執行類構造器<clinit>()[12]方法的程序,<clinit>()是Javac編譯器的自動生成物,

2.3 類加載器

類加載階段中的“通過一個類的全限定名來獲取描述該類的二進制位元組流”這個動作在Java虛擬機外部去實作,以便讓應用程式自己決定如何去獲取所需的類,實作這個動作的代碼被稱為“類加載器”(Class Loader),

類與類加載器

類加載器只用于實作類的加載動作,對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,每 一個類加載器,都擁有一個獨立的類名稱空間,

比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個 Class檔案,被同一個Java虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等,包括代表類的Class物件的equals()方法、isAssignableFrom()方法、isInstance() 方法的回傳結果,也包括了使用instanceof關鍵字做物件所屬關系判定等各種情況,

站在Java虛擬機的角度來看,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實作,是虛擬機自身的一部分;另外一種就是其他所有的類加載器,這些類加載器都由Java語言實作,獨立存在于虛擬機外部,并且全都繼承自抽象類 java.lang.ClassLoader,

JDK 8及之前版本的三層類加載器:

  • 啟動類加載器(Bootstrap Class Loader):負責加載存放在\lib(JAVA_HOME/jre/lib/rt.jar、 resource.jar或sun.boot.class.path路徑下的內容)目錄,或者被-Xbootclasspath引數所指定的路徑中存放的,而且是Java虛擬機能夠識別的(按照檔案名識別,如rt.jar、tools.jar,名字不符合的類別庫即使放在lib目錄中也不會被加載)類別庫加載到虛擬機的記憶體中,

    啟動類加載器無法被Java程式直接參考, 如果需要把加載請求委派給啟動類加載器去處理,那直接使用null代替即可,

  • 擴展類加載器(Extension Class Loader):這個類加載器是在類sun.misc.Launcher$ExtClassLoader 中以Java代碼的形式實作的,它負責加載\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中所有的類別庫,這是一種Java系統類別庫的擴展機制,JDK的開發團隊允許用戶將具有通用性的類別庫放置在ext目錄里以擴展Java SE的功能,在JDK 9之后,這種擴展機制被模塊化帶來的天然的擴展能力所取代,由于擴展類加載器是由Java代碼實作的,開發者可以直接在程式中使用擴展類加載器來加載Class檔案,

  • 應用程式類加載器(Application Class Loader):這個類加載器由 sun.misc.Launcher$AppClassLoader來實作,由于應用程式類加載器是ClassLoader類中的getSystemClassLoader()方法的回傳值,所以有些場合中也稱它為“系統類加載器”,它負責加載用戶類路徑 (ClassPath)上所有的類別庫,開發者同樣可以直接在代碼中使用這個類加載器,如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中默認的類加載器,

JVM默認使用Launcher的getClassLoader()方法回傳的類加載器AppClassLoader的實體加載我們的應用程式,

當一個ClassLoder裝載一個類時,除非顯示的使用另外一個ClassLoder,否則該類所依賴及參考的類也由這個ClassLoder載入,

雙親委派模型

類加載器之間的層次關系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”,雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器,不過這里類加載器之間的父子關系一般不是以繼承(Inheritance)的關系來實作的,而是通常使用組合(Composition)關系來復用父加載器的代碼,

image.png

雙親委派模型的作業程序:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加 載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去完成加載,

為什么要設計雙親委派機制?

  • 沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心 API庫被隨意篡改
  • 避免類的重復加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一 次,保證被加載類的唯一性

如果自定義類加載器,就需要繼承java.lang.ClassLoader,并重寫findClass(),如果想不遵循雙親委派的類加載順序,還需要重寫loadClass()

執行緒背景關系類加載器 (Thread Context ClassLoader):這個類加載器可以通過java.lang.Thread類的setContext-ClassLoader()方法進行設定,如果創建執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域范圍內都沒有設定過的話,那這個類加載器默認就是應用程式類加載器,這是一種父類加載器去請求子類加載器完成類加載的行為,這種行為實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,

Tomcat打破雙親委派機制

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZIcb3QYn-1675133822191)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129152009730.png)]

從圖中的委派關系中可以看出: CommonClassLoader能加載的類都可以被CatalinaClassLoader和SharedClassLoader使用, 從而實作了公有類別庫的共用,而CatalinaClassLoader和SharedClassLoader自己能加載的類則 與對方相互隔離, WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個WebAppClassLoader 實體之間相互隔離, 而JasperLoader的加載范圍僅僅是這個JSP檔案所編譯出來的那一個.Class檔案,它出現的目的 就是為了被丟棄:當Web容器檢測到JSP檔案被修改時,會替換掉目前的JasperLoader的實體, 并通過再建立一個新的Jsp類加載器來實作JSP檔案的熱加載功能,

3 HotSpot虛擬機物件

3.1 物件的記憶體布局

在HotSpot虛擬機里,物件在堆記憶體中的存盤布局可以劃分為三個部分:物件頭(Header)、實體資料(Instance Data)和對齊填充(Padding),

物件頭(Header)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-484jYX1q-1675133822191)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129174847150.png)]

HotSpot虛擬機物件的物件頭部分包括兩類資訊,

第一類是用于存盤物件自身的運行時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部分資料的長度在32位和64位的虛擬機(未開啟壓縮指標)中分別為32個位元和64個位元,官方稱它 為“Mark Word”, Mark Word被設計成一個有著動態定義的資料結構,以便在極小的空間記憶體儲盡量多的資料,根據物件的狀態復用自己的存盤空間;

物件頭的另外一部分是型別指標,即物件指向它的型別元資料的指標,Java虛擬機通過這個指標來確定該物件是哪個類的實體,

jdk1.6 update14開始,在64bit作業系統中,JVM支持指標壓縮,啟用指標壓縮:-XX:+UseCompressedOops(默認開啟),禁止指標壓縮:-XX:-UseCompressedOops

為什么要進行指標壓縮?
1.在64位平臺的HotSpot中使用32位指標,記憶體使用會多出1.5倍左右,使用較大指標在主記憶體和快取之間移動資料,占用較大寬帶,同時GC也會承受較大壓力,
2.為了減少64位平臺下記憶體的消耗,啟用指標壓縮功能,
3.在jvm中,32位地址最大支持4G記憶體(2的32次方),可以通過對物件指標的壓縮編碼、解碼方式進行優化,使得jvm 只用32位地址就可以支持更大的記憶體配置(小于等于32G),
4.堆記憶體小于4G時,不需要啟用指標壓縮,jvm會直接去除高32位地址,即使用低虛擬地址空間,
5.堆記憶體大于32G時,壓縮指標會失效,會強制使用64位(即8位元組)來對java物件尋址,這就會出現1的問題,所以堆內 存不要大于32G為好

實體資料(Instance Data)

實體資料部分是物件真正存盤的有效資訊,即程式代碼里面所定義的各種型別的欄位內容,無論是從父類繼承下來的,還是在子類中定義的欄位都必須記錄起來,這部分的存盤順序會受到虛擬機分配策略引數(-XX:FieldsAllocationStyle引數)和欄位在Java原始碼中定義順序的影響, HotSpot虛擬機默認的分配順序為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),從以上默認的分配策略中可以看到,相同寬度的欄位總是被分配到一起存 放,在滿足這個前提條件的情況下,在父類中定義的變數會出現在子類之前,如果HotSpot虛擬機的 +XX:CompactFields引數值為true(默認為true),那子類之中較窄的變數也允許插入父類變數的空隙之中,以節省出一點點空間,

對齊填充(Padding)

物件的第三部分是對齊填充,這并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用, HotSpot虛擬機的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍, 如果物件實體資料部分沒有對齊的話,就需要通過對齊填充來補全,

3.2 物件的創建

  1. 類加載檢查

    當Java虛擬機遇到一條位元組碼new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到 一個類的符號參考,并且檢查這個符號參考代表的類是否已被加載、決議和初始化過,如果沒有,那必須先執行相應的類加載程序,

  2. 分配記憶體

    接下來從Java堆中劃分出來一塊確定大小的記憶體,使用指標碰撞[13](Bump The Pointer)或空閑串列[14](Free List)的方式分配給新生物件(類加載完成后可完全確定),選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所采用的垃圾收集器是否帶有空間壓縮整理(Compact)的能力決定,因此,當使用Serial、ParNew等帶壓縮整理程序的收集器時,系統采用的分配演算法是指標碰撞,既簡單又高效;而當使用CMS這種基于清除 (Sweep)演算法的收集器時,理論上就只能采用較為復雜的空閑串列來分配記憶體,

  3. 初始化

    存分配完成之后,虛擬機必須將分配到的記憶體空間(但不包括物件頭)都初始化為零值,如果使用了TLAB的話,這一項作業也可以提前至TLAB分配時順便進行, 這一步操作保證了物件的實體欄位在Java代碼中可以不賦初始值就直接使用,程式能訪問到這些欄位的資料型別所對應的零值,

  4. 設定物件頭

    接下來,Java虛擬機還要對物件進行必要的設定,例如這個物件是哪個類的實體、如何才能找到 類的元資料資訊、物件的哈希碼(實際上物件的哈希碼會延后到真正呼叫Object::hashCode()方法時才計算)、物件的GC分代年齡等資訊,這些資訊存放在物件的物件頭(Object Header)之中,

  5. 建構式

    執行方法 <clinit>()執行方法,即物件按照程式員的意愿進行初始化,對應到語言層面上講,就是為屬性賦值(注意,這與上面的賦零值不同,這是由程式員賦的值),和執行構造方法,

解決并發問題的方法

物件創建在并發情況下并不是執行緒安全的,解決這個問題有兩種可選方案:

  • 對分配記憶體空間的動作進行同步處理:實際上虛擬機是采用CAS配上失敗重試的方式保證更新操作的原子性,
  • 把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩沖(Thread Local Allocation Buffer,TLAB),只有本地緩沖區用完了,分配新的快取區時才需要同步鎖定,虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB引數來設定,

3.3 物件的訪問定位

Java程式會通過堆疊上的reference資料來操作堆上的具體物件,主流的訪問方式主要有使用句柄[15]直接指標[16]兩種,HotSpot主要使用第二種方式進行物件訪問,
句柄:

直接指標:
image.png

4 垃圾收集器

程式計數器、虛擬機堆疊、本地方法堆疊3個區域隨執行緒而生,隨執行緒而滅,因此這幾個區域的記憶體分配和回收都具備確定性, 在這幾個區域內就不需要過多考慮如何回收的問題,當方法結束或者執行緒結束時,記憶體自然就跟隨著回收了,

判斷物件存活演算法

參考計數演算法

在物件中添加一個參考計數器,每當有一個地方 參考它時,計數器值就加一;當參考失效時,計數器值就減一;任何時刻計數器為零的物件就是不可 能再被使用的,

但是單純的參考計數就很難解決物件之間相互回圈參考的問題,它們因為互相參考著對方,導致它們的參考計數都不為零,參考計數演算法也就無法回收它們,

可達性分析演算法

當前主流的記憶體管理子系統,都是通過可達性分析(Reachability Analysis)演算法來判定物件是否存活的,這個演算法的基本思路就是通過 一系列稱為“GC Roots”的根物件作為起始節點集,從這些節點開始,根據參考關系向下搜索,搜索程序所走過的路徑稱為“參考鏈”(Reference Chain),如果某個物件到GC Roots間沒有任何參考鏈相連, 或者用圖論的話來說就是從GC Roots到這個物件不可達時,則證明此物件是不可能再被使用的,

在Java技術體系里面,固定可作為GC Roots的物件包括以下幾種:

  • 在虛擬機堆疊(堆疊幀中的本地變數表)中參考的物件,譬如各個執行緒被呼叫的方法堆疊中使用到的 引數、區域變數、臨時變數等,
  • 在方法區中類靜態屬性參考的物件,譬如Java類的參考型別靜態變數,
  • 在方法區中常量參考的物件,譬如字串常量池(String Table)里的參考,
  • 所有被同步鎖(synchronized關鍵字)持有的物件,
  • 在本地方法堆疊中JNI(即通常所說的Native方法)參考的物件,
  • Java虛擬機內部的參考,如基本資料型別對應的Class物件,一些常駐的例外物件(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器,
  • 反映Java虛擬機內部情況的JMXBean、JVMTI中注冊的回呼、本地代碼快取等,

除了這些固定的GC Roots集合以外,根據用戶所選用的垃圾收集器以及當前回收的記憶體區域不同,還可以有其他物件“臨時性”地加入,共同構成完整GC Roots集合,

具備了區域回收特征的垃圾收集器,可以避免GC Roots包含過多物件而過度膨脹,它們在實作上也做出了各種優化處理,

參考分類

在JDK 1.2版之后,Java對參考的概念進行了擴充,將參考分為強參考(Strongly Reference)、軟參考(Soft Reference)、弱參考(Weak Reference)和虛參考(Phantom Reference)4種,這4種參考強 度依次逐漸減弱,

  • 強參考是最傳統的“參考”的定義,是指在程式代碼之中普遍存在的參考賦值,即類似“Object obj=new Object()”這種參考關系,無論任何情況下,只要強參考關系還存在,垃圾收集器就永遠不會回收掉被參考的物件,
  • 軟參考是用來描述一些還有用,但非必須的物件,只被軟參考關聯著的物件,在系統將要發生記憶體溢位例外前,會把這些物件列進回收范圍之中進行第二次回收,如果這次回識訓沒有足夠的記憶體, 才會拋出記憶體溢位例外,在JDK 1.2版之后提供了SoftReference類來實作軟參考,
  • 弱參考也是用來描述那些非必須物件,但是它的強度比軟參考更弱一些,被弱參考關聯的物件只能生存到下一次垃圾收集發生為止,當垃圾收集器開始作業,無論當前記憶體是否足夠,都會回收掉只被弱參考關聯的物件,在JDK 1.2版之后提供了WeakReference類來實作弱參考,
  • 虛參考也稱為“幽靈參考”或者“幻影參考”,它是最弱的一種參考關系,一個物件是否有虛參考的 存在,完全不會對其生存時間構成影響,也無法通過虛參考來取得一個物件實體,為一個物件設定虛參考關聯的唯一目的只是為了能在這個物件被收集器回收時收到一個系統通知,在JDK 1.2版之后提供 了PhantomReference類來實作虛參考,

物件死亡程序

真正宣告一個物件死亡,最多會經歷兩次標記程序:如果物件在進行可達性分析后發現沒有與GC Roots相連接的參考鏈,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法(不推薦使用的語法),假如物件沒有覆寫finalize()方法,或者finalize()方法已經被虛擬機呼叫過,那么虛擬機將這兩種情況都視為“沒有必要執行”,

如果這個物件被判定為確有必要執行finalize()方法,那么該物件將會被放置在一個名為F-Queue的 佇列之中,并在稍后由一條由虛擬機自動建立的、低調度優先級的Finalizer執行緒去執行它們的finalize() 方法,這里所說的“執行”是指虛擬機會觸發這個方法開始運行,但并不承諾一定會等待它運行結束, 這樣做的原因是,如果某個物件的finalize()方法執行緩慢,或者更極端地發生了死回圈,將很可能導 致F-Queue佇列中的其他物件永久處于等待,甚至導致整個記憶體回收子系統的崩潰,finalize()方法是物件逃脫死亡命運的最后一次機會,稍后收集器將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與參考鏈上的任何一個物件建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移出“即將回收”的集 合;如果物件這時候還沒有逃脫,那基本上它就真的要被回收了,

回收方法區

方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的型別,

常量池中的常量已經沒有任何物件參考,且虛擬機中也沒有其他地方參考這個字面量,如果在這時發生記憶體回收,而且垃圾收集器判斷確有必要的話,這個常量就將會被系統清理出常量池,常量池中其他類(介面)、方法、欄位的符號參考也與此類似,

判定一個型別是否屬于“不再被使用的類”的條件比較苛刻,需要同時滿足下面三個條件:

  • 該類所有的實體都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實體,

  • 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,否則通常是很難達成的,

  • 該類對應的java.lang.Class物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,

    關于是否要對型別進行回收,HotSpot虛擬機提供了Xnoclassgc引數進行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX: +TraceClassUnLoading查看類加載和卸載資訊,其中-verbose:class和-XX:+TraceClassLoading可以在 Product版的虛擬機中使用,-XX:+TraceClassUnLoading引數需要FastDebug版的虛擬機支持,

在大量使用反射、動態代理、CGLib等位元組碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載 器的場景中,通常都需要Java虛擬機具備型別卸載的能力,以保證不會對方法區造成過大的記憶體壓力,

垃圾收集演算法

標記-清除演算法

最早出現也是最基礎的垃圾收集演算法是“標記-清除”(Mark-Sweep)演算法,分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成后,統一回收掉所有被標記的物件,也可以反過來,

主要缺點有兩個:

  • 第一個是執行效率不穩定,如果需要標記的物件太多,效率不高,
  • 第二個是記憶體空間的碎片化問題,標記、清除之后會產生大量不連續的記憶體碎片,空間碎片太多可能會導致當以后在程式運行程序中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作,

標記-復制演算法

標記-復制演算法解決了標記-清除演算法面對大量可回收物件時執行效率低的問題,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的記憶體用完了,就將還存活著的物件復制到另外一塊上面,對于多數物件都是可回收的情況,復制的就是占少數的存活物件,而且每次都是針對整個半區進行記憶體回收,分配記憶體時也就不用考慮有空間碎片的復雜情況,只要移動堆頂指標,按順序分配即可,這樣實作簡單,運行高效,不過其缺陷也顯而易見,可用記憶體縮小為了原來的一半,

標記-整理演算法

針對老年代物件的存亡特征,有針對性的“標記-整理”(Mark-Compact)演算法,其中的標記程序仍然與“標記-清除”演算法一樣,但后續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向記憶體空間一端移動,然后直接清理掉邊界以外的記憶體,標記-清除演算法與標記-整理演算法的本質差異在于前者是一種非移動式的回收演算法,而后者是移動式的,

HotSpot的演算法細節實作

根節點列舉

收集器在根節點列舉這一步驟時都是必須暫停用戶執行緒的,必須在一個能保障一致性的快照中才得以進行,

當用戶執行緒停頓下來之后,HotSpot 使用一組稱為OopMap(Ordinary Object Pointer Map,普通物件指標地圖),的資料結構直接得到哪些地方存放著物件參考,在OopMap的協助下,HotSpot可以快速準確地完成GC Roots列舉,一旦類加載動作完成的時候, HotSpot就會把物件內什么偏移量上是什么型別的資料計算出來,在即時編譯程序中,也會在特定的位置記錄下堆疊里和暫存器里哪些位置是參考,這樣收集器在掃描時就可以直接得知這些資訊了,并不需要真正一個不漏地從方法區等GC Roots開始查找,

安全點

導致OopMap內容變化的指令非常多,HotSpot沒有為每條指令都生成OopMap,只是在“特定的位置”記錄 了這些資訊,這些位置被稱為安全點(Safepoint),

安全點位置的選取基本上是以“是否具有讓程式長時間執行的特征”為標準進行選定的,“長時間執行”的最明顯特征就是指令序列的復用,例如方法呼叫、回圈跳轉、例外跳轉 等都屬于指令序列復用,所以只有具有這些功能的指令才會產生安全點,

垃圾收集發生時讓所有執行緒(這里其實不包括執行JNI呼叫的執行緒)都跑到最近的安全點,然后停頓下來,有兩種方案可供選擇:

  • 搶先式中斷 (Preemptive Suspension):搶先式中斷不需要執行緒的執行代碼主動去配合,在垃圾收集發生時,系統首先把所有用戶執行緒全部中斷,如果發現有用戶執行緒中斷的地方不在安全點上,就恢復這條執行緒執行,讓它一會再重新中斷,直到跑到安全點上,現在幾乎沒有虛擬機實作采用搶先式中斷來暫停執行緒回應GC事件,

  • 主動式中斷(Voluntary Suspension):當垃圾收集需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一個標志位,各個執行緒執行程序時會不停地主動去輪詢這個標志,一旦發現中斷標志為真時就自己在最近的安全點上主動中斷掛起,輪詢標志的地方和安全點是重合的,另外還要加上所有創建物件和其他需要在Java堆上分配記憶體的地方,這是為了檢查是否即將要發生垃圾收集,避免沒有足夠記憶體分配新物件,

    由于輪詢操作在代碼中會頻繁出現,這要求它必須足夠高效,HotSpot使用記憶體保護陷阱的方式,把輪詢操作精簡至只有一潭訓編指令的程度,當需要暫停用戶執行緒時,虛擬機把對應地址記憶體頁設定為不可讀,那執行緒執行到相應的指令時就會產生一個自陷例外信號,然后在預先注冊的例外處理器中掛起執行緒實作等待,這樣僅通過一潭訓編指令便完成安全點輪詢和觸發執行緒中斷了,

安全區域

安全點機制保證了程式執行時,在不太長的時間內就會遇到可進入垃圾收集程序的安全點,但是,程式“不執行”的時候呢?所謂的程式不執行就是沒有分配處理器時間,典型的場景便是用戶執行緒處于Sleep狀態或者Blocked狀態,虛擬機也顯然不可能持續等待執行緒重新被激活分配處理器時間,對于這種情況,就必須引入安全區域(Safe Region)來解決,

安全區域是指能夠確保在某一段代碼片段之中,參考關系不會發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的,可以把安全區域看作被擴展拉伸了的安全點,

當用戶執行緒執行到安全區域里面的代碼時,首先會標識自己已經進入了安全區域,那樣當這段時間里虛擬機要發起垃圾收集時就不必去管這些已宣告自己在安全區域內的執行緒了,當執行緒要離開安全區域時,它要檢查虛擬機是否已經完成了根節點列舉(或者垃圾收集程序中其他需要暫停用戶執行緒的階段),如果完成了,那執行緒繼續執行;否則它就必須一直等待,直到收到可以離開安全區域的信號為止,

記憶集與卡表

記憶集是一種用于記錄從非收集區域指向收集區域的指標集合的抽象資料結構,為解決物件跨代參考所帶來的問題,

在垃圾收集的場景中,收集器只需要通過記憶集判斷出某一塊非收集區域是否存在有指向了收集區域的指標就可以了,所以實作記憶集的時候,便可以選擇粗獷的記錄粒度來節省記憶集的存盤和維護成本:

  • 字長精度:每個記錄精確到一個機器字長(就是處理器的尋址位數,如常見的32位或64位,這個精度決定了機器訪問物理記憶體地址的指標長度),該字包含跨代指標,
  • 物件精度:每個記錄精確到一個物件,該物件里有欄位含有跨代指標,
  • 卡精度:每個記錄精確到一塊記憶體區域,該區域內有物件含有跨代指標,

第三種“卡精度”所指的是用一種稱為“卡表”(Card Table)的方式去實作記憶集,這也是目前最常用的一種記憶集實作形式,卡表最簡單的形式可以只是一個位元組陣列,而HotSpot虛擬機確實也是這樣做的,

CARD_TABLE [this address >> 9] = 1;

位元組陣列CARD_TABLE的每一個元素都對應著其標識的記憶體區域中一塊特定大小的記憶體塊,這個記憶體塊被稱作“卡頁”(Card Page),一般來說,卡頁大小都是以2的N次冪的位元組數,HotSpot中使用的卡頁是2的9次冪,即512位元組(地址右移9位,相當于用地址除以512),如果卡表標識記憶體區域的起始地址是0x0000的話,陣列CARD_TABLE的第0、1、2號元素,分別對應了 地址范圍為0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡頁記憶體塊,

在這里插入圖片描述

一個卡頁的記憶體中通常包含不止一個物件,只要卡頁內有一個(或更多)物件的欄位存在著跨代指標,那就將對應卡表的陣列元素的值標識為1,稱為這個元素變臟(Dirty),沒有則標識為0,在垃圾收集發生時,只要篩選出卡表中變臟的元素,就能輕易得出哪些卡頁記憶體塊中包含跨代指標,把它們加入GC Roots中一并掃描,

寫屏障

有其他分代區域中物件參考了本區域物件時,其對應的卡表元素就應該變臟,變臟時間點原則上應該發生在參考型別欄位賦值的那一刻,在HotSpot虛擬機里是通過寫屏障(Write Barrier)技術維護卡表狀態,

寫屏障可以看作在虛擬機層面對“參考型別欄位賦值”這個動作的AOP切面,也就是說賦值的前后都在寫屏障的覆寫范疇內,在賦值前的部分的寫屏障叫作寫前屏障(Pre-Write Barrier),在賦值后的則叫作寫后屏障(Post-Write Barrier),HotSpot虛擬機的許多收集器中都有使用到寫屏障,但直至G1收集器出現之前,其他收集器都只用到了寫后屏障,應用寫屏障后,虛擬機就會為所有賦值操作生成相應的指令,一旦收集器在寫屏障中增加了更新卡表操作,無論更新的是不是老年代對新生代物件的參考,

卡表在高并發場景下還面臨著“偽共享”(False Sharing)問題,偽共享是處理并發底層細節時一種經常需要考慮的問題,現代中央處理器的快取系統中是以快取行(Cache Line) 為單位存盤的,當多執行緒修改互相獨立的變數時,如果這些變數恰好共享同一個快取行,就會彼此影響(寫回、無效化或者同步)而導致性能降低,這就是偽共享問題,一種簡單的解決方案是不采用無條件的寫屏障,而是先檢查卡表標記,只有當該卡表元素未被標記過時才將其標記為變臟,在JDK 7之后,HotSpot虛擬機增加了一個新的引數-XX:+UseCondCardMark,用來決定是否開啟卡表更新的條件判斷,

并發的可達性分析

堆越大,存盤的物件越多,物件圖結構越復雜,要標記更多物件而產生的停頓時間就更長,如果用戶執行緒與收集器是并發作業:收集器在物件圖結構上標記,同時用戶執行緒在修改參考關系——即修改物件圖的結構,可能出現把原本存活的物件錯誤標記為已消亡,

當且僅當以下兩個條件同時滿足時,會產生“物件消失”的問題:

  • 賦值器插入了一潭訓多條從黑色[17]物件到白色[18]物件的新參考;
  • 賦值器洗掉了全部從灰色[19]物件到該白色物件的直接或間接參考;

要解決并發掃描時的物件消失問題,只需破壞這兩個條件的任意一個即可,由此分別產生了兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning, SATB),

  • 增量更新:破壞的是第一個條件,當黑色物件插入新的指向白色物件的參考關系時,就將這個新插入的參考記錄下來,等并發掃描結束之后,再將這些記錄過的參考關系中的黑色物件為根,重新掃描一次,這可以簡化理解為,黑色物件一旦新插入了指向白色物件的參考之后,它就變回灰色物件 了,
  • 原始快照:破壞的是第二個條件,當灰色物件要洗掉指向白色物件的參考關系時,就將這個要洗掉的參考記錄下來,在并發掃描結束之后,再將這些記錄過的參考關系中的灰色物件為根,重新掃描一次,這也可以簡化理解為,無論參考關系洗掉與否,都會按照剛剛開始掃描那一刻的物件圖快照來進行搜索,目的就是讓這種物件在本輪GC清理中能存活下來,待下一輪gc的時候重新掃描,這個物件也有可能是浮動垃圾

HotSpot并發標記時對漏標的處理方案如下: CMS:寫屏障 + 增量更新 ;G1,Shenandoah:寫屏障 + SATB

為什么G1用SATB?CMS用增量更新?

  • SATB相對增量更新效率會高(當然SATB可能造成更多的浮動垃圾),因為不需要在重新標記階段再次深度掃描被洗掉參考物件,而CMS對增量參考的根物件會做深度掃描,G1因為很多物件都位于不同的region,CMS就一塊老年代區域,重新深度掃描物件的話G1的代價會比CMS高,所以G1選擇SATB不深度掃描物件,只是簡單標記,等到下一輪GC 再深度掃描,

垃圾收集器

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-er6LgSYK-1675133822192)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230126150412774.png)]

JDK8中默認使用組合是: Parallel Scavenge 、Parallel Old

Serial

Serial收集器是一個單執行緒作業的收集器,但它的“單線 程”的意義并不僅僅是說明它只會使用一個處理器或一條收集執行緒去完成垃圾收集作業,更重要的是強調在它進行垃圾收集時,必須暫停其他所有作業執行緒,直到它收集結束,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-F4tPUBGO-1675133822192)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230126150718842.png)]

HotSpot虛擬機運行在客戶端模式下的默認新生代收集器,優于其他收集器的地方,那就是簡單而高效(與其他收集器的單執行緒相比)它是所有收集器里額外記憶體消耗(Memory Footprint)最小、單執行緒收集效率最高的,

Serial Old收集器是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用標記-整理演算法,這個收集器的主要意義也是供客戶端模式下的HotSpot虛擬機使用,如果在服務端模式下,它也可能有兩種用途:一種是在JDK5以及之前的版本中與Parallel Scavenge收集器搭配使用,另外一種就是作為CMS 收集器發生失敗時的后備預案,在并發收集發生Concurrent Mode Failure時使用,

使用方式:-XX:+UseSerialGC -XX:+UseSerialOldGC

ParNew

ParNew收集器實質上是Serial收集器的多執行緒并行版本,除了同時使用多條執行緒進行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制引數、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一致,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-EFDhj4Rs-1675133822192)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230126151531157.png)]

它默認開啟的收集執行緒數與處理器核心數量相同,可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數,

ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC選項)的默認新生代收集器,也可以使用-XX:+/-UseParNewGC選項來強制指定或者禁用它,

Parallel

Parallel Scavenge收集器是基于標記-復制演算法實作的,也是能夠并行收集的多執行緒收集器,Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput),所謂吞吐量就是處理器用于運行用戶代碼的時間與處理器總消耗時間的比值,吞吐量=運行用戶代碼時間/(運行用戶代碼時間+運行垃圾收集時間 )

高吞吐量可以最高效率地利用處理器資源,盡快完成程式的運算任務,主要適合在后臺運算而不需要太多互動的分析任務,

Parallel Scavenge收集器提供了兩個引數用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis[20]引數以及吞吐量大小的-XX:GCTimeRatio[21]引數,

Parallel Scavenge收集器還有一個引數-XX:+UseAdaptiveSizePolicy,當這個引數被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代物件大小(-XX:PretenureSizeThreshold)等細節引數了,虛擬機會根據當前系統的運行情況收集性能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為垃圾收集的自適應的調節策略(GC Ergonomics),只需要把基本的記憶體資料設定好(如-Xmx設定最大堆),然后使用-XX:MaxGCPauseMillis引數(更關注最大停頓時間)或XX:GCTimeRatio(更關注吞吐量)引數給虛擬機設立一個優化目標,那具體細節引數的調節作業就由虛擬機完成了,

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多執行緒并行收集,基于標記-整理演算法實作,
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uuhKRiZ2-1675133822193)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230126160351567.png)]

CMS

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,基于標記-清除演算法實作,它的運作程序分為四個步驟,包括:

  1. 初始標記(CMS initial mark):僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快;
  2. 并發標記(CMS concurrent mark):從GC Roots的直接關聯物件開始遍歷整個物件圖的程序,這個程序耗時較長但是不需要停頓用戶執行緒,CMS收集器采用增量更新演算法實作收集執行緒與用戶執行緒互不干擾地運行;
  3. 重新標記(CMS remark):為了修正并發標記期間,因用戶程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一 些,但也遠比并發標記階段的時間短;
  4. 并發清除(CMS concurrent sweep):清理洗掉掉標記階段判斷的已經死亡的物件,由于不需要移動存活物件,所以這個階段也是可以與用戶執行緒同時并發的,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-EnJOFqVC-1675133822193)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230126171325134.png)]

初始標記、重新標記這兩個步驟仍然需要“Stop The World”,

CMS收集器對處理器資源非常敏感,在并發階段,它雖然不會導致用戶執行緒停頓,但卻會因為占用了一部分執行緒(或者說處理器的計 算能力)而導致應用程式變慢,降低總吞吐量,CMS默認啟動的回收執行緒數是(處理器核心數量 +3)/4,也就是說,如果處理器核心數在四個或以上,并發回收時垃圾收集執行緒只占用不少于25%的 處理器運算資源,并且會隨著處理器核心數量的增加而下降,但是當處理器核心數量不足四個時, CMS對用戶程式的影響就可能變得很大,

CMS收集器無法處理“浮動垃圾”(Floating Garbage),有可能出現“Concurrent Mode Failure”失敗進而導致另一次完全“Stop The World”的Full GC的產生,在JDK5的默認設定下,當老年代使用了68%的空間后CMS收集器就會被激活,JDK6時提升至92%,CMS運行期間預留的記憶體無法滿足程式分配新物件的需要,就會出現一次“并發失敗”(Concurrent Mode Failure),這時候虛擬機將啟動后備預案:凍結用戶執行緒的執行,臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,可以通過引數-XX:CMSInitiatingOccupancyFraction設定,

CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關引數(默認開啟, JDK 9廢棄),用于在CMS收集器不得不進行Full GC時開啟記憶體碎片的合并整理程序,由于這個記憶體整理必須移動存活物件,(在Shenandoah和ZGC出現前)是無法并發的,這樣空間碎片問題是解決了,但停頓時間又會變長,因此還提供了另外一個引數-XX:CMSFullGCsBeforeCompaction(JDK9廢棄),這個引數的作用是要求CMS收集器在執行過若干次(數量由引數值決定)不整理空間的Full GC之后,下一次進入Full GC前會先進行碎片整理(默認值為0,表 示每次進入Full GC時都進行碎片整理),

CMS的相關核心引數 :

  1. -XX:+UseConcMarkSweepGC:啟用cms
  2. -XX:ConcGCThreads:并發的GC執行緒數
  3. -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC
  4. -XX:+UseCMSCompactAtFullCollection:FullGC之后做壓縮整理(減少碎片)
  5. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后壓縮一次,默認是0,代表每次FullGC后都會壓縮一 次
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收閾值(-XX:CMSInitiatingOccupancyFraction設定的值),如果不指定,JVM僅在第一次使用設定值,后續則會自動調整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次minor gc,目的在于減少老年代對年輕代的參考,降低CMS GC的標記階段時的開銷,一般CMS的GC耗時 80%都在標記階段
  8. -XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多執行緒執行,縮短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新標記的時候多執行緒執行,縮短STW;

Garbage First

Garbage First(簡稱G1)開創了收集器面向區域收集的設計思路和基于Region的記憶體布局形式,官方給它設定的目標是在延遲可控(-XX:MaxGCPauseMillis)的情況下獲得盡可能高的吞吐量,規劃JDK10功能目標時,HotSpot虛擬機提出了“統一垃圾收集器介面” ,將記憶體回收的“行為”與“實作”進行分離,CMS以及其他收集器都重構成基于這套介面的一種實作,

G1可以面向堆記憶體任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬于哪個分代,而是哪塊記憶體中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kh9kyzpb-1675133822193)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129232037535.png)]

G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間,收集器能夠對扮演不同角色的 Region采用不同的策略去處理,這樣無論是新創建的物件還是已經存活了一段時間、熬過多次收集的舊物件都能獲取很好的收集效果,

默認年輕代對堆記憶體的占比是5%,可以通過-XX:G1NewSizePercent”設定新生代初始占比,在系統運行中,JVM會不停的給年輕代增加更多的Region,但是最多新生代的占比不會超過60%,可以通過“-XX:G1MaxNewSizePercent”調整,年輕代中的Eden和 Survivor對應的region也跟之前一樣,默認8:1:1,假設年輕代現在有1000個region,eden區對應800個,s0對應100 個,s1對應100個,

Region中還有一類特殊的Humongous區域,專門用來存盤大物件,只要大小超過了一個Region容量一半的物件即可判定為大物件,每個Region的大小可以通過引數-XX:G1HeapRegionSize設定,取值范圍為1MB~32MB,且應為2的N次冪,而對于那些超過了整個Region容量的超級大物件, 將會被存放在N個連續的Humongous Region之中,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待,

G1收集器將Region作為單次回收的最小單元,即每次收集到的記憶體空間都是Region大小的整數倍,具體的處理思路是:讓G1收集器去跟蹤各個Region里面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然后在后臺維護一個優先級串列,每次根據用戶設定允許的收集停頓時間(使用引數-XX:MaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,

解決Region里面存在的跨Region參考物件:

  • 每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region 指向自己的指標,并標記這些指標分別在哪些卡頁的范圍之內,G1的記憶集在存盤結構的本質上是一 種哈希表,Key是別的Region的起始地址,Value是一個集合,里面存盤的元素是卡表的索引號,

解決在并發標記階段收集執行緒與用戶執行緒互不干擾地運行:

  • 通過原始快照(SATB)演算法來實作,此外,垃圾收集對用戶執行緒的影響還體現在回收程序中新創建物件的記憶體分配上,程式要繼續運行就肯定會持續有新物件被創建,G1為每一個Region設 計了兩個名為TAMS(Top at Mark Start)的指標,把Region中的一部分空間劃分出來用于并發回收程序中的新物件分配,并發回收時新分配的物件地址都必須要在這兩個指標位置以上,G1收集器默認在 這個地址以上的物件是被隱式標記過的,即默認它們是存活的,不納入回收范圍,

如果不去計算用戶執行緒運行程序中的動作(如使用寫屏障維護記憶集的操作),G1收集器的 運作程序大致可劃分為以下四個步驟:

  1. 初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的物件,并且修改TAMS 指標的值,讓下一階段用戶執行緒并發運行時,能正確地在可用的Region中分配新物件,這個階段需要停頓執行緒,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際并沒有額外的停頓,
  2. 并發標記(Concurrent Marking):從GC Root開始對堆中物件進行可達性分析,遞回掃描整個堆里的物件圖,找出要回收的物件,這階段耗時較長,但可與用戶程式并發執行,當物件圖掃描完成以后,還要重新處理SATB記錄下的在并發時有參考變動的物件,
  3. 最終標記(Final Marking):對用戶執行緒做另一個短暫的暫停,用于處理并發階段結束后仍遺留下來的最后那少量的SATB記錄,
  4. 篩選回收(Live Data Counting and Evacuation):負責更新Region的統計資料,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收的那一部分Region的存活物件復制到空的Region中,再清理掉整個舊 Region的全部空間,這里的操作涉及存活物件的移動,是必須暫停用戶執行緒,由多條收集器執行緒并行完成的,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-FPVJA23W-1675133822193)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230126225447512.png)]

G1從整體來看是基于“標記-整理”演算法實作的收集器,但從區域(兩個Region 之間)上看又是基于“標記-復制”演算法實作,

G1收集器引數設定:

  • -XX:+UseG1GC:使用G1收集器
  • -XX:ParallelGCThreads:指定GC作業的執行緒數量
  • -XX:G1HeapRegionSize:指定磁區大小(1MB~32MB,且必須是2的N次冪),默認將整堆劃分為2048個磁區
  • -XX:MaxGCPauseMillis:目標暫停時間(默認200ms)
  • -XX:G1NewSizePercent:新生代記憶體初始空間(默認整堆5%)
  • -XX:G1MaxNewSizePercent:新生代記憶體最大空間
  • -XX:TargetSurvivorRatio:Survivor區的填充容量(默認50%),Survivor區域里的一批物件(年齡1+年齡2+年齡n的多個 年齡物件)總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的物件都放入老年代
  • -XX:MaxTenuringThreshold:最大年齡閾值(默認15)
  • -XX:InitiatingHeapOccupancyPercent:老年代占用空間達到整堆記憶體閾值(默認45%),則執行新生代和老年代的混合收集(MixedGC),比如我們之前說的堆默認有2048個region,如果有接近1000個region都是老年代的region,則可能 就要觸發MixedGC了
  • -XX:G1MixedGCLiveThresholdPercent(默認85%) region中的存活物件低于這個值時才會回收該region,如果超過這個值,存活物件過多,回收的的意義不大,
  • -XX:G1MixedGCCountTarget:在一次回收程序中指定做幾次篩選回收(默認8次),在最后一個篩選回收階段可以回收一 會,然后暫停回收,恢復系統運行,一會再開始回收,這樣可以讓系統不至于單次停頓時間過長,
  • -XX:G1HeapWastePercent(默認5%): gc程序中空出來的region是否充足閾值,在混合回收的時候,對Region回收都是基于復制演算法進行的,都是把要回收的Region里的存活物件放入其他Region,然后這個Region中的垃圾物件全部清 理掉,這樣的話在回收程序就會不斷空出來新的Region,一旦空閑出來的Region數量達到了堆記憶體的5%,此時就會立 即停止混合回收,意味著本次混合回收就結束了,

ZGC

ZGC也采用基于Region的堆記憶體布局,但不同的是,ZGC的Region(在一些官方資料中將它稱為Page或者ZPage)具有動態性——動態創建和銷毀,以及動態的區域容量大小,在x64硬體平臺下,ZGC的 Region可以具有大、中、小三類容量:

  • 小型Region(Small Region) : 容量固定為2MB, 用于放置小于256KB的小物件,

  • 中型Region(Medium Region) : 容量固定為32MB, 用于放置大于等于256KB但小于4MB的物件,

  • 大型Region(Large Region) : 容量不固定, 可以動態變化, 但必須為2MB的整數倍, 用于放置4MB或 以上的大物件, 每個大型Region中只會存放一個大物件, 這也預示著雖然名字叫作“大型Region”, 但它的實際容量完全有可能小于中型 Region, 最小容量可低至4MB, 大型Region在ZGC的實作中是不會被重分配(重分配是ZGC的一種處理動作, 用于復制物件的收集器階段, 稍后會介紹到)的, 因為復制一個大物件的代價非常高昂,

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lS3nZgPu-1675133822193)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129235532697.png)]

ZGC對于不同頁面回收的策略也不同,簡單地說,小頁面優先回收;中頁面和大頁面則盡量不回收,

NUMA-aware

NUMA對應的有UMA,UMA即Uniform Memory Access Architecture,NUMA就是Non Uniform Memory Access Architecture,UMA表示記憶體只有一塊,所有CPU都去訪問這一塊記憶體,那么就會存在競爭問題(爭奪記憶體總線訪問權),有競爭就會有鎖,有鎖效率就會受到影響,而且CPU核心數越多,競爭就越激烈,NUMA的話每個CPU對應有一塊記憶體,且這塊記憶體在主板上離這個CPU是最近的,每個CPU優先訪問這塊記憶體,那效率自然就提高了,[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-lC3R8h1M-1675133822194)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230129235831737.png)]

ZGC是支持NUMA的,在進行小頁面分配時會優先從本地記憶體分配,當不能分配時才會從遠端的記憶體分配,對 于中頁面和大頁面的分配,ZGC并沒有要求從本地記憶體分配,而是直接交給作業系統,由作業系統找到一塊能 滿足ZGC頁面的空間,ZGC這樣設計的目的在于,對于小頁面,存放的都是小物件,從本地記憶體分配速度很 快,且不會造成記憶體使用的不平衡,而中頁面和大頁面因為需要的空間大,如果也優先從本地記憶體分配,極易 造成記憶體使用不均衡,反而影響性能,

染色指標技術(Colored Pointer)

染色指標是一種直接將少量額外的資訊存盤在指標上的技術,某個物件只有它的參考關系能決定它存活與否,ZGC的染色指標是直接把標記資訊記在參考物件的指標上,這時,與其說可達性分析是遍歷物件圖來標記物件,還不如說是遍歷“參考圖”來標記“參考”了,

64位的Linux則分別支持47位(128TB)的行程虛擬地址空間和46位(64TB)的物理地址空間,64位的Windows系統只支持44位(16TB)的物理地址空間,

ZGC的染色指標技術將高4位提取出來存盤四個標志資訊,通過這些標志位,虛擬機可以直接從指標中看到其參考物件的三色標記狀態、是否進入了重分配集(即被移動過)、是否只能通過finalize()方法才能被訪問到,當然,由于這些標志位進一步壓縮了原本就只有46位的地址空間,也直接導致 ZGC能夠管理的記憶體不可以超過4TB(2的42次冪),
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-A0hRR5nI-1675133822194)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230130000914709.png)]

2個mark標記作用:每一個GC周期開始時,會交換使用的標記位,使上次GC周期中修正的已標記狀態失效,所有參考都變成未標記,

  • GC周期1:使用mark0, 則周期結束所有參考mark標記都會成為01,
  • GC周期2:使用mark1, 則期待的mark標記10,所有參考都能被重新標記,

染色指標技術的三大優勢

  • 染色指標可以使得一旦某個Region的存活物件被移走之后,這個Region立即就能夠被釋放和重用 掉,而不必等待整個堆中所有指向該Region的參考都被修正后才能清理,

  • 染色指標可以大幅減少在垃圾收集程序中記憶體屏障的使用數量,設定記憶體屏障,尤其是寫屏障的 目的通常是為了記錄物件參考的變動情況,如果將這些資訊直接維護在指標中,顯然就可以省去一些專門的記錄操作,

  • 染色指標可以作為一種可擴展的存盤結構用來記錄更多與物件標記、重定位程序相關的資料,以 便日后進一步提高性能,

讀屏障:如果物件在GC時被移動了,接下來JVM就會加上一個讀屏障,這個屏障會把讀出的指標更新到物件的新地址上,并且把堆里的這個指標“修正”到原本的欄位里,這樣就算GC把物件移動 了,讀屏障也會發現并修正指標,于是應用代碼就永遠都會持有更新后的有效指標,而且不需要STW,因為Load Barriers的存在,所以會導致配置ZGC的應用的吞吐量會變低,官方的測驗資料是需要多出額外4%的開銷;
在這里插入圖片描述

Linux/x86-64平臺上的ZGC使用了多重映射(Multi-Mapping)將多個不同的虛擬記憶體地址映射到同一個物理記憶體地址上,這是一種多對一映射,意味著ZGC在虛擬記憶體中看到的地址空間要比實際的堆記憶體容量來得更大,把染色指標中的標志位看作是地址的分段符,那只要將這些不同的地址段都映射到同一個物理記憶體空間,經過多重映射轉換后,就可以使用染色指標正常進行尋址了,[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yFmjFft6-1675133822194)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230130101717419.png)]

ZGC的運作程序

  1. 并發標記(Concurrent Mark):并發標記是遍歷物件圖做可達性分析的階段,前后也要經過類似于G1的初始標記、最終標記(盡管ZGC中的名字不叫這些)的短暫停頓,而且這些停頓階段所做的事情在目標上也是相類似的,ZGC 的標記是在指標上而不是在物件上進行的,標記階段會更新染色指標中的Marked 0、Marked 1標志位,
  2. 并發預備重分配(Concurrent Prepare for Relocate):這個階段需要根據特定的查詢條件統計得出本次收集程序要清理哪些Region,將這些Region組成重分配集(Relocation Set),ZGC每次回收都會掃描所有的Region,用范圍更大的掃描成本換取省去G1中記憶集的維護成本,因此,ZGC的重分配集只是決定了里面的存活物件會被重新復制到其他的Region中,里面的Region會被釋放,而并不能說回收行為就只是針對這個集合里面的Region進行,因為標記程序是針對全堆的,此外,在JDK 12的ZGC中開始支持的類卸載以及弱參考的處理,也是在這個階段中完成的,
  3. 并發重分配(Concurrent Relocate):重分配是ZGC執行程序中的核心階段,這個程序要把重分配集中的存活物件復制到新的Region上,并為重分配集中的每個Region維護一個轉發表(Forward Table),記錄從舊物件到新物件的轉向關系,得益于染色指標的支持,ZGC收集器能僅從參考上就明確得知一個物件是否處于重分配集之中,如果用戶執行緒此時并發訪問了位于重分配集中的物件,這次訪問將會被預置的記憶體屏障(讀屏障)所截獲,然后立即根據Region上的轉發表記錄將訪問轉發到新復制的物件 上,并同時修正更新該參考的值,使其直接指向新物件,ZGC將這種行為稱為指標的“自愈”(SelfHealing)能力,這樣做的好處是只有第一次訪問舊物件會陷入轉發,也就是只慢一次,還有另外一個直接的好處是由于染色指標的存在,一旦重分配集中某個Region的存活物件都復制完畢后,這個Region就可以立即釋放用于新物件的分配(但是轉發表還得留著不能釋放掉),哪怕堆中還有很多指向這個物件的未更新指標也沒有關系,這些舊指標一旦被使用,它們都是可以自愈的,
  4. 并發重映射(Concurrent Remap):重映射所做的就是修正整個堆中指向重分配集中舊物件的所有參考,ZGC的并發重映射并不 是一個必須要“迫切”去完成的任務,因為前面說過,即使是舊參考,它也是可以自愈的,最多只是第一次使用時多一次轉發和修正操作,重映射清理這些舊參考的主要目的是為了不變慢(還有清理結束后可以釋放轉發表這樣的附帶收益),所以說這并不是很“迫切”,因此,ZGC很巧妙地把并發重映射階段要做的作業,合并到了下一次垃圾收集回圈中的并發標記階段里去完成,反正它們都是要遍歷所有物件的,這樣合并就節省了一次遍歷物件圖[22]的開銷,一旦所有指標都被修正之后,原來記錄新舊物件關系的轉發表就可以釋放掉了,

ZGC最大的問題是浮動垃圾,ZGC的停頓時間是在10ms以下,但是ZGC的執行時間還是遠遠大于這個時間的,假如ZGC 全程序需要執行10分鐘,在這個期間由于物件分配速率很高,將創建大量的新物件,這些物件很難進入當次GC,所以只 能在下次GC的時候進行回收,這些只能等到下次GC才能回收的物件就是浮動垃圾, ZGC沒有分代概念,每次都需要進行全堆掃描,導致一些“朝生夕死”的物件沒能及時的被回收, 目前唯一的辦法是增大堆的容量,使得程式得到更多的喘息時間,但是這個也是一個治標不治本的方案,如果需要從根本上解決這個問題,還是需要引入分代收集,讓新生物件都在一個專門的區域中創建,然后專門針對這個區域進行更頻 繁、更快的收集,

**ZGC中GC觸發機制(JAVA16) **:

預熱規則:服務剛啟動時出現,一般不需要關注, 日志中關鍵字是“Warmup”,JVM啟動預熱,如果從來沒有發生過GC,則在堆記憶體使用超過10%、20%、30%時,分別觸發一次GC,以收集GC資料

基于分配速率的自適應演算法:最主要的GC觸發方式(默認方式),其演算法原理可簡單描述為”ZGC根據近期的物件分配速率以及GC時間,計算出當記憶體占用達到什么閾值時觸發下一次GC”,通過ZAllocationSpikeTolerance引數控制閾值大小,該引數默認2,數值越大,越早的觸發GC,日志中關鍵字是“Allocation Rate”,

基于固定時間間隔:通過ZCollectionInterval控制,適合應對突增流量場景,流量平穩變化時,自適應演算法可能在堆使用率達 到95%以上才觸發GC,流量突增時,自適應演算法觸發的時機可能會過晚,導致部分執行緒阻塞,我們通過調整此引數解決流量突增場景的問題,比如定時活動、秒殺等場景,

主動觸發規則:類似于固定間隔規則,但時間間隔不固定,是ZGC自行算出來的時機,我們的服務因為已經加了基于固定時間間隔的觸發機制,所以通過-ZProactive引數將該功能關閉,以免GC頻繁,影響服務可用性,

阻塞記憶體分配請求觸發:當垃圾來不及回收,垃圾將堆占滿時,會導致部分執行緒阻塞,我們應當避免出現這種觸發方式,日志 中關鍵字是“Allocation Stall”,

外部觸發:代碼中顯式呼叫System.gc()觸發, 日志中關鍵字是“System.gc()”,

元資料分配觸發:元資料區不足時導致,一般不需要關注, 日志中關鍵字是“Metadata GC Threshold”,

ZGC引數設定

ZGC 優勢不僅在于其超低的 STW 停頓,也在于其引數的簡單,絕大部分生產場景都可以自適應,當然,極端 情況下,還是有可能需要對 ZGC 個別引數做個調整,大致可以分為三類:

堆大小:Xmx,當分配速率過高,超過回收速率,造成堆記憶體不夠時,會觸發 Allocation Stall,這類 Stall 會級訓當前的用戶執行緒,因此,當我們在 GC 日志中看到 Allocation Stall,通常可以認為堆空間偏小或者 concurrent gc threads 數偏小,

GC 觸發時機:ZAllocationSpikeTolerance, ZCollectionInterval,ZAllocationSpikeTolerance 用 來估算當前的堆記憶體分配速率,在當前剩余的堆記憶體下,ZAllocationSpikeTolerance 越大,估算的達到 OOM 的時間越快,ZGC 就會更早地進行觸發 GC,ZCollectionInterval 用來指定 GC 發生的間隔,以秒 為單位觸發 GC,

GC 執行緒:ParallelGCThreads, ConcGCThreads,ParallelGCThreads 是設定 STW 任務的 GC 線 程數目,默認為 CPU 個數的 60%;ConcGCThreads 是并發階段 GC 執行緒的數目,默認為 CPU 個數的 12.5%,增加 GC 執行緒數目,可以加快 GC 完成任務,減少各個階段的時間,但也會增加 CPU 的搶占開銷,可根據生產情況調整,

ZGC生產注意事項

RSS 記憶體例外現象

由前面 ZGC 原理可知,ZGC 采用多映射 multi-mapping 的方法實作了三份虛擬記憶體指向同一份物理記憶體,而 Linux 統計行程 RSS 記憶體占用的演算法是比較脆弱的,這種多映射的方式并沒有考慮完整,因此根據當前 Linux 采用大頁和小頁時,其統計的開啟 ZGC 的 Java 行程的記憶體表現是不同的,在內核使用小頁的 Linux 版本上, 這種三映射的同一塊物理記憶體會被 linux 的 RSS 占用演算法統計 3 次,因此通常可以看到使用 ZGC 的 Java 行程 的 RSS 記憶體膨脹了三倍左右,但是實際占用只有統計資料的三分之一,會對運維或者其他業務造成一定的困 擾,而在內核使用大頁的 Linux 版本上,這部分三映射的物理記憶體則會統計到 hugetlbfs inode 上,而不是當 前 Java 行程上,

共享記憶體調整

ZGC 需要在 share memory 中建立一個記憶體檔案來作為實際物理記憶體占用,因此當要使用的 Java 的堆大小大 于 /dev/shm 的大小時,需要對 /dev/shm 的大小進行調整,通常來說,命令如下(下面是將 /dev/shm 調整 為 64G):

vi/etc/fstabtmpfs /dev/shm tmpfs defaults,size= 65536M00

首先修改 fstab 中 shm 配置的大小,size 的值根據需求進行修改,然后再進行 shm 的 mount 和 umount,

umount/dev/shmmount /dev/shm

mmap節點上限調整

ZGC 的堆申請和傳統的 GC 有所不同,需要占用的 memory mapping 數目更多,即每個 ZPage 需要 mmap 映射三次,這樣系統中僅 Java Heap 所占用的 mmap 個數為 (Xmx / zpage_size) *3,默認情況下 zpage_size 的大小為 2M,

為了給 JNI 等 native 模塊中的 mmap 映射數目留出空間,記憶體映射的數目應該調整為 (Xmx / zpage_size) 3*1.2, 默認的系統 memory mapping 數目由檔案 /proc/sys/vm/max_map_count 指定,通常數目為 65536,當給 JVM 配置一個很大的堆時,需要調整該檔案的配置,使得其大于 (Xmx / zpage_size) 3*1.2,

5 JVM調優工具

jps:虛擬機行程狀況工具

功能ps命令類似:可以列出正在運行的虛擬機行程,并顯示虛擬機執行主類(Main Class,main()函式所在的類)名稱以及這些行程的本地虛擬機唯一ID(LVMID,Local Virtual Machine Identifier),

jps命令格式:jps [ options ] [ hostid ]

jps還可以通過RMI協議查詢開啟了RMI服務的遠程虛擬機行程狀態,引數hostid為RMI注冊表中注冊的主機名,

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VspKRPxM-1675133822195)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230130122051539.png)]

jstat:虛擬機統計資訊監視工具

jstat(JVM Statistics Monitoring Tool)是用于監視虛擬機各種運行狀態資訊的命令列工具,它可以顯示本地或者遠程虛擬機行程中的類加載、記憶體、垃圾收集、即時編譯等運行時資料,在沒有 GUI圖形界面、只提供了純文本控制臺環境的服務器上,它將是運行期定位虛擬機性能問題的常用工具,

jstat命令格式為:jstat [ option vmid [interval[s|ms] [count]] ]

對于命令格式中的VMID與LVMID需要特別說明一下:如果是本地虛擬機行程,VMID與LVMID 是一致的;如果是遠程虛擬機行程,那VMID的格式應當是:

[protocol:][//]lvmid[@hostname[:port]/servername]

引數interval和count代表查詢間隔和次數,如果省略這2個引數,說明只查詢一次,假設需要每100 毫秒查詢一次行程43260垃圾收集狀況,一共查詢20次,:

jstat -gc 43260 100 20

選項option代表用戶希望查詢的虛擬機資訊,主要分為三類:類加載、垃圾收集、運行期編譯狀況:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7WRkVHfy-1675133822195)(C:\Users\0\AppData\Roaming\Typora\typora-user-images\image-20230130123058771.png)]

jinfo:Java配置資訊工具

jinfo(Configuration Info for Java)的作用是實時查看和調整虛擬機各項引數,使用jps命令的-v引數可以查看虛擬機啟動時顯式指定的引數串列,但如果想知道未被顯式指定的引數的系統默認值,除了去找資料外,就只能使用jinfo的-flag選項進行查詢了(如果只限于JDK 6或以上版本的話,使用javaXX:+PrintFlagsFinal查看引數默認值也是一個很好的選擇),jinfo還可以使用-sysprops選項把虛擬機行程的System.getProperties()的內容列印出來,JDK 6之后,加入了在運行期修改部分引數值的能力(可以使用-flag[+|-]name或者-flag name=value在運行期修改一部分運行期可寫的虛擬機引數值),

jinfo命令格式:

jinfo [ option ] pid

jmap:Java記憶體映像工具

jmap(Memory Map for Java)命令用于生成堆轉儲快照(一般稱為heapdump或dump檔案),如果不使用jmap命令,要想獲取Java堆轉儲快照也還有一些比較“暴力”的手段:XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath=./引數,可以讓虛擬機在記憶體溢位例外出現之后自動生成堆轉儲快照檔案,通過-XX:+HeapDumpOnCtrlBreak引數則可以使用[Ctrl]+[Break]鍵讓虛擬機生成堆轉儲快照檔案,又或者在Linux系統下通過Kill-3命令發送行程退出信號“恐嚇”一下虛擬機,也能順利拿到堆轉儲快照,

jmap的作用并不僅僅是為了獲取堆轉儲快照,它還可以查詢finalize執行佇列、Java堆和方法區的詳細資訊,如空間使用率、當前用的是哪種收集器等,

jmap命令格式:

jmap [ option ] vmid

在這里插入圖片描述

jstack:Java堆疊跟蹤工具

jstack(Stack Trace for Java)命令用于生成虛擬機當前時刻的執行緒快照(一般稱為threaddump或者 javacore檔案),執行緒快照就是當前虛擬機內每一條執行緒正在執行的方法堆疊的集合,生成執行緒快照的目的通常是定位執行緒出現長時間停頓的原因,如執行緒間死鎖、死回圈、請求外部資源導致的長時間掛起等,都是導致執行緒長時間停頓的常見原因,

jstack命令格式:

jstack [ option ] vmid

Options:
-F to force a thread dump. Use when jstack does not respond (process is hung)
-m to print both java and native frames (mixed mode)
-l long listing. Prints additional information about locks

GC日志

列印GC日志檔案:

‐Xloggc:./gc‐%t.log ‐XX:+PrintGCDetails ‐XX:+PrintGCDateStamps ‐XX:+PrintGCTimeStamps ‐XX:+PrintGCCause ‐XX:+UseGCLogFileRotation ‐XX:NumberOfGCLogFiles=10 ‐XX:GCLogFileSize=100M

JVM引數匯總查看命令

java -XX:+PrintFlagsInitial 表示列印出所有引數選項的默認值

java -XX:+PrintFlagsFinal 表示列印出所有引數選項在運行程式時生效的值


  1. 區域變數表:存放編譯期可知的各種Java虛擬機基本資料型別物件參考reference型別,它并不等同于物件本身,可能是一個指向物件起始地址的參考指標,也可能是指向一個代表物件的句柄或者其他與此物件相關的位置)和returnAddress 型別(指向了一條位元組碼指令的地址), 這些資料型別在區域變數表中的存盤空間以區域變數槽(Slot)來表示,其中64位長度的long和double型別的資料會占用兩個變數槽,其余的資料型別只占用一個,區域變數表所需的記憶體空間在編譯期間完成分配,在方法運行期間不會改變區域變數表的大小( 變數槽的數量 ) , ??

  2. reference型別表示對一個物件實體的參考,一般來通過這個引 用做兩件事情,一是從根據參考直接或間接地查找到物件在Java堆中的資料存放的起始地址或索 引,二是根據參考直接或間接地查找到物件所屬資料型別在方法區中的存盤的型別資訊, ??

  3. returnAddress型別目前已經很少見了,它是為位元組碼指令jsr、jsr_w和ret服務的,指向了一條位元組碼指令的地址,某些很古老的Java虛擬機曾經使用這幾條指令來實作例外處理時的跳轉,但現在也已經全部改為采用例外表來代替了, ??

  4. 字面量就是指由字母、數字等構成的字串或者數值常量, ??

  5. 符號參考是編譯原理中的概念,是相對于直接參考來說的,主要包括了以下三類常量:類和介面的全限定名 、欄位的名稱和描述符、方法的名稱和描述符, ??

  6. 檔案格式驗證:第一階段要驗證位元組流是否符合Class檔案格式的規范,并且能被當前版本的虛擬機處理,該驗證階段的主要目的是保證輸入的位元組流能正確地決議并存盤于方法區之內,格式上符合描述一個Java型別資訊的要求,這階段的驗證是基于二進制位元組流進行的,只有通過了這個階段的驗證之后,這段位元組流才被允許進入Java虛擬機記憶體的方法區中進行存盤,所以后面的三個驗證階段全部是基于方法區的存盤結構上進行的,不會再直接讀取、操作位元組流了, ??

  7. 元資料驗證:第二階段是對位元組碼描述的資訊進行語意分析,以保證其描述的資訊符合《Java語言規范》的元資料資訊要求, ??

  8. 位元組碼驗證:第三階段主要目的是通過資料流分析和控制流分析,確定程式語意是合法的、符合邏輯的,在第二階段對元資料資訊中的資料型別校驗完畢以后,這階段就要對類的方法體(Class檔案中的Code屬性)進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為, ??

  9. 符號參考驗證:最后一個階段的校驗行為發生在虛擬機將符號參考轉化為直接參考的時候,這個轉化動作將在連接的第三階段——決議階段中發生,符號參考驗證可以看作是對類自身以外(常量池中的各種符號參考)的各類資訊進行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、欄位等資源, ??

  10. 符號參考:符號參考以一組符號來描述所參考的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可,符號參考與虛擬機實作的記憶體布局無關,參考的目標并不一定是已經加載到虛擬機記憶體當中的內容,各種虛擬機實作的記憶體布局可以各不相同,但是它們能接受的符號參考必須都是一致的, ??

  11. 直接參考:直接參考是可以直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的句柄,直接參考是和虛擬機實作的記憶體布局直接相關的,同一個符號參考在不同虛擬機實體上翻譯出來的直接參考一般不會相同,如果有了直接參考,那參考的目標必定已經在虛擬機的記憶體中存在, ??

  12. <clinit>():父類的<clinit>()方法先執行,由編譯器自動收集類中的所有類變數的賦值動作和靜態陳述句塊(static{}塊)中的陳述句合并產生的,編譯器收集的順序是由陳述句在源檔案中出現的順序決定的,靜態陳述句塊中只能訪問到定義在它之前的類變數,定義在它之后的變數可以賦值,但是不能訪問,介面與類不同只有當父介面中定義的變數被使用時,父介面才會被初始化,介面的實作類在初始化時也一樣不會執行介面的<clinit>()方法,Java虛擬機必須保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖同步,其他執行緒也不會再次進入<clinit>()方法,同一個類加載器下,一個型別只會被初始化一 次, ??

  13. 指標碰撞:假設Java堆中記憶體是絕對規整的,所有被使用過的記憶體都被放在一 邊,空閑的記憶體被放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閑空間方向挪動一段與物件大小相等的距離, ??

  14. 空閑串列:如果Java堆中的記憶體并不是規整的,已被使用的記憶體和空閑的記憶體相互交錯在一起,那就沒有辦法簡單地進行指標碰撞了,虛擬機就必須維護一個串列,記錄上哪些記憶體塊是可用的,在分 配的時候從串列中找到一塊足夠大的空間劃分給物件實體,并更新串列上的記錄, ??

  15. 句柄訪問:Java堆中將可能會劃分出一塊記憶體來作為句柄池,reference中存盤的就是物件的句柄地址,而句柄中包含了物件實體資料與型別資料各自具體的地址資訊 ??

  16. 直接指標:reference中存盤的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷, ??

  17. 黑色:表示物件已經被垃圾收集器訪問過,且這個物件的所有參考都已經掃描過,黑色的物件代 表已經掃描過,它是安全存活的,如果有其他物件參考指向了黑色物件,無須重新掃描一遍,黑色物件不可能直接(不經過灰色物件)指向某個白色物件, ??

  18. 白色:表示物件尚未被垃圾收集器訪問過,顯然在可達性分析剛剛開始的階段,所有的物件都是白色的,若在分析結束的階段,仍然是白色的物件,即代表不可達, ??

  19. 灰色:表示物件已經被垃圾收集器訪問過,但這個物件上至少存在一個參考還沒有被掃描過, ??

  20. 允許的值是一個大于0的毫秒數,收集器將盡力保證記憶體回識訓費的 時間不超過用戶設定值,垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間為代價換取的, ??

  21. 應當是一個大于0小于100的整數,也就是垃圾收集時間占總時間的比率,這個引數設定為N的話,表示用戶代碼執行時間與總執行時間之比為N:N+1,譬如把此引數設定為19,那允許的最大垃圾收集時間就占總時間的5%(即1/(1+19)),默認值為99,即允許最大1%(即1/(1+99))的垃圾收集時間, ??

  22. 如果不是由于兩個階段合并考慮,其實做重映射不需要按照物件圖的順序去做,只需線性地掃描整個堆來清理舊參考即可, ??

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/542665.html

標籤:Java

上一篇:CentOS中docker的使用

下一篇:垃圾收集器必問系列—G1

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more