主頁 > 後端開發 > JVM-垃圾收集器與記憶體分配策略

JVM-垃圾收集器與記憶體分配策略

2021-11-13 12:37:07 後端開發

垃圾收集器與記憶體分配策略

一個垃圾收集器除了垃圾收集這個本職作業之外,它還要負責堆的管理與布局、物件的分配、與解釋器的協作、與編譯器的協作、與監控子系統協作等職責,其中至少堆的管理和物件的分配這部分功能是Java虛擬機能夠正常運作的必要支持,是一個最小化功能的垃圾收集器也必須實作的內容,

垃圾收集關注的是堆和方法區的記憶體如何管理,

程式計數器、虛擬機堆疊、本地方法堆疊3個區域隨執行緒而生,隨執行緒而滅,堆疊中的堆疊幀隨著方法的進入和退出而有條不紊地執行著出堆疊和入堆疊操作,每一個堆疊幀中分配多少記憶體基本上是在類結構確定下來時就已知的(盡管在運行期會由即時編譯器進行一些優化,但在基于概念模型的討論里,大體上可以認為是編譯期可知的),因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內就不需要過多考慮如何回收的問題,當方法結束或者執行緒結束時,記憶體自然就跟隨著回收了,

判斷物件存活狀態

參考計數演算法

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

客觀來說,這種方法只需要占據一部分額外記憶體即可實作進行計數,微軟COM技術,Python語言等等一些應用中都有參考計數法進行記憶體管理,但是Java的主流虛擬機都沒有選用這種演算法,因為有很多例外情況需要判斷,比如說單純的參考計數很難解決物件之間相互參考的問題,(A->B,B->A這樣的話雙方計數器都為1,除此之外再無參考,也不被訪問)

可達性分析演算法

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

image-20211106144914392

Java中,可固定作GC Roots的物件有:虛擬機堆疊中參考的物件,方法區中類靜態屬性參考的物件,方法區中常量參考的物件,本地方法堆疊中JNI參考的物件,所有被同步鎖持有的物件,反應JVM內部情況的JMXBean,JVMTI中注冊的回呼,本地代碼快取等,

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

參考

JDK1.2之前,Java中只有“被參考”和“未被參考”兩種狀態,

JDK1.2之后,參考分為:強參考(Strongly Re-ference)、軟參考(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()方法是物件逃離死亡命運的最后一次機會,稍后收集器將會對等待佇列中的物件進行第二次小規模的標記,如果物件在finalize()方法中拯救了自己——重新與參考鏈上任何一個物件建立關聯即可,

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;   //該物件被參考
}

可以通過重寫finalize()方法實作拯救一個物件,(對于一個物件,finalize()方法只會被系統呼叫一次)

不建議使用finalize()方法進行拯救函式,通過try-finally或者其他作業方式都可以做到,

回收方法區

方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的型別,回收廢棄常量與回收Java堆中的物件非常類似,

判斷一個廢棄常量和回收堆中物件類似:舉例,常量池中有“java”字串,如果當前系統中沒有任何一個字串物件的值為“java”,且虛擬機中也沒有其他地方參考此常量,這時候發生記憶體回收且垃圾收集器判斷確有必要的話,這個“java”常量將會被系統清理出常量池,常量池中其他類也類似,

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

  • 該類所有的實體都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實體,
  • 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的,
  • 該類對應的java.lang.Class物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,

Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和物件一樣,沒有參考了就必然會回收,關于是否要對型別進行回收,HotSpot虛擬機提供了-Xnoclassgc引數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看類加載和卸載資訊,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機中使用,-XX+TraceClassUnLoading引數需要FastDebug版的虛擬機支持,

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


垃圾收集演算法

從如何判定物件消亡的角度出發,垃圾收集演算法可以劃分為“參考計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類,這兩類也常被稱作“直接垃圾收集”和“間接垃圾收集”,不過主流虛擬機中均采用的是追蹤式垃圾收集,

分代收集理論

兩個分代假說:

  • 弱分代假說:絕大多數物件都是朝生夕滅的,
  • 強分代假說:熬過越多次垃圾收集程序的物件就越難以消亡,

這兩個分代假說奠定了多款常用的垃圾收集器的一致的設計原則:將Java堆劃分出不同的區域,然后將回收物件依據其年齡(年齡即物件熬過垃圾收集程序的次數)分配到不同的區域中存盤,

如果一個區域中大多數物件都是朝生夕滅,難以熬過垃圾收集程序的話,那么把它們集中放在一起,每次回收時只關注如何保留少量存活而不是去標記那些大量將要被回收的物件,就能以較低代價回收到大量的空間;如果剩下的都是難以消亡的物件,那把它們集中放在一塊,虛擬機便可以使用較低的頻率來回收這個區域,這就同時兼顧了垃圾收集的時間開銷和記憶體的空間有效利用,

