JVM-GC:G1回收器和JVM(2)

分區(Heap Region,HR)

分區類型

  • 自由分區(Free Heap Region,FHR)
  • 新生代分區(Young Heap Region,YHR)
    • Eden
    • Survivor
  • 大對象分區(Humongous Heap Region,HHR)
    • 大對象頭分區
    • 大對象連續分區
  • 老年代分區(Old Heap Region,OHR)

分區大小設置(1M ~ 32M,且爲2的冪)

HR的大小直接影響分配和垃圾回收的效率。
大,HR能夠放更多對象,分配效率高,回收花費時間過長
小,分配效率低,易回收java

HR大小分配方式

  • 配置參數 G1HeapRegionSize,默認值爲0
  • G1HeapRegionSize 爲0,則開啓啓發式推斷

啓發式推斷

  • 依據 堆空間的最大值和最小值以及HR個數進行推斷
  • 設置Initial HeapSize(默認爲0)等價於設置Xms
  • 設置MaxHeapSize(默認爲96M)等價於設置Xmx
  • 計算大小的方式在HeapRegion.cpp中的setup_heap_region_size()
    • 因爲分區大小須要落在1M~32M之間,按照默認的分區個數(2048個)來計算,最大內存爲 64G,最小內存爲2G
      • 例如設置xms = 32G,xmx=128G,則xms算出的HR大小爲 16M,xmx算出的分區大小爲 64M > 32M,因此設置爲32M,二者取最大值,因此HR大小爲32M。因此分區個數動態範圍變化爲1024個到4096個之間。

大對象

  • 算出HR大小後,就能夠根據HR大小來判斷大對象,即只要 >= 1/2 heap_region_size 的都爲大對象

新生代大小分配

  • 直接設置 MaxNewSize (新生代最大值)NewSize(新生代最小值)
  • 若是設置了Xmn參數,等價於設置了 MaxNewSize = NewSize = Xmn
  • 若是既設置了最大值或最小值,又設置了NewRatio,則NewRatio不生效
  • 若是沒設置最大值或最小值,可是設置了NewRatio,則 MaxNewSize = NewSize = 堆空間/(NewRatio+1)
  • 若是沒設置最大值或最小值,或只設置了其中一個,那麼G1將根據參數G1MaxNewSizePercent(默認爲60)和G1NewSizePercent(默認爲5)佔整個堆空間的比例來計算最大最小值。
  • 若是MaxNewSize == NewSize,則說明新生代不會動態變化,在後續堆新生代垃圾回收的時候可能不能知足指望停頓的時間。

新生代的變化如何實現

  • G1有個線程專門抽樣處理預測新生代列表的長度應該多大,並動態調整
  • 使用分區列表。
    • 擴展時
      • 若是有空閒的分區列表,則能夠直接把空閒分區加入到新生代分區列表中。
      • 若是沒有的話,分配新的分區而後把它加入新生代分區列表中。

分配新的分區時,如何擴展,一次拓展多少內存

  • 參數 -XX:GCTimeRatio 表示GC與應用的耗費時間比,G1默認是9
  • GC時間/應用時間超過(GCTimeRatio+1)% 時,就能夠動態擴展;按照默認值,這個比例爲10%
  • 擴展的比例由G1ExpandByPercentOfAvailable(默認爲20)控制;即每次從未提交的內存中申請20%
  • 一次拓展的內存不能小於1M,最可能是目前已分配的一倍。

G1停頓預測模型

G1是個響應時間優先的GC算法

  • 參數MaxGCPauseMills控制(默認值200ms),該值爲指望值。G1會盡量靠近這個指望值,可是也有可能完不成。
  • G1根據這個模型來分析,此次須要回收多少個分區,能夠知足這個指望值。 例如過去N次回收時間和回收分區數量之間的關係。
  • G1利用衰減平均算法,給越近的數據以更高的權重,來計算數據的平均值。

卡表和位圖

  • 卡表(CardTable):在CMS中用來記錄內存對象應用關係。
  • 位圖(bitmap)
    • 設有HR1和HR2,HR1中有對象A,HR2中有對象B,且A.obj = B,這時候兩個HR就有引用關係了。這時候,咱們在HR1中,如何能引用到HR2呢?這時候位圖就登場了。
      • 設置位圖的方法,記錄兩個內存分區之間的引用關係。假設在32位的機器上(一個字爲32位),須要32KB的空間來描述一個分區。那麼咱們就在A中添加一個額外的指針,這個指針指向B的位圖。從這個指針指向的位圖就能找到被A引用的HR2對應的內存塊。這時候咱們只須要判斷位圖裏對應的位是否有1,有的話則認爲發生了引用。

