JVM性能優化, Part 3 垃圾回收

ImportNew注:本文是JVM性能優化 系列-第3篇-《JVM性能優化, Part 3 —— 垃圾回收

第一篇 《JVM性能優化, Part 1 ―― JVM簡介 》

第二篇《JVM性能優化, Part 2 ―― 編譯器

Java平臺的垃圾回收機制大大提高的開發人員的生產力,但實現糟糕的垃圾回收器卻會大大消耗應用程序的資源。本文作爲JVM性能優化系列的第3篇,Eva Andeasson將爲Java初學者介紹Java平臺的內存模型和GC機制。她將解釋爲什麼碎片化(不是GC)是Java應用程序出現性能問題的主要原因,以及爲什麼當前主要通過分代垃圾回收和壓縮,而不是其他最具創意的方法,來解決Java應用程序中碎片化的問題。

垃圾回收(GC)是旨在釋放不可達Java對象所佔用的內存的過程,是Java virtual machine(JVM)中動態內存管理系統的核心組成部分。在一個典型的垃圾回收週期中,所有仍被引用的對象,即可達對象,會被保留。沒有被引用的Java對象所佔用的內存會被釋放並回收,以便分配給新創建的對象。

爲了更好的理解垃圾回收與各種不同的GC算法,你首先需要了解一些關於Java平臺內存模型的內容。

垃圾回收與Java平臺內存模型

當你在啓動Java應用程序時指定了啓動參數_-Xmx_(例如,java -Xmx2g MyApp),則相應大小的內存會被分配給Java進程。這塊內存即所謂的*Java堆*(或簡稱爲*堆*)。這塊專用的內存地址空間用於存儲Java應用程序(有時是JVM)所創建的對象。隨着Java應用程序的運行,會不斷的創建新對象併爲之分配內存,Java堆(即地址空間)會逐漸被填滿。

最後,Java堆會被填滿,這就是說想要申請內存的線程無法獲得一塊足夠大的連續空閒空間來存放新創建的對象。此時,JVM判斷需要啓動垃圾回收器來回收內存了。當Java程序調用System.gc()方法時,也有可能會觸發垃圾回收器以執行垃圾回收的工作。使用System.gc()方法並不能保證垃圾回收工作肯定會被執行。在執行垃圾回收前,垃圾回收機制首先會檢查當前是否是一個“恰當的時機”,而“恰當的時機”指所有的應用程序活動線程都處於安全點(safe point),以便啓動垃圾回收。簡單舉例,爲對象分配內存時,或正在優化CPU指令(參見本系列的前一篇文章)時,就不是“恰當的時機”,因爲你可能會丟失上下文信息,從而得到混亂的結果。

垃圾回收不應該回收當前有活動引用指向的對象所佔用的內存;因爲這樣做將違反JVM規範。在JVM規範中,並沒有強制要求垃圾回收器立即回收已死對象(dead object)。已死對象最終會在後續的垃圾回收週期中被釋放掉。目前,已經有多種垃圾回收的實現,它們都包含兩個溝通的假設。對垃圾回收來說,真正的挑戰在於標識出所有活動對象(即仍有引用指向的對象),回收所有不可達對象所佔用的內存,並儘可能不對正在運行的應用程序產生影響。因此,垃圾回收器運行的兩個目標:

  1. 快速釋放不可達對象所佔用的內存,防止應用程序出現OOM錯誤。
  2. 回收內存時,對應用程序的性能(指延遲和吞吐量)的影響要緊性能小。

兩類垃圾回收

在本系列的第一篇文章中,我提到了2種主要的垃圾回收方式,引用計數(reference counting)和引用追蹤(tracing collector。譯者注,在第一篇中,給出的名字是“reference tracing”,這裏仍沿用之前的名字)。這裏,我將深入這兩種垃圾回收方式,並介紹用於生產環境的實現了引用追蹤的垃圾回收方式的相關算法。

     相關閱讀:JVM性能優化系列

  •            JVM性能優化,第一部分: 概述
  •            JVM性能優化,第二部分: 編譯器

引用計數垃圾回收器

引用計數垃圾回收器會對指向每個Java對象的引用數進行跟蹤。一旦發現指向某個對象的引用數爲0,則立即回收該對象所佔用的內存。引用計數垃圾回收的主要優點就在於可以立即訪問被回收的內存。垃圾回收器維護未被引用的內存並不需要消耗很大的資源,但是保持並不斷更新引用計數卻代價不菲。

