深入理解JVM --- 垃圾收集算法終章

      網友投稿 643 2025-03-31

      大部分的Java對象只存活一小段時間,而存活下來的小部分Java對象則會存活很長一段時間

      pmd中Java對象生命周期的直方圖,紅色的表示被逃逸分析優化掉的對象

      之所以要提到這個假設,是因為它造就了Java虛擬機的分代回收思想

      就是將堆空間劃分為兩代,分別叫做新生代和老年代

      新生代用來存儲新建的對象

      當對象存活時間夠長時,則將其移動到老年代

      Java虛擬機可以給不同代使用不同的回收算法

      對于新生代,大部分的Java對象只存活一小段時間,那么便可以頻繁地采用耗時較短的垃圾回收算法,讓大部分的垃圾都能夠在新生代被回收掉。

      對于老年代,大部分的垃圾已經在新生代中被回收了,而在老年代中的對象有大概率會繼續存活。當真正觸發針對老年代的回收時,則代表這個假設出錯了,或者堆的空間已經耗盡了。

      這時候,Java虛擬機往往需要做一次全堆掃描,耗時也將不計成本。(當然,現代的垃圾回收器都在并發收集的道路上發展,來避免這種全堆掃描的情況。)

      今天來關注一下針對新生代的Minor GC

      首先,我們來看看

      Java虛擬機的堆劃分

      Java虛擬機將堆劃分為新生代和老年代

      其中,新生代又被劃分為Eden區,以及兩個大小相同的Survivor區。

      默認情況下,Java虛擬機采取的是一種動態分配的策略(對應Java虛擬機參數-XX:+UsePSAdaptiveSurvivorSizePolicy),根據生成對象的速率,以及Survivor區的使用情況動態調整Eden區和Survivor區的比例。

      當然,你也可以通過參數-XX:SurvivorRatio來固定這個比例

      但是需要注意的是,其中一個Survivor區會一直為空,因此比例越低浪費的堆空間將越高。

      當調用new指令時,它會在Eden區中劃出一塊作為存儲對象的內存

      由于堆空間是線程共享的,因此直接在這里邊劃空間是需要進行同步的

      否則,將有可能出現兩個對象共用一段內存的事故

      就相當于兩個司機(線程)同時將車停入同一個停車位,因而發生剮蹭事故。

      Java虛擬機的解決方法是為每個司機預先申請多個停車位,并且只允許該司機停在自己的停車位上。那么當司機的停車位用完了該怎么辦呢(假設這個司機代客泊車)?

      答案是:再申請多個停車位便可以了

      這項技術被稱之為TLAB(Thread Local Allocation Buffer,對應虛擬機參數-XX:+UseTLAB,默認開啟)。

      具體來說,每個線程可以向Java虛擬機申請一段連續的內存,比如2048字節,作為線程私有的TLAB

      這個操作需要加鎖,線程需要維護兩個指針(實際上可能更多,但重要也就兩個),一個指向TLAB中空余內存的起始位置,一個則指向TLAB末尾

      接下來的new指令,便可以直接通過指針加法(bump the pointer)來實現,即把指向空余內存位置的指針加上所請求的字節數。

      為什么不把bump the pointer翻譯成指針碰撞

      在英語中通常省略了bump up the pointer中的up

      在這個上下文中bump的含義應為“提高”

      另外一個例子是當我們發布軟件的新版本時,也會說bump the version number

      如果加法后空余內存指針的值仍小于或等于指向末尾的指針,則代表分配成功。否則,TLAB已經沒有足夠的空間來滿足本次新建操作。這個時候,便需要當前線程重新申請新的TLAB。

      當Eden區的空間耗盡了怎么辦?

      這個時候Java虛擬機便會觸發一次Minor GC,來收集新生代的垃圾存活下來的對象,則會被送到Survivor區。

      新生代共有兩個Survivor區,我們分別用from和to來指代

      其中to指向的Survivior區是空的。

      當發生Minor GC時,Eden區和from指向的Survivor區中的存活對象會被復制到to指向的Survivor區中,然后交換from和to指針,以保證下一次Minor GC時,to指向的Survivor區還是空的。

      Java虛擬機會記錄Survivor區中的對象一共被來回復制了幾次

      如果一個對象被復制的次數為15(對應虛擬機參數-XX:+MaxTenuringThreshold),那么該對象將被晉升(promote)至老年代

      另外,如果單個Survivor區已經被占用了50%(對應虛擬機參數-XX:TargetSurvivorRatio),那么較高復制次數的對象也會被晉升至老年代。

      總而言之,當發生Minor GC時,我們應用了復制算法,將Survivor區中的老存活對象晉升到老年代,然后將剩下的存活對象和Eden區的存活對象復制到另一個Survivor區中

      理想情況下,Eden區中的對象基本都死亡了,那么需要復制的數據將非常少,因此采用這種復制算法的效果極好。

      Minor GC的另外一個好處是不用對整個堆進行垃圾回收

      但是,它卻有一個問題,那就是老年代的對象可能引用新生代的對象

      也就是說,在標記存活對象的時候,我們需要掃描老年代中的對象。如果該對象擁有對新生代對象的引用,那么這個引用也會被作為GC Roots。

      這樣一來,豈不是又做了一次全堆掃描呢?

      卡表(Card Table)

      HotSpot給出的解決方案.

      將整個堆劃分為一個個大小為512字節的卡,并且維護一個卡表,用來存儲每張卡的一個標識位

      這個標識位代表對應的卡是否可能存有指向新生代對象的引用

      如果可能存在,那么我們就認為這張卡是臟的。

      在進行Minor GC的時候,便可以不用掃描整個老年代,而是在卡表中尋找臟卡,并將臟卡中的對象加入到Minor GC的GC Roots里。當完成所有臟卡的掃描之后,Java虛擬機便會將所有臟卡的標識位清零。

      由于Minor GC伴隨著存活對象的復制,而復制需要更新指向該對象的引用。

      因此,在更新引用的同時,又會設置引用所在的卡的標識位

      這個時候,可以確保臟卡中必定包含指向新生代對象的引用。

      在Minor GC之前,我們并不能確保臟卡中包含指向新生代對象的引用。其原因和如何設置卡的標識位有關。

      首先,如果想要保證每個可能有指向新生代對象引用的卡都被標記為臟卡,那么Java虛擬機需要截獲每個引用型實例變量的寫操作,并作出對應的寫標識位操作。

      這個操作在解釋執行器中比較容易實現

      但是在即時編譯器生成的機器碼中,則需要插入額外的邏輯。這也就是所謂的寫屏障(write barrier,注意不要和volatile字段的寫屏障混淆)。

      寫屏障需要盡可能地保持簡潔。這是因為我們并不希望在每條引用型實例變量的寫指令后跟著一大串注入的指令。

      因此,寫屏障并不會判斷更新后的引用是否指向新生代中的對象,而是寧可錯殺,不可放過,一律當成可能指向新生代對象的引用。

      這么一來,寫屏障便可精簡為下面的偽代碼

      這里右移9位相當于除以512,Java虛擬機便是通過這種方式來從地址映射到卡表中的索引的。

      最終,這段代碼會被編譯成一條移位指令和一條存儲指令。

      CARD_TABLE [this address >> 9] = DIRTY;

      1

      雖然寫屏障不可避免地帶來一些開銷,但是它能夠加大Minor GC的吞吐率( 應用運行時間/(應用運行時間+垃圾回收時間) )

      總的來說還是值得的。不過,在高并發環境下,寫屏障又帶來了虛共享(false sharing)問題

      在介紹對象內存布局中我曾提到虛共享問題,講的是幾個volatile字段出現在同一緩存行里造成的虛共享。這里的虛共享則是卡表中不同卡的標識位之間的虛共享問題。

      在HotSpot中,卡表是通過byte數組來實現的。對于一個64字節的緩存行來說,如果用它來加載部分卡表,那么它將對應64張卡,也就是32KB的內存。

      如果同時有兩個Java線程,在這32KB內存中進行引用更新操作,那么也將造成存儲卡表的同一部分的緩存行的寫回、無效化或者同步操作,因而間接影響程序性能。

      為此,HotSpot引入了一個新的參數-XX:+UseCondCardMark,來盡量減少寫卡表的操作。其偽代碼如下所示:

      if (CARD_TABLE [this address >> 9] != DIRTY) CARD_TABLE [this address >> 9] = DIRTY;

      1

      2

      總結

      Java虛擬機將堆分為新生代和老年代,并且對不同代采用不同的垃圾回收算法。

      其中,新生代分為Eden區和兩個大小一致的Survivor區,并且其中一個Survivor區是空的。

      在只針對新生代的Minor GC中,Eden區和非空Survivor區的存活對象會被復制到空的Survivor區中,當Survivor區中的存活對象復制次數超過一定數值時,它將被晉升至老年代。

      因為Minor GC只針對新生代進行垃圾回收,所以在枚舉GC Roots的時候,它需要考慮從老年代到新生代的引用。為了避免掃描整個老年代,Java虛擬機引入了名為卡表的技術,大致地標出可能存在老年代到新生代引用的內存區域。

      Java虛擬機的分代垃圾回收是基于大部分對象只存活一小段時間,小部分對象卻存活一大段時間的假設的。

      然而,現實情況中并非每個程序都符合前面提到的假設。如果一個程序擁有中等生命周期的對象,并且剛移動到老年代便不再使用,那么將給默認的垃圾回收策略造成極大的麻煩。

      下面這段程序將生成64G的Java對象。并且,我通過ALIVE_OBJECT_SIZE這一變量來定義同時存活的Java對象的大小。這也是一種對于垃圾回收器來說比較直觀的生命周期。

      當我們使用Java 8的默認GC,并且將新生代的空間限制在100M時,試著估算當ALIVE_OBJECT_SIZE為多少時,這段程序不會觸發Full GC(提示一下,如果Survivor區沒法存儲所有存活對象,將發生什么。)。實際運行情況又是怎么樣的?

      // Run with java -XX:+PrintGC -Xmn100M -XX:PretenureSizeThreshold=10000 LifetimeTest // You may also try with -XX:+PrintHeapAtGC,-XX:-UsePSAdaptiveSurvivorSizePolicy or -XX:SurvivorRatio=N public class LifetimeTest { private static final int K = 1024; private static final int M = K * K; private static final int G = K * M; private static final int ALIVE_OBJECT_SIZE = 32 * M; public static void main(String[] args) { int length = ALIVE_OBJECT_SIZE / 64; ObjectOf64Bytes[] array = new ObjectOf64Bytes[length]; for (long i = 0; i < G; i++) { array[(int) (i % length)] = new ObjectOf64Bytes(); } } } class ObjectOf64Bytes { long placeholder0; long placeholder1; long placeholder2; long placeholder3; long placeholder4; long placeholder5; }

      1

      2

      3

      4

      5

      6

      7

      8

      9

      10

      11

      12

      13

      14

      15

      16

      17

      18

      19

      20

      深入理解JVM --- 垃圾收集算法終章

      21

      22

      23

      24

      25

      26

      參考

      http://psy-lob-saw.blogspot.com/2014/10/the-jvm-write-barrier-card-marking.html

      https://blogs.oracle.com/dave/false-sharing-induced-by-card-table-marking

      http://openjdk.java.net/jeps/291

      https://www.zhihu.com/question/287945354/answer/458761494

      深入拆解Java虛擬機

      深入理解Java虛擬機

      Java JVM

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:word如何實現表格自動到下頁(如何讓表格自動到下一頁)
      下一篇:MindArmour 使用
      相關文章
      亚洲精品和日本精品| 99亚洲男女激情在线观看| 国产精品V亚洲精品V日韩精品 | 亚洲成A人片777777| 国产精品亚洲玖玖玖在线观看| 国产亚洲精彩视频| 日韩亚洲人成在线综合| 亚洲性无码AV中文字幕| 亚洲第一页在线播放| 亚洲短视频在线观看| 亚洲成人福利在线| 亚洲一级在线观看| 亚洲国产人成在线观看| 亚洲一区二区三区播放在线| 久久久久se色偷偷亚洲精品av| 亚洲不卡中文字幕| 亚洲va久久久久| 亚洲精品欧美综合四区 | 亚洲av成人无码久久精品| 国精无码欧精品亚洲一区 | 亚洲成a人片在线观看日本| 亚洲国产精品线在线观看| 亚洲伦理一区二区| 亚洲狠狠狠一区二区三区| 亚洲国产成人在线视频| 2020年亚洲天天爽天天噜| 国产午夜亚洲精品| 亚洲av色香蕉一区二区三区 | 亚洲av综合av一区二区三区| 精品国产日韩亚洲一区在线| 亚洲国产精品丝袜在线观看| 国产亚洲精品AA片在线观看不加载 | 亚洲好看的理论片电影| 久久精品亚洲一区二区三区浴池| 久久亚洲AV成人无码软件 | 在线观看亚洲精品国产| 亚洲av综合av一区| 亚洲视屏在线观看| 色老板亚洲视频免在线观| 亚洲AV无码一区二区三区性色| 亚洲国产aⅴ综合网|