現在的商用Java虛擬機里,設計者一般至少會把Java堆劃分為新生代(Young Generation)和老年代(Old Generation)兩個區域,在新生代中,每次垃圾收集時都發現有大批物件死去,而每次回收后存活的少量物件,將會逐步晉升到老年代中存放,

但是分代收集并非只是簡單的劃分記憶體區域,假如說要進行一次新生代區域內的收集,新生代區域內的物件是有可能被老年代參考的,因此除了固定的GC Roots外,還需要對老年代的所有物件進行一次遍歷來確保可達性分析結果的正確性,這樣的話會對記憶體回收帶來很大的性能負擔,因此分代收集理論添加了第三條經驗法則:

跨代參考假說(Intergenerational Reference Hypothesis):跨代參考相對于同代參考來說僅占極少數,

存在互相參考關系的兩個物件,是應該傾向于同時生存或者同時消亡的,舉個例子,如果某個新生代物件存在跨代參考,由于老年代物件難以消亡,該參考會使得新生代物件在收集時同樣得以存活,進而在年齡增長之后晉升到老年代中,這時跨代參考也隨即被消除了,

依據這條假說,我們就不應再為了少量的跨代參考去掃描整個老年代,也不必浪費空間專門記錄每一個物件是否存在及存在哪些跨代參考,只需在新生代上建立一個全域的資料結構(該結構被稱為“記憶集”,Remembered Set),這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊記憶體會存在跨代參考,此后當發生Minor GC(只收集新生代)時,只有包含了跨代參考的小塊記憶體里的物件才會被加入到GC Roots進行掃描,雖然這種方法需要在物件改變參考關系(如將自己或者某個屬性賦值)時維護記錄資料的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是劃算的,

一些專有名詞:

部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,其中又分為:

  • 新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集,
  • 老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集,目前只有CMS收集器會有單獨收集老年代的行為,另外請注意“Major GC”這個說法現在有點混淆,在不同資料上常有不同所指,讀者需按背景關系區分到底是指老年代的收集還是整堆收集,
  • 混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集,目前只有G1收集器會有這種行為

整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集,

值得注意的是,分代收集理論也有其缺陷,最新出現(或在實驗中)的幾款垃圾收集器都展現出了面向全區域收集設計的思想,或者可以支持全區域不分代的收集的作業模式,

標記-清除演算法

演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成后,統一回收掉所有被標記的物件,也可以反過來,標記存活的物件,統一回收所有未被標記的物件,(標記即判斷物件是否屬于垃圾)

它的主要缺點有兩個:第一個是執行效率不穩定,如果Java堆中包含大量物件,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個程序的執行效率都隨物件數量增長而降低;第二個是記憶體空間的碎片化問題,標記、清除之后會產生大量不連續的記憶體碎片,空間碎片太多可能會導致當以后在程式運行程序中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作,

演算法執行程序:

image-20211109220704459

標記-復制演算法

將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊,當這一塊的記憶體用完了,就將還存活著的物件復制到另外一塊上面,然后再把已使用過的記憶體空間一次清理掉,

演算法執行程序:

image-20211109221500313

如果記憶體中多數物件都是存活的,這種演算法將會產生大量的記憶體間復制的開銷,但對于多數物件都是可回收的情況,演算法需要復制的就是占少數的存活物件,而且每次都是針對整個半區進行記憶體回收,分配記憶體時也就不用考慮有空間碎片的復雜情況,只要移動堆頂指標,按順序分配即可,這樣實作簡單,運行高效,不過其缺陷也顯而易見,這種復制回收演算法的代價是將可用記憶體縮小為了原來的一半,空間浪費未免太多了一點,

現在的商用Java虛擬機大多都優先采用了這種收集演算法去回收新生代,IBM公司曾有一項專門研究對新生代“朝生夕滅”的特點做了更量化的詮釋——新生代中的物件有98%熬不過第一輪收集,因此并不需要按照1∶1的比例來劃分新生代的記憶體空間,

HotSpot虛擬機的Serial、ParNew等新生代收集器均采用了Appel式回收策略來設計新生代的記憶體布局[1],Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配記憶體只使用Eden和其中一塊Survivor,發生垃圾搜集時,將Eden和Survivor中仍然存活的物件一次性復制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間,HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,