使用引用計數方式執行垃圾回收的主要困難在於保持引用計數的準確性,而另一個衆所周知的問題在於解決循環引用結構所帶來的麻煩。如果兩個對象互相引用,並且沒有其他存活東西引用它們,那麼這兩個對象所佔用的內存將永遠不會被釋放,兩個對象都會因引用計數不爲0而永遠存活下去。要解決循環引用帶來的問題需要,而這會使算法複雜度增加,從而影響應用程序的運行性能。

引用跟蹤垃圾回收

引用跟蹤垃圾回收器基於這樣一種假設,所有存活對象都可以通過迭代地跟蹤從已知存活對象集中對象發出的引用及引用的引用來找到。可以通過對寄存器、全局域、以及觸發垃圾回收時棧幀的分析來確定初始存活對象的集合(稱爲“根對象”,或簡稱爲“根”)。在確定了初始存活對象集後,引用跟蹤垃圾回收器會跟蹤從這些對象中發出的引用,並將找到的對象標記爲“活的(live)”。標記所有找到的對象意味着已知存活對象的集合會隨時間而增長。這個過程會一直持續到所有被引用的對象(因此是“存活的”對象)都被標記。當引用跟蹤垃圾回收器找到所有存活的對象後,就會開始回收未被標記的對象。

不同於引用計數垃圾回收器,引用跟蹤垃圾回收器可以解決循環引用的問題。由於標記階段的存在,大多數引用跟蹤垃圾回收器無法立即釋放“已死”對象所佔用的內存。

引用跟蹤垃圾回收器廣泛用於動態語言的內存管理;到目前爲止,在Java編程語言的視線中也是應用最廣的,並且在多年的商業生產環境中,已經證明其實用性。在本文餘下的內容中,我將從一些相關的實現算法開始,介紹引用跟蹤垃圾回收器,

引用跟蹤垃圾回收器算法

拷貝和*標記-清理*垃圾回收算法並非新近發明,但仍然是當今實現引用跟蹤垃圾回收器最常用的兩種算法。

拷貝垃圾回收器

傳統的拷貝垃圾回收器會使用一個“from”區和一個“to”區,它們是堆中兩個不同的地址空間。在執行垃圾回收時,from區中存活對象會被拷貝到to區。當from區中所有的存活對象都被拷貝到to後,垃圾回收器會回收整個from區。當再次分配內存時,會首先從to區中的空閒地址開始分配。

在該算法的早期實現中,from區和to區會在垃圾回收週期後進行交換,即當to區被填滿後,將再次啓動垃圾回收,這是to區會“變成”from區。如圖Figure 1所示。

Figure 1. A traditional copying garbage collection sequence

在該算法的近期實現中,可以將堆中任意地址空間指定爲from區和to區,這樣就不再需要交換from區和to區,堆中任意地址空間都可以成爲from區或to區。

拷貝垃圾回收器的一個優點是存活對象的位置會被to區中重新分配,緊湊存放,可以完全消除碎片化。碎片化是其他垃圾回收算法所要面臨的一大問題,這點會在後續討論。

拷貝垃圾回收的缺陷

通常來說,拷貝垃圾回收器是“stop-the-world”式的,即在垃圾回收週期內,應用程序是被掛起的,無法工作。在“stop-the-world”式的實現中,所需要拷貝的區域越大,對應用程序的性能所造成的影響也越大。對於那些非常注重響應時間的應用程序來說,這是難以接受的。使用拷貝垃圾回收時,你還需要考慮一下最壞情況,即當from區中所有的對象都是存活對象的時候。因此,你不得不給存活對象預留出足夠的空間,也就是說to區必須足夠大,大到可以將from區中所有的對象都放進去。正是由於這個缺陷,拷貝垃圾回收算法在內存使用效率上略有不足。

標記-清理垃圾回收器

大多數部署在企業生產環境的商業JVM都使用了標記-清理(或標記)垃圾回收器,這種垃圾回收器並不會想拷貝垃圾回收器那樣對應用程序的性能有那麼大的影響。其中最著名的幾款是CMS、G1、GenPar和DeterministicGC(參見相關資源)。