對象頭

  • java代碼首先被編譯爲字節碼,在JVM執行的時候,才能肯定執行函數的地址,經過把java對象映射/封裝成一個C++對象。好比加一個對象頭,裏面指向一個對象,而這個對象存儲了java代碼的地址。
  • JVM設計了一個對象結構來描述java對象,結構分爲 對象頭(Header)實例數據(Instance Data)對齊填充(Padding)
  • 虛指針: 虛指針指向一個虛表,虛表裏存的是虛函數地址

對象頭分爲兩部分:標記信息元數據信息

  • jvm中的java對象都是繼承於oopDesc
class oopDesc {
  friend class VMStructs;
  friend class JVMCIVMStructs;
 private:
  // 對象頭 
  volatile markOop _mark;
  // 元數據
  union _metadata {
    // 對應的Klass對象 
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

複製代碼
  • 標記信息就位於 markOop,java對象頭的位格式以下
32 bits:
  --------
             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
             size:32 ------------------------------------------>| (CMS free block)
             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
 
  64 bits:
  --------
  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
  size:64 ----------------------------------------------------->| (CMS free block)
 
  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

複製代碼
  • age:分代年齡
  • biased_lock:是否偏向鎖(1是0非)
  • lock:鎖狀態標誌位
    • 當lock爲 11時,指針配合對象晉升時候發生的複製:
      1. 當新生代晉升爲老年代時
      2. 先分配空間
      3. 再把原有對象的全部數據都複製過去
      4. 最後修改對象引用指針
      5. lock設置爲marked,即11,表示對象已經被標記複製了,ptr指向新的地址。
      6. 當遍歷其餘引用對象時,若是發現被引用對象已經完成標記,則不用再複製對象,直接完成對象引用的更新便可。
  • promoted:當對象重新生代晉升到老年代的時候,若是晉升失敗,須要從新恢復對象頭。若是晉升成功,則promo_bits沒有意義。實際上只須要在如下三種狀況時才須要保存對象頭:
    • 使用了偏向鎖,而且偏向鎖被設置了。
    • 對象被上鎖了
    • 對象設置了hashcode
  • 原數據信息
    • 指向Klass對象,Klass對象是元數據對象。
    • GC在根結點發現了一個值(例如0x12345678),那麼JVM如何判斷這個是個當即數仍是地址呢。實際上垃圾回收器沒法判斷。
    • JVM會將這個值當作一個地址,轉換成OOP對象,再看看這個OOP是否含有Klass指針,若是有的話,認爲這個值是個指針,不然則認爲是個數。
    • 若是這個數正好和一個OOP地址相同,JVM同時維護了一個全局的OopMap,標記棧裏的數是當即數仍是個值。
    • 每一個InstanceKlass都維護了這個map(OopMapBlock)用於標記是OOP仍是當即數。

內存分配和管理

JVM如何管理內存的

  1. JVM經過操做系統的**系統調用(System Call)**申請,典型的就是mmap。
  2. 內存只能以**頁(Page Size)**的方式來映射,若是映射非Page Size整數倍的,就先進行內存對齊,再以Page Size的倍數進行映射。
  3. 告知操做系統,須要爲其**保留(reserve)**一段連續的虛擬內存,進程其餘分配內存的操做不得使用這段內存。
  4. **提交(commit)**虛擬地址,映射到真實的物理地址內存中,這塊內存就能夠正常使用。

JVM常見的對象類型

  1. ResourceObj:線程有個資源空間(Resource Area),裏面存放的就是ResourceObj,用於對JVM提供其餘功能的支持。
  2. StackObj:棧對象。
  3. ValueObj:值對象。在堆對象須要嵌套時使用
  4. AllStatic:靜態對象,全局對象,只有一個。JVM中的靜態對象的初始化,都是顯式調用靜態初始化函數。
  5. MetaspaceObj:元對象。例如InstanceKlass
  6. CHeapObj:堆空間的對象,由new/delete/free/malloc管理。

線程

JVM線程結構類

image.png

  1. JavaThread:執行java代碼的線程
    • java代碼的啓動會經過JNI_CreateJavaVM建立一個JavaThread運行
    • java的通常線程經過調用 Thread 中的**start()**方法,start()方法再經過JNI調用建立JavaThread對象。
  2. CompilerThread:執行JIT的線程
  3. WatcherThread:執行週期性任務,例如JVM內存抽樣等
  4. NameThread:JVM內部線程
  5. VMThread:JVM執行GC的同步線程,主要用於處理垃圾回收。
    • 若是是多線程回收,則啓動多個線程。
    • 若是是單線程回收,則使用VMThread
  6. ConcurrentGCThread:併發執行GC任務的線程
  7. WorkerThread:工做線程,在G1中使用了FlexibleWorkGang,這個線程是並行執行的,能夠認爲是一個線程池

JVM線程狀態

image.png

棧幀(frame)