Appel式回識訓有一個充當罕見情況的“逃生門”的安全設計,當Survivor空間不足以容納一次Minor GC之后存活的物件時,就需要依賴其他記憶體區域(實際上大多就是老年代)進行分配擔保(Handle Promotion),

標記-整理演算法

標記-復制演算法在面對存活率較高的情況時需要進行大量復制操作,效率將會降低,老年代一般不能直接選用這種演算法,

標記-整理演算法的標記程序仍然與“標記-清除”演算法一樣,但后續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向記憶體空間一端移動,然后直接清理掉邊界以外的記憶體,

回收程序:

image-20211109230107984

至于什么時候采取哪種演算法,則需要根據情況討論:

介紹一下吞吐量

吞吐量 = CPU在用戶應用程式運行的時間 / (CPU在用戶應用程式運行的時間 + CPU垃圾回收的時間)

程式運行時間可以理解為記憶體分配和訪問的時間(還有其他的操作),

標記-清除演算法,即使不移動物件會使得收集器的效率提升一些,但因記憶體分配和訪問相比垃圾收集頻率要高得多,這部分的耗時增加,總吞吐量仍然是下降的,

HotSpot虛擬機里面關注吞吐量的Parallel Scavenge收集器是基于標記-整理演算法的,而關注延遲的CMS收集器則是基于標記-清除演算法的,

還有一種“和稀泥式”解決方案可以不在記憶體分配和訪問上增加太大額外負擔,做法是讓虛擬機平時多數時間都采用標記-清除演算法,暫時容忍記憶體碎片的存在,直到記憶體空間的碎片化程度已經大到影響物件分配時,再采用標記-整理演算法收集一次,以獲得規整的記憶體空間,前面提到的基于標記-清除演算法的CMS收集器面臨空間碎片過多時采用的就是這種處理辦法,

HotSpot的演算法實作

固定可作為GC Roots的節點主要在全域性的參考(例如常量或類靜態屬性)與執行背景關系(例如堆疊幀中的本地變數表)中,目前所有收集器在根節點列舉這一步驟時都是需要暫停用戶執行緒的,

根節點列舉必須在一個能保障一致性的快照中才得以進行——這里“一致性”的意思是整個列舉期間執行子系統看起來就像被凍結在某個時間點上,不會出現分析程序中,根節點集合的物件參考關系還在不斷變化的情況,若這點不能滿足的話,分析結果準確性也就無法保證,

目前主流Java虛擬機使用的都是準確式垃圾收集,當用戶執行緒停頓下來之后,并不需要一個不漏地檢查完所有執行背景關系和全域的參考位置,虛擬機應當是有辦法直接得到哪些地方存放著物件參考的,在HotSpot的解決方案里,是使用一組稱為OopMap的資料結構來達到這個目的,一旦類加載動作完成的時候,HotSpot就會把物件內什么偏移量上是什么型別的資料計算出來,在即時編譯程序中,也會在特定的位置記錄下堆疊里和暫存器里哪些位置是參考,這樣收集器在掃描時就可以直接得知這些資訊了,并不需要真正一個不漏地從方法區等GC Roots開始查找,

[Verified Entry Point]
0x026eb730: mov %eax,-0x8000(%esp)
…………
;; ImplicitNullCheckStub slow case
0x026eb7a9: call 0x026e83e0 ; OopMap{ebx=Oop [16]=Oop off=142}
							; *caload
							; - java.lang.String::hashCode@48 (line 1489)
							; {runtime_call}
0x026eb7ae: push $0x83c5c18 ; {external_word}
0x026eb7b3: call 0x026eb7b8
0x026eb7b8: pusha
0x026eb7b9: call 0x0822bec0 ; {runtime_call}
0x026eb7be: hlt

String::hashCode()方法的本地代碼,可以看到在0x026eb7a9處的call指令有OopMap記錄,它指明了EBX暫存器和堆疊中偏移量為16的記憶體區域中各有一個普通物件指標(Ordinary Object Pointer,OOP)的參考,有效范圍為從call指令開始直到0x026eb730(指令流的起始位置)+142(OopMap記錄的偏移量)=0x026eb7be,即hlt指令為止,

安全點

物件參考的變化會導致OopMap的變化,但是我們不可能每條指令都進行更新,這樣的話太過影響性能,因此通過設定安全點,避免頻繁更新OopMap,只在達到安全點的位置才更新OopMap,用戶執行緒在安全點停頓,GC在安全點進行,采取主動式中斷,執行緒輪詢中斷標志位,當標志位為真時,在最近的安全點主動中斷掛起,

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

安全區域