標記-清理垃圾回收器會跟蹤引用,並使用標記位將每個找到的對象標記位“live”。通常來說,每個標記位都關聯着一個地址或堆上的一個地址集合。例如,標記位可能是對象頭(object header)中一位,一個位向量,或是一個位圖。

當所有的存活對象都被標記位“live”後,將會開始*清理*階段。一般來說,垃圾回收器的清理階段包含了通過再次遍歷堆(不僅僅是標記位live的對象集合,而是整個堆)來定位內存地址空間中未被標記的區域,並將其回收。然後,垃圾回收器會將這些被回收的區域保存到空閒列表(free list)中。在垃圾回收器中可以同時存在多個空閒列表——通常會按照保存的內存塊的大小進行劃分。某些JVM(例如JRockit實時系統, JRockit Real Time System)在實現垃圾回收器時會給予應用程序分析數據和對象大小統計數據來動態調整空閒列表所保存的區域塊的大小範圍。

當清理階段結束後,應用程序就可以再次啓動了。給新創建的對象分配內存時會從空閒列表中查找,而空閒列表中內存塊的大小需要匹配於新創建的對象大小、某個線程中平均對象大小,或應用程序所設置的TLAB的大小。從空閒列表中爲新創建的對象找到大小合適的內存區域塊有助於優化內存的使用,減少內存中的碎片。

關於TLAB
        更多關於TLAB和TLA(Thread Local Allocation Buffer和Thread Local Area)的內容,請參見ImportNew翻譯整理的第一篇《JVM性能優化, Part 1 ―― JVM簡介》。

標記-清理垃圾回收器的缺陷

標記階段的時長取決於堆中存活對象的總量,而清理階段的時長則依賴於堆的大小。由於在*標記*階段和*清理*階段完成前,你無事可做,因此對於那些具有較大的堆和較多存活對象的應用程序來說,使用此算法需要想辦法解決暫停時間(pause-time)較長這個問題。

對於那些內存消耗較大的應用程序來說,你可以使用一些GC調優選項來滿足其在某些場景下的特殊需求。很多時候,調優至少可以將標記-清理階段給應用程序或性能要求(SLA,SLA指定了應用程序需要達到的響應時間的要求,即延遲)所帶來的風險推後。當負載和應用程序發生改變後,需要重新調優,因爲某次調優只對特定的工作負載和內存分配速率有效。

標記-清理算法的實現

目前,標記-清理垃圾回收算法至少已有2種商業實現,並且都已在生產環境中被證明有效。其一是並行垃圾回收,另一個是併發(或多數時間併發)垃圾回收。

並行垃圾回收器

並行垃圾回收指的是垃圾回收是多線程並行完成的。大多數商業實現的並行垃圾回收器都是stop-the-world式的垃圾回收器,即在整個垃圾回收週期結束前,所有應用程序線程都會被掛起。掛起所有應用程序線程使垃圾回收器可以以並行的方式,更有效的完成標記和清理工作。並行使得效率大大提高,通常可以在像SPECjbb這樣的吞吐量基準測試中跑出高分。如果你的應用程序好似有限考慮吞吐量的,那麼並行垃圾回收是你最好的選擇。

對於大多數並行垃圾回收器來說,尤其是考慮到應用於生產環境中,最大的問題是,像拷貝垃圾回收算法一樣,在垃圾回收週期內應用程序無法工作。使用stop-the-world式的並行垃圾回收會對優先考慮響應時間的應用程序產生較大影響,尤其是當你有大量的引用需要跟蹤,而此時恰好又有大量的、具有複雜結構的對象存活於堆中的時候,情況將更加糟糕。(記住,標記-清理垃圾回收器回收內存的時間取決於跟蹤存活對象中所有引用的時間與遍歷整個堆的時間之和。)以並行方式執行垃圾回收所導致的應用程序暫停會一直持續到整個垃圾回收週期結束。

併發垃圾回收器