  • 棧幀是虛擬機棧的組成元素,c++

  • 棧幀所包含的元素:算法

    img

  • 在GC第一步就是遍歷根,棧幀就是根元素之一,經過StactFrameStream遍歷根元素。多線程

句柄(handle)

jvm經過線程的資源區(handleArea)來管理全部的句柄。若是函數還在調用,那麼句柄有效,句柄關聯的對象也是活躍對象。 句柄的做用主要是用於管理本地代碼對堆上資源的調用。併發

如何管理句柄的生命週期

  • JVM引入HandleMark,一般HandleMark分配在棧上,在建立HandleMark的時候標記HandleArea有效
  • 在HandleMark析構的時候,從HandleArea中刪除這個對象的引用。
  • 全部的句柄都造成了一個鏈表,那麼訪問這個句柄鏈表就能夠得到本地代碼執行中對堆對象的引用。

G1參數介紹和注意事項

  • G1HeapRegionSize:指定堆分區大小。分區大小能夠指定,也能夠不指定;不指定時,由內存管理器啓發式推斷分區大小。
  • xms/xmx:指定堆空間的最小值/最大值。必定要正確設置xms/xmx,不然將使用默認配置,將影響分區大小推斷。
  • 在之前的內存管理器中(非G1),爲了防止新生代由於內存不斷地從新分配致使性能變低,一般設置Xmn或者NewRatio。可是G1中不要設置MaxNewSize、NewSize、Xmn和NewRatio。緣由有兩個,第一G1對內存的管理不是連續的,因此即便從新分配一個堆分區代價也不高,第二也是最重要的,G1的目標知足垃圾收集停頓,這須要G1根據停頓時間動態調整收集的分區,若是設置了固定的分區數,即G1不能調整新生代的大小,那麼G1可能不能知足停頓時間的要求。具體狀況本書後續還會繼續討論。
  • GCTimeRatio指的是GC與應用程序之間的時間佔比,默認值爲9,表示GC與應用程序時間佔比爲10%。增大該值將減小GC佔用的時間,帶來的後果就是動態擴展內存更容易發生;在不少狀況下10%已經很大,例如能夠將該值設置爲19,則表示GC時間不超過5%。
  • 根據業務請求變化的狀況,設置合適的擴展G1ExpandByPercentOfAvailable速率,保持效率。
  • JVM在對新生代內存分配管理時,還有一個參數就是保留內存G1ReservePercent(默認值是10),即在初始化,或者內存擴展/收縮的時候會計算更新有多少個分區是保留的,在新生代分區初始化的時候,在空閒列表中保留必定比例的分區不使用,那麼在對象晉升的時候就可使用了,因此能有效地減少晉升失敗的機率。這個值最大不超過50,即最多保留50%的空間,可是保留過多會致使新生代可用空間少,過少可能會增長新生代晉升失敗,那將會致使更爲複雜的串行回收
  • G1NewSizePercent是一個實驗參數,須要使用 -XX:+UnlockExperimentalVMOptions 才能改變選項。有實驗代表G1在回收Eden分區的時候,大概每GB須要100ms,因此能夠根據停頓時間,相應地調整。這個值在內存比較大的時候須要減小,例如32G能夠設置-XX:G1NewSizePercent = 3,這樣Eden至少保留大約1GB的空間,從而保證收集效率。
  • MaxGCPauseMillis指望停頓時間,可根據系統配置和業務動態調整。由於G1在垃圾收集的時候必定會收集新生代,因此須要配合新生代大小的設置來肯定,若是該值過小,連新生代都不能收集完成,則沒有任何意義,每次除了新生代以外只能多收集一個額外老生代分區。
  • 參數GCPauseIntervalMillisGCGC間隔時間,默認值爲0,GC啓發式推斷爲MaxGCPauseMillis + 1,設置該值必需要大於MaxGCPauseMillis。
  • 參數G1ConfidencePercentGC預測置信度,該值越小說明基於過去歷史數據的預測越準確,例如設置爲0則表示收集的分區基本和過去的衰減均值相關,無波動,因此能夠根據過去的衰減均值直接預測下一次預測的時間。反之該值越大,說明波動越大,越不許確,須要加上衰減方差來補償。
  • JVM中提供了一個對象對齊的值ObjectAlignmentInBytes,默認值爲8,須要明白該值對內存使用的影響,這個影響不只僅是在JVM對對象的分配上面,正如上面看到的它也會影響對象在分配時的標記狀況。注意這個值最少要和操做系統支持的位數一致才能提升對象分配的效率。因此32位系統最少是4,64位最少是8。通常不用修改該值
相關文章
相關標籤/搜索