設定安全點之后,在用戶執行緒執行程序中就會遇到安全點,進入到垃圾收集,但是當執行緒處于Sleep或Blocked狀態時,CPU沒有分配處理時間,執行緒無法回應虛擬機的中斷請求,不能走到安全的地方中斷掛起,這時就引入了安全區域,

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

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

記憶集與卡表

記憶集是一種用于記錄從非收集區域指向收集區域的指標集合的抽象資料結構,

在垃圾收集的場景中,收集器只需要通過記憶集判斷出某一塊非收集區域是否存在有指向了收集區域的指標就可以了,并不需要了解這些跨代指標的全部細節,三種記錄精度:

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

第三種“卡精度”所指的是用一種稱為“卡表”(Card Table)的方式去實作記憶集,

卡表最簡單的形式可以只是一個位元組陣列,而HotSpot虛擬機確實也是這樣做的,以下這行代碼是HotSpot默認的卡表標記邏輯:

CARD_TABLE [this address >> 9] = 0;

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

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

寫屏障

卡表元素變臟的時刻,即該時刻有其他分代區域的物件參考了當前區域物件,該如何更新卡表元素呢?

HotSpot虛擬機里是通過寫屏障(Write Barrier)技術維護卡表狀態,

寫屏障可以看作在虛擬機層面對“參考型別欄位賦值”這個動作的AOP切面,在參考物件賦值時會產生一個環形(Around)通知,供程式執行額外的動作,也就是說賦值的前后都在寫屏障的覆寫范疇內,在賦值前的部分的寫屏障叫作寫前屏障(Pre-Write Barrier),在賦值后的則叫作寫后屏障(Post-Write Barrier),

應用寫屏障后,虛擬機就會為所有賦值操作生成相應的指令,一旦收集器在寫屏障中增加了更新卡表操作,無論更新的是不是老年代對新生代物件的參考,每次只要對參考進行更新,就會產生額外的開銷,不過這個開銷與Minor GC時掃描整個老年代的代價相比還是低得多的,

假設處理器的快取行大小為64位元組,由于一個卡表元素占1個位元組,64個卡表元素將共享同一個快取行,這64個卡表元素對應的卡頁總的記憶體為32KB(64×512位元組),也就是說如果不同執行緒更新的物件正好處于這32KB的記憶體區域內,就會導致更新卡表時正好寫入同一個快取行而影響性能,為了避免偽共享問題,一種簡單的解決方案是不采用無條件的寫屏障,而是先檢查卡表標記,只有當該卡表元素未被標記過時才將其標記為變臟,代碼如下:

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

在JDK 7之后,HotSpot虛擬機增加了一個新的引數-XX:+UseCondCardMark,用來決定是否開啟卡表更新的條件判斷,開啟會增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有性能損耗,是否打開要根據應用實際運行情況來進行測驗權衡,

并發的可達性分析(增量更新,原始快照)

可達性分析演算法理論上要求全程序都基于一個能保障一致性的快照中才能夠進行分析,這意味著必須全程凍結用戶執行緒的運行,在根節點列舉這個步驟中,由于GC Roots相比起整個Java堆中全部的物件畢竟還算是極少數,且在各種優化技巧(如OopMap)的加持下,它帶來的停頓已經是非常短暫且相對固定(不隨堆容量而增長)的了,可從GC Roots再繼續往下遍歷物件圖,這一步驟的停頓時間就必定會與Java堆容量直接成正比例關系了:堆越大,存盤的物件越多,物件圖結構越復雜,要標記更多物件而產生的停頓時間自然就更長,這聽起來是理所當然的事情,

引入三色標記:

  • 白色:表示物件尚未被垃圾收集器訪問過,顯然在可達性分析剛剛開始的階段,所有的物件都是白色的,若在分析結束的階段,仍然是白色的物件,即代表不可達,
  • 黑色:表示物件已經被垃圾收集器訪問過,且這個物件的所有參考都已經掃描過,黑色的物件代表已經掃描過,它是安全存活的,如果有其他物件參考指向了黑色物件,無須重新掃描一遍,黑色物件不可能直接(不經過灰色物件)指向某個白色物件,
  • 灰色:表示物件已經被垃圾收集器訪問過,但這個物件上至少存在一個參考還沒有被掃描過,

并發情況下,可達性分析是可能出現問題的,造成“物件消失”,這個問題沒有贅述,可以自己查資料,

造成該問題需要滿足以下兩個條件:

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

解決并發掃描時物件消失的問題,有兩種解決方案:增量更新和原始快照,

增量更新要破壞的是第一個條件,當黑色物件插入新的指向白色物件的參考關系時,就將這個新插入的參考記錄下來,等并發掃描結束之后,再將這些記錄過的參考關系中的黑色物件為根,重新掃描一次,這可以簡化理解為,黑色物件一旦新插入了指向白色物件的參考之后,它就變回灰色物件了,