併發垃圾回收器更適用於那些對響應時間比較敏感的應用程序。併發指的是一些(或大多數)垃圾回收工作可以與應用程序線程同時運行。由於並非所有的資源都由垃圾回收器使用,因此這裏所面臨的問題如何決定何時開始執行垃圾回收,可以保證垃圾回收順利完成。這裏需要足夠的時間來跟蹤存活對象即的引用,並在應用程序出現OOM錯誤前回收內存。如果垃圾回收器無法及時完成,則應用程序就會拋出OOM錯誤。此外,一直做垃圾回收也不好,會不必要的消耗應用程序資源,從而影響應用程序吞吐量。要想在動態環境中保持這種平衡就需要一些技巧,因此設計了啓發式方法來決定何時開始垃圾回收,何時執行不同的垃圾回收優化任務,以及一次執行多少垃圾回收優化任務等。

併發垃圾回收器所面臨的另一個挑戰是如何決定何時執行一個需要完整堆快照的操作時安全的,例如,你需要知道是何時標記所有存活對象的,這樣才能轉而進入清理階段。在大多數並行垃圾回收器採用的stop-the-world方式中,*階段轉換(phase-switching)*並不需要什麼技巧,因爲世界已靜止(堆上對象暫時不會發生變化)。但是,在併發垃圾回收中,轉換階段時可能並不是安全的。例如,如果應用程序修改了一塊垃圾回收器已經標記過的區域,可能會涉及到一些新的或未被標記的引用,而這些引用使其指向的對象成爲存活狀態。在某些併發垃圾回收的實現中,這種情況有可能會使應用程序陷入長時間運行重標記(re-mark)的循環,因此當應用程序需要分配內存時無法得到足夠做的空閒內存。

到目前爲止的討論中,已經介紹了各種垃圾回收器和垃圾回收算法,他們各自適用於不同的場景,滿足不同應用程序的需求。各種垃圾回收方式不僅在算法上有所區別,在具體實現上也不盡相同。所以,在命令行中指定垃圾回收器之前,最好能瞭解應用程序的需求及其自身特點。在下一節中,將介紹Java平臺內存模型中的陷阱,在這裏,陷阱指的是在動態生產環境中,Java程序員常常做出的一些中使性能更糟,而非更好的假設。

爲什麼調優無法取代垃圾回收

大多數Java程序員都知道,如果有不少方法可以最大化Java程序的性能。而當今衆多的JVM實現,垃圾回收器實現,以及多到令人頭暈的調優選項都可能會讓開發人員將大量的時間消耗在無窮無盡的性能調優上。這種情況催生了這樣一種結論,“GC是糟糕的,努力調優以降低GC的頻率或時長才是王道”。但是,真這麼做是有風險的。

考慮一下針對指定的應用程序需求做調優意味着什麼。大多數調優參數,如內存分配速率,對象大小,響應時間,以及對象死亡速度等,都是針對特定的情況而來設定的,例如測試環境下的工作負載。例如。調優結果可能有以下兩種:

  1. 測試時正常,上線就失敗。
  2. 一旦應用程序本身,或工作負載發生改變,就需要全部重調。

調優是需要不斷往復的。使用併發垃圾回收器需要做很多調優工作,尤其是在生產環境中。爲滿足應用程序的需求,你需要不斷挑戰可能要面對的最差情況。這樣做的結果就是,最終形成的配置非常刻板,而且在這個過程中也浪費了大量的資源。這種調優方式(試圖通過調優來消除GC)是一種堂吉訶德式的探索——以根本不存在的理由去挑戰一個假想敵。而事實是,你針對某個特定的負載而垃圾回收器做的調優越多,你距離Java運行時的動態特性就越遠。畢竟,有多少應用程序的工作負載能保持不變呢?你所預估的工作負載的可靠性又有多高呢?

那麼,如果不從調優入手又該怎麼辦呢?有什麼其他的辦法可以防止應用程序出現OOM錯誤,並降低響應時間呢?這裏,首先要做的是明確影響Java應用程序性能的真正因素。

碎片化

影響Java應用程序性能的罪魁禍首並不是垃圾回收器本身,而是碎片化,以及垃圾回收器如何處理碎片。碎片是Java堆中空閒空間,但由於連續空間不夠大而無法容納將要創建的對象。正如我在本系列第2篇中提到的,碎片可能是TLAB中的剩餘空間,也可能是(這種情況比較多)被釋放掉的具有較長生命週期的小對象所佔用的空間。

