知识蒸馏 循环蒸馏_Java垃圾收集蒸馏
知識蒸餾 循環蒸餾
串行,并行,并發,CMS,G1,Young Gen,New Gen,Old Gen,Perm Gen,Eden,Tenured,Survivor Spaces,Safepoints和數百個JVM啟動標志。 在嘗試從Java應用程序獲取所需的吞吐量和延遲的同時調整垃圾收集器時,這是否使您感到困惑? 如果確實如此,那就不用擔心,您并不孤單。 描述垃圾收集的文檔就像飛機的手冊頁。 每個旋鈕和轉盤都有詳細的說明,但找不到任何有關如何飛行的指南。 本文將嘗試解釋為特定工作負載選擇和調整垃圾收集算法時的權衡取舍。
重點將放在Oracle Hotspot JVM和OpenJDK收集器上,因為它們是最常用的收集器。 最后,將討論其他商用JVM以說明替代方案。
權衡
明智的人們不斷告訴我們: “您一無所獲” 。 當我們得到一些東西時,我們通常不得不放棄一些回報。 當涉及到垃圾收集時,我們使用3個主要變量來為收集器設置目標:
注意: Hotspot通常無法實現這些目標,并且會在沒有任何警告的情況下默默地繼續運行,因為它已經大大偏離了目標。
延遲是事件之間的分布。 可以增加平均等待時間以減少最壞情況的等待時間,或者降低等待時間,這是可以接受的。 我們不應將“實時”一詞解釋為意味著最低的延遲。 實時是指無論吞吐量如何都具有確定性的延遲。
對于某些應用程序工作負載,吞吐量是最重要的目標。 一個例子是長期運行的批處理作業。 只要可以更快地完成整個作業,那么在進行垃圾收集時是否偶爾將批處理作業暫停幾秒鐘并不重要。
對于幾乎所有其他工作負載,從面向人類的交互式應用程序到金融交易系統,如果系統在某些情況下無法響應的時間超過幾秒鐘甚至幾毫秒,則可能會帶來災難。 在金融交易中,通常值得犧牲一些吞吐量以換取一致的延遲。 我們可能還會有一些應用程序,這些應用程序受到可用物理內存量的限制,并且必須保持占用空間,在這種情況下,我們必須放棄延遲和吞吐量方面的性能。
權衡通常表現如下:
- 通過為垃圾回收算法提供更多的內存,可以在很大程度上減少作為攤銷成本的垃圾回收成本。
- 通過包含活動集并保持堆大小較小,可以減少由于垃圾收集而導致的觀察到的最壞情況的延遲引發的暫停。
- 通過管理堆和生成大小以及控制應用程序的對象分配速率,可以減少出現暫停的頻率。
- 通過與應用程序同時運行GC,可以減少較大的暫停頻率,有時會犧牲吞吐量。
對象壽命
垃圾收集算法通常經過優化,以期大多數對象的生存期很短,而很少有對象生存期很長。 在大多數應用程序中,生存期很長的對象往往構成隨時間分配的對象的很小一部分。 在垃圾收集理論中,這種觀察到的行為通常被稱為“ 嬰兒死亡率 ”或“ 弱代假設 ”。 例如,循環迭代器通常壽命很短,而靜態字符串實際上是永生的。
實驗表明,世代垃圾收集器通常可以比非世代收集器支持更大數量級的吞吐量,因此幾乎在服務器JVM中廣泛使用。 通過分離對象的世代,我們知道新分配對象的區域對于活動對象可能非常稀疏。 因此,收集器在此新區域中清除少量活動對象并將其復制到較舊對象的另一個區域中可以非常有效。 熱點垃圾收集器根據生存的GC周期數記錄對象的壽命。
注意:如果您的應用程序持續生成許多可以生存很長時間的對象,則可以預期您的應用程序將花費大量時間進行垃圾回收,并希望花費大量時間來調整Hotspot垃圾收集器。 這是由于世代“過濾器”效率降低時發生的GC效率降低,以及導致更頻繁地收集更長壽命的世代的成本。 老一輩人稀疏,因此老一輩人收集算法的效率往往要低得多。 分代垃圾收集器通常以兩個不同的收集周期運行:收集短期對象的次要垃圾收集,以及收集較舊區域的次要垃圾收集。
世界停止事件
在垃圾回收期間,應用程序遭受的暫停是由于所謂的世界停止事件造成的。 為了使垃圾收集器運行,出于實際工程上的原因,有必要定期停止正在運行的應用程序,以便可以管理內存。 根據算法的不同,不同的收集器將在特定的執行點停下世界,并持續不同的時間。 要使應用程序完全停止,必須暫停所有正在運行的線程。 垃圾收集器通過發信號通知線程在到達“ 安全點 ”時停止運行來做到這一點,這是程序執行期間所有GC根已知且所有堆對象內容一致的點。 根據線程在做什么,可能需要一些時間才能達到安全點。 安全點檢查通常在方法返回和回送邊沿上執行,但可以在某些地方進行優化,從而使其在動態上更加罕見。 例如,如果線程正在復制大型數組,克隆大型對象或執行具有有限界限的單調計數循環,則到達安全點可能要花費幾毫秒的時間。 安全時間(TTS)是低延遲應用程序中的重要考慮因素。 通過啟用?XX:+ PrintGCApplicationStoppedTime標志以及其他GC標志,可以浮出水面。
注意:對于具有大量正在運行的線程的應用程序,當世界停止事件發生時,隨著線程在釋放后恢復,系統將承受重大的調度壓力。 因此,較少依賴于世界停止事件的算法可能會更有效。
熱點堆組織
要了解不同收集器的工作方式,最好探索如何組織Java堆以支持分代收集器。
伊甸園是最初分配大多數對象的區域。 幸存者空間是一個臨時存儲區,用于存儲在伊甸園空間中幸存的對象。 討論次要收藏時,將描述幸存者空間的使用情況。 伊甸園和幸存者空間統稱為“年輕”或“新生代”。
壽命足夠長的對象最終將提升為使用期限 。
燙發生成是運行時將其“知道”為有效的對象(例如類和靜態字符串)存儲的地方。 不幸的是,在許多應用程序中持續使用類加載的常見用法使燙發生成背后的動機假設錯誤,即類是不朽的。 在Java 7中,已將字符串從permgen轉移到Tenured ,而從Java 8中不再存在perm的生成,因此本文將不進行討論。 大多數其他商業收藏家并不使用單獨的燙發空間,而是傾向于將所有長期存在的物品視為永久使用。
注意:虛擬空間允許收集器調整區域的大小,以滿足吞吐量和延遲目標。 收集器會保留每個收集階段的統計信息,并相應地調整區域大小,以達到目標。
對象分配
為了避免爭用,每個線程都分配有一個線程本地分配緩沖區(TLAB),從該線程中分配對象。 使用TLAB可以避免對象在單個內存資源上的爭用,從而使對象分配隨線程數擴展。 通過TLAB分配對象是非常便宜的操作; 它只是碰觸對象大小的指針,在大多數平臺上大約需要10條指令。 Java的堆內存分配比從C運行時使用malloc還要便宜。
注意:盡管單個對象分配非常便宜,但必須進行次要收集的速率與對象分配的速率成正比。
當TLAB耗盡時,一個線程只需向Eden空間請求一個新線程。 當伊甸園裝滿后,便開始小規模收集。
大對象(-XX:PretenureSizeThreshold = <n>)可能無法容納在年輕的一代中,因此必須在舊的一代中進行分配,例如大型數組。 如果將閾值設置為低于TLAB大小,則不會在舊版本中創建適合TLAB的對象。 新的G1收集器以不同的方式處理大型物體,稍后將在其單獨的部分中進行討論。
小型收藏
當伊甸園變滿時,將觸發次要回收。 這是通過將新一代的所有活動對象適當地復制到幸存者空間或保有權空間來完成的。 復制到使用權空間稱為升級或使用權。 對于足夠舊的對象(– XX:MaxTenuringThreshold = <n>),或幸存者空間溢出時,將進行升級。
活動對象是應用程序可訪問的對象。 任何其他物體均無法到達,因此可以視為已死亡。 在次要集合中,首先通過遵循所謂的GC根目錄執行活動對象的復制,然后反復復制可到達生存空間的任何對象。 GC根通常包括來自應用程序和JVM內部靜態字段以及線程堆棧框架的引用,所有這些引用均有效指向應用程序的可訪問對象圖。
在世代集合中,新一代可訪問對象圖的GC根目錄還包括從舊一代到新一代的所有引用。 還必須對這些引用進行處理,以確保新一代中的所有可訪問對象在次要集合中都不會丟失。 通過使用“ 卡片表 ”來識別這些跨代參考。 熱點卡表是一個字節數組,其中每個字節用于跟蹤舊一代的相應512字節區域中跨代引用的潛在存在。 在將引用存儲到堆時,“存儲屏障”代碼將標記卡,以指示從舊一代到新一代的潛在引用可能存在于關聯的512字節堆區域中。 在收集時,卡片表用于掃描此類跨代引用,這些引用有效地代表了新一代的其他GC根。 因此,次要藏品的重大固定成本與上一代的大小成正比。
新一代Hotspot中有兩個幸存者空間,它們的“ 到太空 ”和“ 從太空 ”角色交替出現。 在次要收集開始時,到太空幸存者空間始終為空,并充當次要收集的目標副本區域。 先前的次要收藏的目標幸存者空間是起始空間的一部分,起始空間還包括伊甸園,在伊甸園中可以找到需要復制的活動對象。
少量GC收集的成本通常由將對象復制到幸存者和保有權空間的成本決定。 不能幸免的對象可以有效地自由處理。 在次要收藏期間完成的工作與發現的活動對象的數量成正比,而不與新一代的大小成正比。 每次將伊甸園面積擴大一倍時,花在次要收藏上的總時間幾乎可以減少一半。 因此可以將內存用于吞吐量。 將Eden大小增加一倍會導致每個收集周期的收集時間增加,但是如果要提升的對象數和舊一代的大小都恒定,則這相對較小。
注意:在熱點中,次要收藏是世界停止事件。 隨著越來越多的活動對象堆越來越大,這正Swift成為一個主要問題。 我們已經開始看到需要同時收集年輕一代以達到暫停時間目標的需求。
主要收藏
主要藏品收集了老一代,以便可以從年輕一代中推廣物品。 在大多數應用程序中,絕大多數程序狀態最終出現在老一代。 對于前代來說,存在種類最多的GC算法。 有些會在填滿時壓縮整個空間,而另一些會與應用程序同時收集以防止填滿。
老一代的收藏家將嘗試預測何時需要收藏,以避免年輕一代的晉升失敗。 收集器跟蹤舊一代的填充閾值,并在超過該閾值時開始收集。 如果該閾值不足以滿足促銷要求,那么將觸發“ FullGC ”。 FullGC涉及推廣年輕一代的所有活動對象,然后收集和壓縮舊一代。 升級失敗是一項非常昂貴的操作,因為必須解開此循環中的狀態和升級對象,以便發生FullGC事件。
注意:為避免升級失敗,您將需要調整舊版本允許容納升級的填充(?XX:PromotedPadding = <n>)。
注意:當堆需要增長時,會觸發FullGC。 通過將–Xms和–Xmx設置為相同的值,可以避免這些調整堆大小的FullGC。
除了FullGC,舊版本的壓縮很可能是應用程序將遇到的最大的停頓停頓狀態。 壓縮的時間往往會隨著使用權空間中活動對象的數量線性增長。
有時可以通過增加幸存者空間的大小和對象的年齡來降低占位空間的填充率,然后再提升其為占位空間。 但是,在促銷之前增加次要收藏中幸存者空間的大小和對象年齡(–XX:MaxTenuringThreshold = <n>)也會增加次要收藏物中的成本和暫停時間,這是由于未成年收藏者之間的生存空間之間的復制成本增加了集合。
串行收集器
串行收集器(-XX:+ UseSerialGC)是最簡單的收集器,是單處理器系統的不錯選擇。 它還具有所有收集器中最小的占地面積。 它對次要和主要集合都使用一個線程。 使用簡單的凹凸指針算法在持久空間中分配對象。 當使用權空間已滿時,將觸發主要集合。
并聯收集器
并行收集器有兩種形式。 并行收集器 (?XX:+ UseParallelGC),它使用多個線程來執行年輕代的次要收集,并使用單個線程來執行舊代的主要收集。 自Java 7u4起默認的Parallel Old收集器 (?XX:+ UseParallelOldGC)使用多個線程進行次要收集,并使用多個線程進行主要收集。 使用簡單的凹凸指針算法在持久空間中分配對象。 當使用權空間已滿時,將觸發主要集合。
在多處理器系統上,并行舊收集器將提供所有收集器中最大的吞吐量。 直到發生收集為止,它對正在運行的應用程序沒有影響,然后將使用最有效的算法使用多個線程并行收集。 這使得Parallel Old Collector非常適合批處理應用。
收集舊版本的成本受要保留的對象數量的影響要比與堆大小的影響更大。 因此,可以通過提供更多內存并接受較大但較少的收集暫停來提高Parallel Old收集器的效率,以實現更大的吞吐量。
期望使用該收集器獲得最快的次要收集,因為升級到保有空間僅是指針和復制操作的簡單顛簸。
對于服務器應用程序,Parallel Old收集器應該是第一個調用端口。 但是,如果主要的收集暫停時間超出了您的應用程序所能承受的范圍,則您需要考慮使用并發收集器,該并發收集器在應用程序運行時同時收集歷時對象。
注意:在壓縮舊版本的同時,現代硬件上每GB實時數據的暫停時間大約為1到5秒。
注意:通過為多插槽CPU服務器應用程序的-XX:+ UseNUMA分配并行的收集器,有時可以為CPU套接字本地的線程分配Eden內存,從而獲得性能優勢。 遺憾的是,該功能對其他收集器不可用。
并發標記掃描(CMS)收集器
CMS(-XX:+ UseConcMarkSweepGC)收集器在舊版本中運行,以收集在大型收集期間不再可訪問的終身對象。 它與應用程序同時運行,目的是在老一代中保留足夠的可用空間,從而不會發生年輕一代的升級失敗。
升級失敗將觸發FullGC。 CMS遵循多個步驟:
當租用對象變得不可訪問時,CMS將回收該空間并將其放入空閑列表。 進行促銷時,必須在自由列表中搜索要促銷的對象的合適大小的Kong。 與Parallel Collector相比,這增加了推廣成本,從而增加了Minor收藏的成本。
注意 :CMS不是壓縮收集器,隨著時間的推移,它可能導致舊的碎片化。 對象升級可能會失敗,因為大型對象可能不適合舊版本中的可用Kong。 發生這種情況時,將記錄“ 升級失敗 ”消息,并觸發FullGC壓縮活動的使用權對象。 對于此類壓縮驅動的FullGC,由于CMS僅使用單個線程進行壓縮,因此期望的延遲比使用Parallel Old收集器的主要收集更糟糕。
CMS通常與應用程序并發,這具有許多含義。 首先,CPU時間由收集器占用,從而減少了可用于應用程序的CPU。 CMS所需的時間與將對象提升到保有空間的數量成線性增長。 其次,對于并發GC周期的某些階段,必須將所有應用程序線程帶入一個安全點,以標記GC根并執行并行重新標記以檢查變異。
注意 :如果應用程序發現使用權對象發生了重大變化,則重新標記階段可能很重要,在極端情況下,重新標記階段可能比使用Parallel Old Collector進行完全壓縮要花費更長的時間。
CMS使FullGC成為不太頻繁的事件,但代價是吞吐量降低,更昂貴的次要收集和更大的占用空間。 與并行收集器相比,吞吐量的降低幅度可能在10%-40%之間,具體取決于提升率。 CMS還需要占用20%的空間,以容納其他數據結構和“浮動垃圾”,這些并發標記在傳遞到下一個周期的并發標記期間可能會丟失。
有時可以通過增加年輕一代空間和老一代空間的大小來降低高晉升率和由此造成的分裂。
注意 :如果CMS收集速度不足以跟上升級的速度,則CMS可能會遇到“ 并發模式故障 ”,這可以在日志中看到。 當收集開始太晚時可能會導致這種情況,有時可以通過調整來解決。 但是,當收集率無法跟上某些應用程序的高推廣率或高對象突變率時,也會發生這種情況。 如果應用程序的提升率或變異率太高,則您的應用程序可能需要進行一些更改以減輕提升壓力。 向這樣的系統添加更多的內存有時會使情況變得更糟,因為CMS將需要更多的內存來進行掃描。
垃圾優先(G1)收集器
G1(-XX:+ UseG1GC)是Java 6中引入的新收集器,現已從Java 7u4開始正式支持。 這是一種部分并發的收集算法,該算法還嘗試在較小的增量“停止世界”停頓中壓縮占位空間,以盡量減少由于碎片而困擾CMS的FullGC事件。 G1是一個世代收集器,通過將其劃分為大量(?2000個)可變大小的固定大小區域(而不是出于相同目的的連續區域)來與其他收集器進行不同的組織。
G1采用同時標記區域的方法來跟蹤區域之間的引用,并將收集集中在具有最大可用空間的區域上。 然后,通過將活動對象疏散到一個空的區域,以停下來的暫停增量收集這些區域,從而在此過程中進行壓縮。 一個循環中要收集的區域稱為收集集 。
大于某個區域50%的對象被分配在多個區域中的大型區域中。 在G1下,大型對象的分配和收集可能會非常昂貴,并且迄今為止幾乎沒有或沒有進行任何優化工作。
任何壓縮收集器所面臨的挑戰不是對象的移動,而是對這些對象的引用的更新。 如果從許多區域引用了一個對象,則更新這些引用所花費的時間可能比移動該對象要長得多。 G1通過“ 記住的集合 ”跟蹤區域中的哪些對象具有其他區域的引用。 記住集是標記為突變的牌的集合。 如果“記住的集合”變大,則G1會顯著降低速度。 當將對象從一個區域撤離到另一個區域時,相關的世界停止事件的時間長度往往與需要掃描并可能需要打補丁的參考區域的數量成正比。
維護“已記住的集合”會增加次要集合的成本,從而導致停頓的時間要長于Parallel Old或CMS的次要集合。
G1是目標驅動程序,其時延為–XX:MaxGCPauseMillis = <n>,默認值= 200ms。 該目標將盡力而為地影響每個周期的工作量。 在幾十毫秒內設置目標通常是徒勞的,而在撰寫本文時,針對數十毫秒的目標還不是G1的重點。
對于較大的堆,G1是一個很好的通用收集器,當應用程序可以容忍0.5-1.0秒范圍內的增量壓縮暫停時,G1往往會變得碎片化。 G1傾向于減少CMS看到的最壞情況的停頓的頻率,這是因為碎片化的代價是擴展了次要收集的范圍和老一代的增量壓縮。 大多數暫停最終都局限于區域壓縮,而不是全部堆壓縮。
像CMS一樣,G1也可能無法跟上晉升率,并且會退回到世界末日的FullGC。 就像CMS具有“ 并發模式故障 ”一樣,G1可能會發生疏散故障,在日志中被視為“ 空間溢出 ”。 當沒有空閑區域可將對象撤離時,就會發生這種情況,這類似于升級失敗。 如果發生這種情況,請嘗試使用更大的堆和更多的標記線程,但是在某些情況下,可能需要更改應用程序以降低分配率。
對于G1來說,一個具有挑戰性的問題是處理受歡迎的物體和區域。 當區域中的活動對象沒有從其他區域大量引用時,增量停止世界壓縮將非常有效。 如果某個對象或區域很受歡迎,則“記住的集合”將很大,G1將嘗試避免收集這些對象。 最終,它別無選擇,這會導致堆壓縮時非常頻繁的中長度暫停。
替代并行收集器
CMS和G1通常被稱為并發收集器。 當您查看所執行的全部工作時,很顯然,年輕一代,晉升甚至許多老一代工作根本不是同時發生的。 CMS在大多數情況下是并發的。 G1更像是一個停滯不前的增量收集器。 CMS和G1都有重大且定期發生的世界停止事件,以及最壞的情況,通常使它們不適用于嚴格的低延遲應用程序,例如金融交易或React性用戶界面。
可以使用其他收集器,例如Oracle JRockit Real Time,IBM Websphere Real Time和Azul Zing。 JRockit和Websphere收集器在大多數情況下都比CMS和G1具有延遲優勢,但是經常遇到吞吐量限制,并且仍然遭受重大的世界停止事件。 Zing是該作者所知的唯一Java收集器,它可以真正地并發進行收集和壓縮,同時保持所有代的高吞吐率。 Zing確實有一些毫秒級的世界停止事件,但這些事件是與收集周期中的相移有關的,這些相移與活動對象集的大小無關。
對于在包含的堆大小下的高分配率而言,JRockit RT可以實現數十毫秒的典型暫停時間,但有時還必須恢復到完全壓縮暫停。 Websphere RT可以通過受限制的分配速率和活動集大小來實現單位毫秒的暫停時間。 通過在所有階段(包括次要收集期間)并發執行,Zing可以以高分配率實現亞毫秒級的暫停。 無論堆大小如何,Zing都可以保持這種一致的行為,從而使用戶可以根據需要應用大堆大小,以適應應用程序吞吐量或對象模型狀態需求,而不必擔心增加暫停時間。
對于所有針對延遲的并發收集器,您必須放棄一些吞吐量并增加占用空間。 根據并發收集器的效率,您可能會放棄一點吞吐量,但是始終會增加大量占用空間。 如果是真正的并發,幾乎沒有停滯事件,則需要更多的CPU內核來啟用并發操作并保持吞吐量。
注意:分配足夠的空間后,所有并發收集器往往會更有效地發揮作用。 作為經驗法則,您應該將堆的預算至少為活動集大小的2到3倍,以實現高效操作。 但是,用于維持并發操作的空間需求隨應用程序吞吐量以及相關的分配和提升率而增長。 因此,對于更高吞吐量的應用程序,可以保證更高的堆大小與活動集比率。 鑒于當今系統可用的巨大內存空間,在服務器端很少出現問題。
垃圾收集監控和調整
要了解您的應用程序和垃圾收集器的行為方式,請至少使用以下設置啟動JVM:
-verbose:gc -Xloggc: -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime然后將日志加載到Chewiebug之類的工具中進行分析。
要查看GC的動態性質,請啟動JVisualVM并安裝Visual GC插件。 這將使您能夠如下所示查看適用于您的應用程序的GC。
為了了解您的應用程序的GC需求,您需要可以重復執行的代表性負載測試。 當您掌握每個收集器的工作方式時,然后以不同的配置運行負載測試作為實驗,直到達到吞吐量和延遲目標。 從最終用戶的角度衡量延遲很重要。 這可以通過在直方圖中捕獲每個測試請求的響應時間來實現,您可以在此處了解更多信息。 如果您的延遲峰值超出可接受范圍,請嘗試將其與GC日志關聯,以確定是否是GC問題。 其他問題可能會導致延遲峰值。 另一個值得考慮的有用工具是jHiccup ,它可用于跟蹤JVM中以及整個系統中的暫停。 用jHiccup測量您的空閑系統幾個小時,您通常會感到非常驚訝。
如果延遲高峰是由于GC引起的,則投資調整CMS或G1以查看您的延遲目標是否可以實現。 有時,這可能是由于高分配率和提升率以及低延遲要求而無法實現的。 GC調整可以成為一項高技能的練習,通常需要更改應用程序以減少對象分配率或對象壽命。 如果是這種情況,則可能需要在時間和花費在GC調整和應用程序更改上的資源之間進行商業平衡,例如,可能需要購買商業并發壓縮JVM中的一種,例如JRockit Real Time或Azul Zing。
翻譯自: https://www.javacodegeeks.com/2013/07/java-garbage-collection-distilled.html
知識蒸餾 循環蒸餾
總結
以上是生活随笔為你收集整理的知识蒸馏 循环蒸馏_Java垃圾收集蒸馏的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 问界新M7单日大定突破2000台 这波华
- 下一篇: Javascript中的AES加密和Ja