原始快照要破壞的是第二個條件,當灰色物件要洗掉指向白色物件的參考關系時,就將這個要洗掉的參考記錄下來,在并發掃描結束之后,再將這些記錄過的參考關系中的灰色物件為根,重新掃描一次,這也可以簡化理解為,無論參考關系洗掉與否,都會按照剛剛開始掃描那一刻的物件圖快照來進行搜索,

以上無論是對參考關系記錄的插入還是洗掉,虛擬機的記錄操作都是通過寫屏障實作的,在HotSpot虛擬機中,增量更新和原始快照這兩種解決方案都有實際應用,譬如,CMS是基于增量更新來做并發標記的,G1、Shenandoah則是用原始快照來實作,

經典垃圾收集器

Serial收集器:單執行緒作業的收集器,在它進行垃圾收集時,必須暫停其他作業執行緒,直至收集結束,

image-20211110212026654

迄今為止,它依然是HotSpot虛擬機運行在客戶端模式下的默認新生代收集器

優點:簡單高效,目前在用戶桌面的應用場景以及近年流行的部分微服務應用中使用,因為這些應用分配給虛擬機管理的記憶體一般來說不會很大,幾十兆甚至一兩百兆的新生代,垃圾收集的停頓時間可控在十幾,幾十毫秒,不影響體驗,

ParNew收集器實質上是Serial收集器的多執行緒并行版本,

除了同時使用多條執行緒進行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制引數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一致,

image-20211110212303992

它是不少運行在服務端模式下的HotSpot虛擬機,尤其是JDK 7之前的遺留系統中首選的新生代收集器,其中有一個與功能、性能無關但其實很重要的原因是:除了Serial收集器外,目前只有它能與CMS收集器配合作業,

而G1是一個面向全堆的收集器,不再需要其他新生代收集器的配合作業,自JDK 9開始,ParNew加CMS收集器的組合就不再是官方推薦的服務端模式下的收集器解決方案了,

Parallel Scavenge收集器

新生代收集器,基于標記-復制演算法實作,關注吞吐量,

垃圾收集停頓時間越短就越適合需要與用戶互動或需要保證服務回應質量的程式,良好的回應速度能提升用戶體驗;而高吞吐量則可以最高效率地利用處理器資源,盡快完成程式的運算任務,主要適合在后臺運算而不需要太多互動的分析任務,

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

具有自適應調節策略:-XX:+UseAdaptiveSizePolicy(開關)

Serial Old收集器

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

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多執行緒并發收集,基于標記-整理演算法實作,這個收集器是直到JDK 6時才開始提供的,直到Parallel Old收集器出現后,“吞吐量優先”收集器終于有了比較名副其實的搭配組合,在注重吞吐量或者處理器資源較為稀缺的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器這個組合,

image-20211111141044646

CMS收集器(Concurrent Mark Sweep)

一種以獲取最短回收停頓時間為目標的收集器,目前很大一部分的Java應用集中在互聯網網站或者基于瀏覽器的B/S系統的服務端上,這類應用通常都會較為關注服務的回應速度,希望系統停頓時間盡可能短,以給用戶帶來良好的互動體驗,CMS收集器就非常符合這類應用的需求,

它的運作程序相對于前面幾種收集器來說要更復雜一些,整個程序分為四個步驟,包括:

1)初始標記(CMS initial mark)
2)并發標記(CMS concurrent mark)
3)重新標記(CMS remark)
4)并發清除(CMS concurrent sweep)

其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”,初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快;并發標記階段就是從GC Roots的直接關聯物件開始遍歷整個物件圖的程序,這個程序耗時較長但是不需要停頓用戶執行緒,可以與垃圾收集執行緒一起并發運行;而重新標記階段則是為了修正并發標記期間,因用戶程式繼續運作而導致標記產生變動的那一部分物件的標記記錄(關于增量更新的講解),這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比并發標記階段的時間短;最后是并發清除階段,清理洗掉掉標記階段判斷的已經死亡的物件,由于不需要移動存活物件,所以這個階段也是可以與用戶執行緒同時并發的,

由于在整個程序中耗時最長的并發標記和并發清除階段中,垃圾收集器執行緒都可以與用戶執行緒一起作業,所以從總體上來說,CMS收集器的記憶體回收程序是與用戶執行緒一起并發執行的,

image-20211111142348832

優點:并發收集、低停頓,