隨着應用程序的運行,這種無法使用的碎片會遍佈於整個堆空間。在某些情況下,這種狀態會因靜態調優選項(如提升速率和空閒列表等)更糟糕,以至於無法滿足應用程序的原定需求。這些剩下的空間(也就是碎片)無法被應用程序有效利用起來。如果你對此放任自流,就會導致不斷垃圾回收,垃圾回收器會不斷的釋放內存以便創建新對象時使用。在最差情況下,甚至垃圾回收也無法騰出足夠的內存空間(因爲碎片太多),JVM會強制拋出OOM(out of memory)錯誤當然,你也可以重啓應用程序來消除碎片,這樣可以使Java堆煥然一新,於是就又可以爲對象分配內存了。但是,重新啓動會導致服務器停機,另外,一段時間之後,堆將再次充滿碎片,你也不得不再次重啓。

OOM錯誤(OutOfMemoryErrors)會掛起進程,日誌中顯示的垃圾回收器很忙,是垃圾回收器努力釋放內存的標誌,也說明了堆中碎片非常多。一些開發人員通過重新調優垃圾回收器來解決碎片化的問題,但我覺着在解決碎片問題成爲垃圾回收的使命之前應該用一些更有新意的方法來解決這個問題。本文後面的內容將聚焦於能有效解決碎片化問題的方法:分代黛式垃圾回收和壓縮。

分代式垃圾回收

這個理論你可以已經聽說過,即在生產環境中,大部分對象的生命週期都很短。分代式垃圾回收就源於這個理論。在分代式垃圾回收中,堆被分爲兩個不同的空間(或成爲“代”),每個空間存放具有不同年齡的對象,在這裏,年齡是指該對象所經歷的垃圾回收的次數(也就是該對象挺過了多少次垃圾回收而沒有死掉)。

當新創建的對象所處的空間,即*年輕代*,被對象填滿後,該空間中仍然存活的對象會被移動到老年代。(譯者注,以HotSpot爲例,這裏應該是挺過若干次GC而不死的,纔會被搬到老年代,而一些比較大的對象會直接放到老年代。)大多數的實現都將堆會分爲兩代,年輕代和老年代。通常來說,分代式垃圾回收器都是單向拷貝的,即從年輕代向老年代拷貝,這點在早先曾討論過。近幾年出現的年輕代垃圾回收器已經可以實現並行垃圾回收,當然也可以實現一些其他的垃圾回收算法實現對年輕代和老年代的垃圾回收。如果你使用拷貝垃圾回收器(可能具有並行收集功能)對年輕代進行垃圾回收,那垃圾回收是stop-the-world式的(參見前面的解釋)。

分代式垃圾回收的缺陷

在分代式垃圾回收中,老年代執行垃圾回收的平率較低,而年輕代中較高,垃圾回收的時間較短,侵入性也較低。但在某些情況下,年輕代的存在會是老年代的垃圾回收更加頻繁。典型的例子是,相比於Java堆的大小,年輕代被設置的太大,而應用程序中對象的生命週期又很長(又或者給年輕代對象提升速率設了一個“不正確”的值)。在這種情況下,老年代因太小而放不下所有的存活對象,因此垃圾回收器就會忙於釋放內存以便存放從年輕代提升上來的對象。但一般來說,使用分代式垃圾回收器可以使用應用程序的性能和系統延遲保持在一個合適的水平。

使用分代式垃圾回收器的一個額外效果是部分解決了碎片化的問題,或者說,發生最差情況的時間被推遲了。可能造成碎片的小對象被分配於年輕代,也在年輕代被釋放掉。老年代中的對象分佈會相對緊湊一些,因爲這些對象在從年輕代中提升上來的時候會被會緊湊存放。但隨着應用程序的運行,如果運行時間夠長的話,老年代也會充滿碎片的。這時就需要對年輕代和老年代執行一次或多次stop-the-world式的全垃圾回收,導致JVM拋出_OOM錯誤_或者表明提升失敗的錯誤。但年輕代的存在使這種情況的出現被推遲了,對某些應用程序來說,這就就足夠了。(在某些情況下,這種糟糕情況會被推遲到應用程序完全不關心GC的時候。)對大多數應用程序來說,對於大多數使用年輕代作爲緩衝的應用程序來說,年輕代的存在可以降低出現stop-the-world式垃圾回收頻率,減少拋出OOM錯誤的次數。

 調優分代式垃圾回收

