怎么去除網址(怎樣去除圖片上的網址)
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
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小時內刪除侵權內容。