缺點:

  • 對處理器資源非常敏感,CMS默認啟動的回收執行緒數是(處理器核心數量+3)/4,在處理器核心數不足4個時,CMS對用戶程式的影響就可能變得很大,
  • 無法處理“浮動垃圾”,在CMS的并發標記和并發清理階段,用戶執行緒是還在繼續運行的,程式在運行自然就還會伴隨有新的垃圾物件不斷產生,但這一部分垃圾物件是出現在標記程序結束以后,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉,這一部分垃圾就稱為“浮動垃圾”,此外,CMS收集老年代時無法等待老年代填滿才收集,因為并發程序需要給用戶執行緒預留足夠的記憶體空間,JDK5設定的默認老年代空間閾值為68%,JDK6時提高到了92%,如果CMS運行期間預留的記憶體無法滿足程式分配新物件的需要,則會出現“Concurrent Mode Failure”并發失敗,這時候的后備選擇是臨時啟用Serial Old收集器重新進行老年代的垃圾收集,因此閾值設定還是比較重要的,
  • 標記-清除演算法會造成大量的空間碎片,可能會出現老年代還有很多剩余空間,但是不得不提前觸發Full GC,解決這個問題出現了兩種方案:一種是當CMS不得不進行Full GC時開啟記憶體碎片的合并整理,這時候移動存活物件無法并發(在Shenandoah和ZGC出現前),另一種是在進行若干次不整理空間的Full GC后,下一次進入Full GC前整理空間,這兩種方案都是通過兩個開關引數決定,JDK9之后廢棄,

G1收集器

G1收集器是垃圾收集器技術發展歷史上的里程碑式的成果,G1是一款主要面向服務端應用的垃圾收集器,JDK 9發布之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成為服務端模式下的默認垃圾收集器,而CMS則淪落至被宣告為不推薦使用(Deprecate)的收集器,從整體來看是基于“標記-整理”演算法實作的,從區域來看是基于“標記-復制”演算法實作的,

G1面向堆記憶體任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬于哪個分代,而是哪塊記憶體中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式,可指定停頓時間,G1關注于吞吐量和延遲之間的平衡,

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

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

G1仍保留新生代和老年代的概念,但是新生代和老年代不再是固定的了,它們是一系列區域的動態集合,