正如上面提到的,由於使用了分代式垃圾回收,你需要針對每個新版本的應用程序和不同的工作負載來調整年輕代大小和對象提升速度。我無法完整評估出固定運行時的代價:由於針對某個指定工作負載而設置了一系列優化參數,垃圾回收器應對動態變化的能力降低了,而變化是不可避免的。

對於調整年輕代大小來說,最重要的規則是要確保年輕代的大小不應該使因執行stop-the-world式垃圾回收而導致的暫停過長。(假設年輕代中使用的並行垃圾回收器。)還要記住的是,你要在堆中爲老年代留出足夠的空間來存放那些生命週期較長的對象。下面還有一些在調優分代式垃圾回收器時需要考慮的因素:

  1. 大多數年輕代垃圾回收都是stop-the-world式的,年輕代越大,相應的暫停時間越長。所以,對於那些受GC暫停影響較大的應用程序來說,應該仔細斟酌年輕代的大小。
  2. 你可以綜合考慮不同代的垃圾回收算法。可以在年輕代使用並行垃圾回收,而在老年代使用並行垃圾回收。
  3. 當提升失敗頻繁發生時,這通常說明老年代中的碎片較多。提升失敗指的是老年代中沒有足夠大的空間來存放年輕代中的存活對象。當出現提示失敗時,你可以微調對象提升速率(即調整對象提升時年齡),或者確保老年代垃圾回收算法會將對象進行壓縮(將在下一節討論),並以一種適合當前應用程序工作負載的方式調整壓縮。你也可以增大堆和各個代的大小,但這會使老年代垃圾回收的暫停時間延長——記住,碎片化是不可避免的。
  4. 分代式垃圾回收最適用於那些具有大量短生命週期對象的應用程序,這些對象的生命週期短到活不過一次垃圾回收週期。在這種場景中,分代式垃圾回收可有效的減緩碎片化的趨勢,主要是將碎片化隨帶來的影響推出到將來,而那時可能應用程序對此毫不關心。

壓縮

儘管分代式垃圾回收推出了碎片化和OOM錯誤出現的時機,但壓縮仍然是唯一真正解決碎片化的方法。*壓縮*是將對象移動到一起,以便釋放掉大塊連續內存空間的GC策略。因此,壓縮可以生成足夠大的空間來存放新創建的對象。

移動對象並修改相關引用是一個stop-the-world式的操作,這會對應用程序的性能造成影響。(只有一種情況是個例外,將在本系列的下一篇文章中討論。)存活對象越多,垃圾回收造成的暫停也越長。假如堆中的空間所剩無幾,而且碎片化又比較嚴重(這通常是由於應用程序運行的時間很長了),那麼對一塊存活對象多的區域進行壓縮可能會耗費數秒的時間。而如果因出現OOM而導致應用程序無法運行,因此而對整個堆進行壓縮時,所消耗的時間可達數十秒。

壓縮導致的暫停時間的長短取決於需要移動的存活對象所佔用的內存有多大以及有多少引用需要更新。當堆比較大時,從統計上講,存活對象和需要更新的引用都會很多。從已觀察到的數據看,每壓縮1到2GB存活數據的需要約1秒鐘。所以,對於4GB的堆來說,很可能會有至少25%的存活數據,從而導致約1秒鐘的暫停。

壓縮與應用程序內存牆

應用程序內存牆涉及到在GC暫停時間對應用程序的影響大到無法達到滿足預定需求之前所能設置的的堆的最大值。目前,大部分Java應用程序在碰到內存牆時,每個JVM實例的堆大小介於4GB到20GB之間,具體數值依賴於具體的環境和應用程序本身。這也是大多數企業及應用程序會部署多個小堆JVM而不是部署少數大堆(50到60GB)JVM的原因之一。在這裏,我們需要思考一下:現代企業中有多少Java應用程序的設計與部署架構受制於JVM中的壓縮?在這種情況下,我們接受多個小實例的部署方案,以增加管理維護時間爲代價,繞開爲處理充滿碎片的堆而執行stop-the-world式垃圾回收所帶來的問題。考慮到現今的硬件性能和企業級Java應用程序中對內存越來越多的訪問要求,這種方案是在非常奇怪。爲什麼僅僅只能給每個JVM實例設置這麼小的堆?併發壓縮是一種可選方法,它可以降低內存牆帶來的影響,這將是本系列中下一篇文章的主題。
從已觀察到的數據看,每壓縮1到2GB存活數據的需要約1秒鐘。所以,對於4GB的堆來說,很可能會有至少25%的存活數據,從而導致約1秒鐘的暫停。