G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作為單次回收的最小單元,即每次收集到的記憶體空間都是Region大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾收集,更具體的處理思路是讓G1收集器去跟蹤各個Region里面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然后在后臺維護一個優先級串列,每次根據用戶設定允許的收集停頓時間(使用引數-XX:MaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來,

將Java堆分成多個獨立Region后,Region里面存在的跨Region參考物件如何解決?解決的思路我們已經知道:使用記憶集避免全堆作為GC Roots掃描,但在G1收集器上記憶集的應用其實要復雜很多,它的每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region指向自己的指標,并標記這些指標分別在哪些卡頁的范圍之內,G1的記憶集在存盤結構的本質上是一種哈希表,Key是別的Region的起始地址,Value是一個集合,里面存盤的元素是卡表的索引號,這種“雙向”的卡表結構(卡表是“我指向誰”,這種結構還記錄了“誰指向我”)比原來的卡表實作起來更復雜,同時由于Region數量比傳統收集器的分代數量明顯要多得多,因此G1收集器要比其他的傳統垃圾收集器有著更高的記憶體占用負擔,根據經驗,G1至少要耗費大約相當于Java堆容量10%至20%的額外記憶體來維持收集器作業,

另外,面對垃圾收集程序中,用戶改變物件參考關系時,G1采用原始快照演算法實作,

此外,垃圾收集對用戶執行緒的影響還體現在回收程序中新創建物件的記憶體分配上,程式要繼續運行就肯定會持續有新物件被創建,G1為每一個Region設計了兩個名為TAMS(Top at Mark Start)的指標,把Region中的一部分空間劃分出來用于并發回收程序中的新物件分配,并發回收時新分配的物件地址都必須要在這兩個指標位置以上,G1收集器默認在這個地址以上的物件是被隱式標記過的,即默認它們是存活的,不納入回收范圍,與CMS中的“Concurrent Mode Failure”失敗會導致Full GC類似,如果記憶體回收的速度趕不上記憶體分配的速度,G1收集器也要被迫凍結用戶執行緒執行,導致Full GC而產生長時間“Stop The World”,

收集程序:

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

G1收集器除了并發標記外,其余階段也是要完全暫停用戶執行緒的,換言之,它并非純粹地追求低延遲,官方給它設定的目標是在延遲可控的情況下獲得盡可能高的吞吐量,所以才能擔當起“全功能收集器”的重任與期望,而增加停頓時間能最大程度地提高垃圾收集的效果,

image-20211111165759431

從G1開始,最先進的垃圾收集器的設計導向都不約而同地變為追求能夠應付應用的記憶體分配速率(Allocation Rate),而不追求一次把整個Java堆全部清理干凈,這樣,應用在分配,同時收集器在收集,只要收集的速度能跟得上物件分配的速度,那一切就能運作得很完美,這種新的收集器設計思路從工程實作上看是從G1開始興起的,所以說G1是收集器技術發展的一個里程碑,

相比CMS,G1的優點有很多,暫且不論可以指定最大停頓時間、分Region的記憶體布局、按收益動態確定回收集這些創新性設計帶來的紅利,單從最傳統的演算法理論上看,G1也更有發展潛力,與CMS的“標記-清除”演算法不同,G1從整體來看是基于“標記-整理”演算法實作的收集器,但從區域(兩個Region之間)上看又是基于“標記-復制”演算法實作,無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,垃圾收集完成之后能提供規整的可用記憶體,這種特性有利于程式長時間運行,在程式為大物件分配記憶體時不容易因無法找到連續記憶體空間而提前觸發下一次收集,

比起CMS,G1的弱項也可以列舉出不少,如在用戶程式運行程序中,G1無論是為了垃圾收集產生的記憶體占用(Footprint)還是程式運行時的額外執行負載(Overload)都要比CMS要高,

CMS和G1的對比

就記憶體占用來說,雖然G1和CMS都使用卡表來處理跨代指標,但G1的卡表實作更為復雜,而且堆中每個Region,無論扮演的是新生代還是老年代角色,都必須有一份卡表,這導致G1的記憶集(和其他記憶體消耗)可能會占整個堆容量的20%乃至更多的記憶體空間;相比起來CMS的卡表就相當簡單,只有唯一一份,而且只需要處理老年代到新生代的參考,反過來則不需要,由于新生代的物件具有朝生夕滅的不穩定性,參考變化頻繁,能省下這個區域的維護開銷是很劃算的,

在執行負載的角度上,同樣由于兩個收集器各自的細節實作特點導致了用戶程式運行時的負載會有不同,譬如它們都使用到寫屏障,CMS用寫后屏障來更新維護卡表;而G1除了使用寫后屏障來進行同樣的(由于G1的卡表結構復雜,其實是更煩瑣的)卡表維護操作外,為了實作原始快照搜索(SATB)演算法,還需要使用寫前屏障來跟蹤并發時的指標變化情況,相比起增量更新演算法,原始快照搜索能夠減少并發標記和重新標記階段的消耗,避免CMS那樣在最終標記階段停頓時間過長的缺點,但是在用戶程式運行程序中確實會產生由跟蹤參考變化帶來的額外負擔,由于G1對寫屏障的復雜操作要比CMS消耗更多的運算資源,所以CMS的寫屏障實作是直接的同步操作,而G1就不得不將其實作為類似于訊息佇列的結構,把寫前屏障和寫后屏障中要做的事情都放到佇列里,然后再異步處理,

淺色表示用戶執行緒掛起,深色為并發,

image-20211112152412720

經典垃圾收集器中還有ZGC,Shenandoah等,JDK11中默認使用的就是ZGC,

在本篇中,很多涉及虛擬機的具體引數并未提及,可參考其他博客,

記憶體分配與回收策略

Java技術體系的自動記憶體管理,最根本的目標是自動化地解決兩個問題:自動給物件分配記憶體以及自動回收分配給物件的記憶體,驗證的實際是使用Serial加Serial Old客戶端默認收集器組合下的記憶體分配和回收的策略,

物件優先在Eden分配

大多數情況下,物件在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC,HotSpot虛擬機提供了-XX:+PrintGCDetails這個收集器日志引數,告訴虛擬機在發生垃圾收集行為時列印記憶體回收日志,并且在行程退出的時候輸出當前的記憶體各區域分配情況,

private static final int _1MB = 1024 * 1024;
/**
* VM引數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
分配 20M 虛擬機運行記憶體,10M新生代,10M老年代,運行日志,Eden:Survivor=8:1
*/  
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; // 出現一次Minor GC,這時6MB的1,2,3轉移到老年代,4M的進入Eden
}
[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs][Times:user=0.00 sys=0.00....
Heap
	def new generation total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)
		eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000)
		from space 1024K, 14% used [0x032d0000, 0x032f5370, 0x033d0000)
		to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
	tenured generation total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)
		the space 10240K, 60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)
	compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
		the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

日志如上,

執行testAllocation()中分配allocation4物件的陳述句時會發生一次Minor GC,這次回收的結果是新生代6651KB變為148KB,而總記憶體占用量則幾乎沒有減少(因為allocation1、2、3三個物件都是存活的,虛擬機幾乎沒有找到可回收的物件),產生這次垃圾收集的原因是為allocation4分配記憶體時,發現Eden已經被占用了6MB,剩余空間已不足以分配allocation4所需的4MB記憶體,因此發生Minor GC,垃圾收集期間虛擬機又發現已有的三個2MB大小的物件全部無法放入Survivor空間(Survivor空間只有1MB大小),所以只好通過分配擔保機制提前轉移到老年代去,

大物件直接進入老年代

大物件就是指需要大量連續記憶體空間的Java物件,最典型的大物件便是那種很長的字串,或者元素數量很龐大的陣列,上面例子中的byte[]陣列就是典型的大物件

在Java虛擬機中要避免大物件的原因是,在分配空間時,它容易導致記憶體明明還有不少空間時就提前觸發垃圾收集,以獲取足夠的連續空間才能安置好它們,而當復制物件時,大物件就意味著高額的記憶體復制開銷,

HotSpot虛擬機提供了-XX:PretenureSizeThreshold引數,指定大于該設定值的物件直接在老年代分配,這樣做的目的就是避免在Eden區及兩個Survivor區之間來回復制,產生大量的記憶體復制操作,

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

HotSpot虛擬機中多數收集器都采用了分代收集來管理堆記憶體,那記憶體回收時就必須能決策哪些存活物件應當放在新生代,哪些存活物件放在老年代中,為做到這點,虛擬機給每個物件定義了一個物件年齡(Age)計數器,存盤在物件頭中(詳見物件的記憶體布局),物件通常在Eden區里誕生,如果經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,該物件會被移動到Survivor空間中,并且將其物件年齡設為1歲,物件在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15),就會被晉升到老年代中,物件晉升老年代的年齡閾值,可以通過引數-XX:MaxTenuringThreshold設定,

動態物件年齡判定

為了能更好地適應不同程式的記憶體狀況,HotSpot虛擬機并不是永遠要求物件的年齡必須達到-XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的物件就可以直接進入老年代,無須等到-XX:MaxTenuringThreshold中要求的年齡,

private static final int _1MB = 1024 * 1024;
/**
* VM引數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold2() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空間一半
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}

all1和all2加起來占用512KB,滿足同年齡物件打到Suvivor空間一半的規則,因此將兩個物件放入老年代,

空間分配擔保

在發生Minor GC之前,虛擬機必須先檢查老年代最大可用的連續空間是否大于新生代所有物件總空間,如果這個條件成立,那這一次Minor GC可以確保是安全的,如果不成立,則虛擬機會先查看-XX:HandlePromotionFailure引數的設定值是否允許擔保失敗(Handle Promotion Failure);如果允許,那會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代物件的平均大小,如果大于,將嘗試進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者-XX:HandlePromotionFailure設定不允許冒險,那這時就要改為進行一次Full GC,

解釋一下“冒險”是冒了什么風險:前面提到過,新生代使用復制收集演算法,但為了記憶體利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量物件在Minor GC后仍然存活的情況——最極端的情況就是記憶體回收后新生代中所有物件都存活,需要老年代進行分配擔保,把Survivor無法容納的物件直接送入老年代,這與生活中貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些物件的剩余空間,但一共有多少物件會在這次回收中活下來在實際完成記憶體回收之前是無法明確知道的,所以只能取之前每一次回收晉升到老年代物件容量的平均大小作為經驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間,

取歷史平均值來比較其實仍然是一種賭概率的解決辦法,也就是說假如某次Minor GC存活后的物件突增,遠遠高于歷史平均值的話,依然會導致擔保失敗,如果出現了擔保失敗,那就只好老老實實地重新發起一次Full GC,這樣停頓時間就很長了,雖然擔保失敗時繞的圈子是最大的,但通常情況下都還是會將-XX:HandlePromotionFailure開關打開,避免Full GC過于頻繁,

在JDK 6 Update 24之后,雖然原始碼中還定義了-XX:HandlePromotionFailure引數,但是在實際虛擬機中已經不會再使用它,JDK 6 Update 24之后的規則變為只要老年代的連續空間大于新生代物件總大小或者歷次晉升的平均大小,就會進行Minor GC,否則將進行Full GC,

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

標籤:Java

上一篇:動力節點SSM框架專案「虎牙個人博客」手把手帶你搭建個人博客

下一篇:45歲程式員,精通各種技術體系,卻連個面試機會都沒有…

標籤雲
其他(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