總結:回顧

本文對垃圾回收做了總體介紹,目的是爲了使你能瞭解垃圾回收的相關概念和基本知識。希望本文能激發你繼續深入閱讀相關文章的興趣。這裏所介紹的大部分內容,它們。在下一篇文章中,我將介紹一些較新穎的概念,併發壓縮,目前只有Azul公司的Zing JVM實現了這一技術。併發壓縮是對GC技術的綜合運用,這些技術試圖重新構建Java內存模型,考慮當今內存容量與處理能力的不斷提升,這一點尤爲重要。

現在,回顧一下本文中所介紹的關於垃圾回收的一些內容:

  1. 不同的垃圾回收算法的方式是爲滿足不同的應用程序需求而設計。目前在商業環境中,應用最爲廣泛的是引用跟蹤垃圾回收器。
  2. 並行垃圾回收器會並行使用可用資源執行垃圾回收任務。這種策略的常用實現是stop-the-world式垃圾回收器,使用所有可用系統資源快速完成垃圾回收任務。因此,並行垃圾回收可以提供較高的吞吐量,但在垃圾回收的過程中,所有應用程序線程都會被掛起,對延遲有較大影響。
  3. 併發垃圾回收器可以與應用程序併發工作。使用併發垃圾回收器時要注意的是,確保在應用程序發生OOM錯誤之前完成垃圾回收。
  4. 分代式垃圾回收可以推遲碎片化的出現,但並不能消除碎片化。它將堆分爲兩塊空間,一塊用於存放“年輕對象”,另一塊用於存放從年輕代中存活下來的存活對象。對於那些使用了很多具有較短生命週期活不過幾次垃圾回收週期的Java應用程序來說,使用分代式垃圾回收是非常合適的。
  5. 壓縮是可以完全解決碎片化的唯一方法。大多數垃圾回收器在壓縮的時候是都stop-the-world式的。應用程序運行的時間越長,對象間的引就用越複雜,對象大小的異質性也越高。相應的,完成壓縮所需要的時間也越長。如果堆的大小較大的話也會對壓縮所佔產生的暫停有影響,因爲較大的堆就會有更多的活動數據和更多的引用需要處理。
  6. 調優可以推遲OOM錯誤的出現,但過度調優是無意義的。在通過試錯方式初始調優前,一定要明確生產環境負載的動態性,以及應用程序中的對象類型和對象間的引用情況。在動態負載下,過於刻板的配置很容會失效。在設置非動態調優選項前一定要清楚這樣做後果。

下個月的_JVM性能調優系列_:深入瞭解C4垃圾回收器(Continuously Concurrent Compacting Collector)相關算法。

關於作者

Eva Andearsson對JVM計數、SOA、雲計算和其他企業級中間件解決方案有着10多年的從業經驗。在2001年,她以JRockit JVM開發者的身份加盟了創業公司Appeal Virtual Solutions(即BEA公司的前身)。在垃圾回收領域的研究和算法方面,EVA獲得了兩項專利。此外她還是提出了確定性垃圾回收(Deterministic Garbage Collection),後來形成了JRockit實時系統(JRockit Real Time)。在技術上,Eva與Sun公司和Intel公司合作密切,涉及到很多將JRockit產品線、WebLogic和Coherence整合的項目。2009年,Eva加盟了Azul System公司,擔任產品經理。負責新的Zing Java平臺的開發工作。最近,她改換門庭,以高級產品經理的身份加盟Cloudera公司,負責管理Cloudera公司Hadoop分佈式系統,致力於高擴展性、分佈式數據處理框架的開發。

英文原文:JVM performance optimization, Part 3,翻譯:ImportNew - 曹旭東

譯文鏈接:http://www.importnew.com/2233.html

【如需轉載,請在正文中標註並保留原文鏈接、譯文鏈接和譯者等信息,謝謝合作!】

相關資源


JVM性能優化系列早期文章:


JavaWorld中的相關文章:

 

垃圾回收的相關書籍:

 

JVM調優與GC算法相關文章:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章