Java GC 專家系列3:GC調優實踐

segmentfault發表於2016-01-16

本篇是”GC專家系列“的第三篇。在第一篇理解Java垃圾回收中我們學習了幾種不同的GC演算法的處理過程,GC的工作方式,新生代與老年代的區別。所以,你應該已經瞭解了JDK 7中的5種GC型別,以及每種GC對效能的影響。

在第二篇Java垃圾回收的監控中介紹了在真實場景中JVM是如何執行GC,如何監控GC資料以及有哪些工具可用來方便進行GC監控。

在本篇中,我將基於真實的案例來介紹一些GC調優的最佳選項。寫本篇文章時,我假設你已經理解了前兩篇的內容。為了深入理解本部分內容,你最好先瀏覽一下前兩篇的內容——如果你尚未了解的話。

GC調優是必須的嗎

更精確的說, 基於Java的服務是否一定需要GC調優 ?應該說,GC調優並非所有Java服務都必須做的事情。當然這是基於你已經使用了下面的選項或事實:

  • 通過 -Xms 和 -Xmx 選項指定了記憶體大小
  • 使用了 -server 選項
  • 系統未產生太多超時日誌

也就是說,如果你未設定記憶體大小並且你的系統產生了過多的超時日誌,恭喜你需要為你的系統執行GC調優。

但是,請記住: GC調優是不得已時的選擇

思考一下GC調優的深層原因。垃圾回收器會去清理Java中建立的物件。GC需要清理的物件資料以及GC執行的次數取決於應用建立物件的多少。因此,為了控制GC的執行,首先你需要 減少物件的建立

俗話說“積重難返”。所以我們需要從小處著手,否則它們將不斷壯大直到難以管理。

  • 應該多使用 StringBuilder 和 StringBuffer 物件替代 String 。
  • 減少不必要的日誌輸出。

即便如此,面對有些場景我們依然無能為力。我們知道解析XML和JSON會佔用大量的記憶體空間。即便我們儘可能少的使用 String ,儘可能好的優化日誌輸出,然而在解析XML和JSON時仍然會有大量的記憶體開銷,甚至有10~100MB之多,可我們很難杜絕XML和JSON的使用。但是請記住:XML和JSON會帶來很大的記憶體開銷。

如果應用的記憶體佔用不斷提升,你就要開始對其進行GC調優了。我把GC調優的目標分為以下兩類:

  • 降低移動到老年代的物件數量
  • 縮短Full GC的執行時間

降低移動到老年代的物件數量

在Oracle JVM中除了JDK 7及最高版本中引入的G1 GC外,其他的GC都是基於分代回收的。也就是物件會在Eden區中建立,然後不斷在Survivor中來回移動。之後如果該物件依然存活,就會被移到老年代中。有些物件,因為佔用空間太大以致於在Eden區中建立後就直接移動到了老年代。老年代的GC較新生代會耗時更長,因此減少移動到老年代的物件數量可以降低full GC的頻率。減少物件轉移到老年代可能會被誤解為把物件保留在新生代,然而這是不可能的,相反你可以 調整新生代的空間大小

縮短Full GC耗時

Full GC的單次執行與Minor GC相比,耗時有較明顯的增加。如果執行Full GC佔用太長時間(例如超過1秒),在對外服務的連線中就可能會出現超時。

  • 如果企圖通過縮小老年代空間的方式來降低Full GC執行時間,可能會面臨 OutOfMemoryError 或者帶來更頻繁的Full GC。
  • 如果通過增加老年代空間來減少Full GC執行次數,單次Full GC耗時將會增加。

因此,需要 為老年代空間設定適當的大小

影響GC效能的選項

在理解Java垃圾回收的結尾,我說過不要有這樣的想法: 別人通過某個GC選項獲得了明顯的效能提升,為什麼我不直接用這個選項呢 。因為 不同的服務所擁有的物件數量和物件的生命週期是不同的

一個簡單場景,如果執行一個任務需要五個條件:A, B, C, D和E,另外一個任務只需要兩個條件A和B,哪個任務會快一些?通常只需要條件A和B的任務會快一些。

Java GC選項的設定也是一樣的道理。設定很多選項未必能提高GC執行速度,相反還可能會更加耗時。 GC調優的基本規則是對兩臺或更多的伺服器設定不同的選項,並對比效能表現 ,然後把被證明能提升效能的選項新增到應用伺服器上。請記住這一點。

下表列出了與記憶體相關的且會影響效能的GC選項:

表1: GC調優需要關注的選項

分類 選項 說明
堆空間 -Xms 啟動JVM時的初始堆空間大小
-Xmx 堆空間最大值
新生代空間 -XX:NewRatio 新生代與老年代的比例
-XX:NewSize 新生代大小
-XX:SurvivorRatio Eden區與Survivor區的比例

我經常會使用的選項是: -Xms , -Xmx 和 -XX:NewRatio ,其中 -Xms 和 -Xmx 是必須的。而如何設定 -XX:NewRatio 對效能會有顯著的影響。

可能有人會問 如何設定永久代(Perm)的大小 , 可以使用 -XX:PermSize 和 -XX:MaxPermSize 進行設定,但記住只有發生由Perm空間不足導致的 OutOfMemoryError 時才需要設定。

另外一個會影響GC效能的選項是GC型別,下表列出了JDK 6.0中能使用的相關設定選項:

表2: GC型別選項

分類 選項 說明
Serial GC -XX:+UseSerialGC
Parallel GC -XX:+UseParallelGC-XX:ParallelGCThreads=<value>
Parallel Compacting GC -XX:+UseParallelOldGC
CMS GC -XX:+UseConcMarkSweepGC
-XX:UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=<value>
-XX:+UseCMSInitiatingOccupancyOnly
G1 -XX:+UnlockExperimentalVMOptions-XX:+UseG1GC 在JDK6中使用G1時,這兩個選項必須同時設定

除了G1,其他GC型別都是通過每個選行列的第一行選項進行設定。通常最不會使用的是Serial GC,它是為client應用優化和設計的。

還有很多其他影響GC效能的選項,但不如上面這些對效能的影響明顯。另外設定更多選項未必能優化GC的執行時間。

GC調優過程

GC調優過程與一般的效能改進流程很相似,下面會介紹我在GC調優過程中的流程。

1. 監控GC狀態

首先需要監控GC狀態資訊以明確在GC操作過程中對系統的影響。具體方式可以回顧上一篇文章:Java 垃圾回收的監控。

2. 分析監控資料並決定是否需要GC調優

然後通過GC操作狀態,對監控結果進行分析,並判斷是否有必要進行GC調優。如果分析結果顯示GC耗時在0.1-0.3秒以內的話,一般不需要花費額外的時間做GC調優。然而, 如果GC耗時達到1-3秒甚至10秒以上,就需要立即對系統進行GC調優 。

但是如果你的應用分配了10GB的記憶體,且不能降低記憶體容量的話,其實是沒辦法進行GC調優的。這種情況下,你首先要去思考為什麼需要分配這麼大的記憶體。如果只給應用分配了1GB或者2GB記憶體,當有 OutOfMemeoryError 發生時,你需要通過堆dump來分析驗證記憶體溢位的原因並進行修復。

註釋:堆dump是把記憶體情況按一定格式輸出到檔案,可用於檢查Java 記憶體中的物件和資料情況。可使用JDK中內建的 jmap 命令建立堆dump檔案。建立檔案過程中,Java程式會中斷,因此不要在正常執行時系統上做此操作。

3. 設定GC型別和記憶體大小

如果決定做GC調優,就需要考慮如何選擇GC型別、如何設定記憶體大小。如果你有多臺伺服器,可通過為每臺伺服器設定不同的GC選項並對比不同的表現,這一步很重要。

4. 分析GC調優結果

設定GC選項後,至少要收集24小時的GC表現資料,然後就可以著手分析這些資料了。如果足夠幸運,通過分析就剛好找到了最合適的GC選項。否則就需要分析GC日誌,並分析記憶體的分配情況。然後通過不同的調整GC型別和記憶體大小來找到系統的最優選項。

5. 如果結果可接受,則對所有服務應用調優選項並停止調優

如果GC結果令人滿意,就可以把相應的選項應用到所有伺服器並停止GC調優。

下面的章節會詳細介紹每個步驟中的詳細過程。

監控GC狀態並分析GC結果

監控Web應用(WAS: Web Application Server)GC執行狀態的最好方式是使用 jstat 命令。在Java 垃圾回收的監控部分已經介紹瞭如何使用jstat命令,所以這裡就直接介紹怎麼樣來校驗結果資料。

下面的例子中列出了JVM未做GC調優時的資料:

$ jstat -gcutil 21719 1s
S0    S1    E    O    P    YGC    YGCT    FGC    FGCT GCT
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673

看一下表中的YGC和YGCT,YGCT 除以 YGC算出平均單次YGC耗時為0.05秒。也就是說在新生代執行一次垃圾回收的平均耗時為50毫秒。通過這份結果,我們可以無須關注新生代的垃圾回收。

然後再看一下FGCT和FGC,FGCT除以FGC算出平均單次FGC耗時為19.68秒。也就是平均需要消耗19.68秒來執行一次Full GC。上面的結果(共3次Full GC)可能是每次Full GC都耗時19.68秒,也有可能是其中兩次都只耗時1秒,而另外一次卻消耗了58秒。然而不管哪種情況,都迫切需要進行GC調優。

當然也可以通過 jstat 來校驗結果,不過分析GC的最好方式是使用 -verbosegc 選項來啟動JVM。在前面的文章中我已經詳細介紹了生成日誌的方式以及如何進行分析。就分析 -verbosegc 日誌而言, HPJMeter 是我最偏愛的工具,因為它簡單易用。使用HPJMeter可以輕鬆獲取GC執行時間的開銷以及GC發生的頻率。

如果GC執行時間滿足以下判斷條件,那麼GC調優並沒那麼必須。

  • Minor GC執行迅速(50毫秒以內)
  • Minor GC執行不頻繁(間隔10秒左右一次)
  • Full GC執行迅速(1秒以內)
  • Full GC執行不頻繁(間隔10分鐘左右一次)

括號內的值並非絕對,依據應用的服務狀態會有不同。有些服務可能要求Full GC處理速度不能超過0.9秒,另外一些服務可能會寬鬆些。因此校驗GC結果並根據具體的服務需要,決定是否要進行GC調優。

在校驗GC狀態時,不要只關心Minor GC和Full GC的耗時,也要 GC執行次數也同樣重要 。如果新生代太小,Minor GC就會頻繁執行(甚至每間隔1秒就要執行一次)。另外,新生代太小導致轉移到老年代的物件增多,也會引起Full GC的頻繁執行。因此使用`-gccapacity`配合jstat命令,以檢查記憶體空間的使用情況。

設定GC型別和記憶體大小

設定GC型別

Oracle JVM提供了5種GC型別,如果是低於JDK 7的版本,可以使用Parallel GC, Parallel Compacting GC, CMS GC。當然,到底選哪一個並沒有統一的準則或標準。

所以 如何選擇合適的GC型別 ?推薦方案是將這三種GC都應用到應用中進行對比。不過可以明確的是CMS GC肯定比Parallel GCs更快,即然這樣只使用CMS GC便好。然而CMS GC也有出問題的時候,通常Full GC中使用CMS GC會執行更快,如果CMS GC的併發模式失敗,則會出現比Parallel GCs慢的情況。

併發模式失敗

我們來深入看一下併發模式失敗的場景。

Parallel GC與CMS GC最大的區別在於壓縮任務。壓縮任務通過壓縮記憶體使用來移除記憶體中的碎片空間,以清理兩塊已分配使用的記憶體空間中的間隙。

在Parallel GC中,只要執行Full GC便會進行記憶體壓縮,因此耗時更長。不過Full GC之後,因為壓縮的原故,可以分配連續的空間,所以記憶體的分配速度為更快一些。

與之相反,CMS GC的執行中並不會伴隨記憶體壓縮,因此GC速度會更快一些。然而,因此未做記憶體壓縮, GC清理過程中釋放的記憶體便會成為空閒空間。因為空間不連續,可能會導致在建立大物件時空間不足。例如,如果老年代尚有300M空閒,卻不能為10MB的物件分配足夠的連續空間。這時便會發生 併發模式失敗 的警告,並觸發記憶體壓縮。如果使用CMS GC,在記憶體壓縮過程中可能會比Parallel GCs更為耗時,也可能會帶來其他問題。關於”併發模式失敗”更詳細的介紹可以看Oracle 工程師的文章: 理解CMS GC 日誌

結論就是,要為你的系統尋找合適的GC型別。

每個系統都有一個最適當的GC型別,所以你需要找到這個GC型別。如果你有6臺伺服器,建議你為每兩組設定相同的選項,並通過 -verbosegc 選項對結果進行分析和比較。

調整記憶體大小

下面先列出記憶體大小與GC執行次數、每次GC耗時之間的關係:

  • 大記憶體
    • 會降低GC執行次數
    • 相應的會增加GC執行耗時
  • 小記憶體
    • 會縮知單次GC耗時
    • 相應的會增加GC執行次數

當然,關於使用大記憶體還是小記憶體並沒有唯一正確的答案。如果伺服器資源足夠且Full GC執行耗時能控制在1秒以內,使用10GB的記憶體也是可以的。但大多數時候如果設定記憶體為10GB,GC執行效果並不盡人意,執行一次Full GC可能要消耗10~30秒(具體時長也會根據物件大小情況而不同)。

既然如此, 如何正確設定記憶體大小 。通常情況下,我會推薦500MB大小。這不是說你要把自己的WAS(Web Application Server)記憶體選項設定為 -Xms500 和 -Xmx500m 。基於當前未調優時的場景,檢查Full GC之後記憶體大小變化。如果Full GC之後尚有300MB空間剩餘,這樣最好把記憶體設定到1GB(300MB(預設使用) + 500MB(老年代最小容量) + 200MB(空閒空間))。這意味著你應該才老年代至少設定500MB空間。如果你有3臺伺服器,可以分別設定1GB、1.5GB和2GB,並檢查每臺機器的執行結果。

理論上,根據記憶體大小不同單次執行GC速度應該是1GB > 1.5GB > 2GB,所以1GB的記憶體會中三個之中GC速度最快的。但並不能保證1GB的記憶體Full GC耗時1秒,2GB的記憶體Full GC耗時2秒。實際耗時與機器效能和物件大小也有關係。所以最好的度量方式是設定每種可能性並分析他們的監控結果。

有設定記憶體大小時,還需要設定另外一選項: NewRatio 。 NewRatio 是新生代與老年代的比值的倒數(即老年代與新生代的比值)。如果 XX:NewRatio=1 ,就是說新生代 : 老年代的比值為1:1。對於1GB記憶體,就是新生代與老年代各500MB。如果 NewRatio 的值是2,則是新生代 : 老年代的值為1:2。因此比值設定的越大,老年代的空間就越大,相應的新生代空間會越小。

設定 NewRatio 也不是一件重要的事,但可能會對整個GC效能帶來嚴重影響。如果新生代太小,物件就會轉移到老年代,引起頻繁的Full GC,導致更多的耗時。

你可能簡單的認為設定 NewRatio=1 會帶來最佳的效果,然而並非如此。把 NewRatio 設定為2或3更容易帶來好的GC表現。當然我也實際遇到過一些這樣的例子。

完成GC調優的最快途徑是什麼?通過對比效能測試的結果是得到GC調優結果的最快途徑。通過為每個伺服器設定不同的選項並觀察GC狀態,最好能觀察1到2天的資料。如果是通過效能測試來做GC調優的話,要為每個伺服器準備相同的負載和業務操作。請求比例的分配也要與業務條件相一致。然而即便是專業的效能測試人員,準備精確的負載資料也並非易事,通常需要花費很大精力來做準備。所以更簡捷的GC調優方式就是對業務應用準備GC選項,然後通過等待GC結果並進行分析,儘管可能需要更長的等待時間。

分析GC調優結果

在應用GC選項並設定 -verbosegc 後,可以通過 tail 命令檢查日誌是否按期望的方式正常輸出。如果選項未精確的設定或者沒有按期望輸出,你所花費的時間都將白費。如果日誌輸出與期望相符,等待1到2天的執行後便可檢查和分析結果。最簡單的方式是把日誌檔案複製到本地PC,並使用 HPJMeter 進行分析。

分析過程中主要關注以下資料,下面列表是按我自己定義的優先順序列出的。其中決定GC選項的最重要的資料是Full GC執行時間。

  • Full GC(平均)耗時
  • Minor GC(平均)耗時
  • Full GC執行間隔
  • MinorGC執行間隔
  • Full GC整體耗時
  • Minor GC整體耗時
  • GC整體耗時
  • Full GC執行次數
  • Minor GC執行次數

如果足夠幸運,你能恰好找到合適的GC選項,通常你並沒這麼幸運。執行GC調優時一定要格外小心,因為如果你試圖一次就完成GC調優,得到的可能會是 OutOfMemoryError 。

調優案例

上面我們對於GC調優的討論還僅是紙上談兵,現在開始我們看一些具體的GC調優的案例。

案例1

這個例子是為服務S進行的GC優化。對於這個新上線的服務S,在執行Full GC時有些過於耗時。

先看一下 jstat -gcutil 的結果:

S0 S1 E O P YGC YGCT FGC FGCT GCT
12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993

在開始進行調優時不用太關心 持久代 空間的設定,相對而言YGC的數值更值得關注。

從上面的結果中我們可算出執行Minor GC和Full GC的平均時間上的開銷,如下表:

表3:服務S執行Minor GC和Full GC的平均耗時

GC型別 GC 執行次數 GC執行時間 平均耗時
Minor GC 54 2.047 37 ms
Full GC 5 6.946 1389 ms

對於Minor GC來說, 37 ms 還不算壞,而Full GC的平均耗時 1.389 s 對於系統來說在執行Full GC時可能會導致頻繁的超時現象,例如DB超時設定為1 s的話就會發生超時。所以這個案例中的系統需要進行GC調優。

首先在開始GC調優之前先檢查當前的記憶體設定。可以使用 jstat -gccapacity 選項檢視記憶體的使用情況。下面是服務S的檢查結果:

NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC
212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5

其中關鍵的資料如下:

  • 新生代使用:212, 992 KB(約208 MB)
  • 老年代使用:1,884,160 KB(約1.8 GB)

所以除去持久代之外的記憶體分配為2 GB,且新生代 : 老年代為 1:9 (即 NewRatio=9 )。為了看到更詳細的資訊,對系統的三個不同實現均設定了 -verbosegc 並分別設定了 NewRatio 選項,除此之外未新增其他選項。

  • NewRatio = 2
  • NewRatio = 3
  • NewRatio = 4

一天之後檢查GC時日誌時幸運的發生,在設定 NewRatio 之後尚未有Full GC發生。

發生了什麼?因為大多數物件在建立之後不久就被銷燬,所以新生代裡的物件在移到老年代之前就被銷燬掉了。

既然如此,就沒必要再設定其他選項,只是選擇好最佳的 NewRatio 即可。 如何選取最佳NewRatio ?只能逐個分析設定不同 NewRatio 值時的Minor GC的平均耗時。

上面三個 NewRatio 設定對應的Minor GC平均耗時如下:

  • NewRatio=2: 45ms
  • NewRatio=3: 34ms
  • NewRatio=4: 30ms

因為 NewRatio=4 時Minor GC具有最小的耗時,所以就是我們選擇的最佳設定,即便此時新生代的空間相對較小。應用此選項後,服務再也沒有Full GC發生。

下面是系統重新設定過選項後,某天通過 jstat -gcutil 檢視到的結果:

S0 S1 E O P YGC YGCT FGC FGCT GCT
8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219

你可能認為因為系統接收的請求太少以致於GC發生頻率較低,然而在Minor GC執行了2,424次的情況下系統未發生Full GC。

案例2

下面介紹的是服務A的例子。我們在公司的應用效能管理平臺(APM: Application Performance Manager)上發現服務A的JVM週期性的出現長時間的停頓(超過8秒未有響應)的現象。所以我們決定對其進行GC調優。經過排查我們發現此係統在執行Full GC時太過耗時,需要進行優化。

在著手優化之前,我們為系統加上了 -verbosegc 選項,輸出結果如下圖:

[譯]GC專家系列3-GC調優

圖1:GC調優之前的GC耗時

上圖是HPJMeter自動分析結果後提供的系統GC隨著JVM執行的耗時圖。 X-軸 是JVM從啟動後的執行時間軸, Y-軸 是每次GC的響應時間。其中綠色的是Full GC使用的CMS垃圾回收的耗時,藍色的是Minor GC使用的Parallel Scavenge垃圾回收的耗時。

前面我說過CMS GC是最快的,但上圖可看到有場景耗時竟達到15秒之多。 什麼原因導致這種後果? 回想一下我前面說過的:當記憶體壓縮時CMS將會變慢。另外服務A設定了 -Xms1g 和 -Xmx4g 的選項,作業系統為其分配的記憶體為4 GB。

然後我把GC型別由GMS換成了Parallel GC,並把記憶體大小設定為2G, NewRatio 設定為3。一段時間之後通過 jstat -gcutil 檢視到的結果如下:

S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890

Full GC的速度提升了,與4GB記憶體時的15秒相比,現在平均每次只需要3秒。但3秒仍然不盡人意,所以我設計了以下六組選項:

  • -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2
  • -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3
  • -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3
  • -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2
  • -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3
  • -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3

哪一個會更快呢?結果顯示記憶體越小,速度越快。下圖是第六組選項的GC持續時長分佈圖,代表了最優的GC效能提升。圖中看到最慢的為1.7秒,而平均值降低到1秒以內。

[譯]GC專家系列3-GC調優

圖2:使用第六組選項後的GC耗時

因此我把服務A的GC選項調整為了第六組中的設定,然而每天夜裡卻連續發生了 OutOfMemoryError 。箇中艱辛不再細說,簡而言之就是批量的資料處理任務導致了JVM記憶體洩露。到此為止,所有的問題都明瞭了。

如果只對GC日誌做短時間的觀察例把GC調優的結果應用到所有伺服器上是一件非常危險的事情。一定要記住,如果GC調優能夠順利執行而無故障只有一條途徑:像分析GC日誌一樣分析系統的每一個服務操作。

上面通過兩個GC調優的案例演示了GC調優的具體處理過程。如我所述,案例中的GC選項可以不做調整的應用到那些具有相同CPU、作業系統和 JDK 版本以及執行相同功能的服務上去。然而不要把這些選項應用到你的系統上,因為他們未必適用。

總結

我執行GC調優一般基於經驗而無需通過堆dump後對記憶體進行詳細的分析,儘管精確的記憶體狀態可能會帶來更好的GC調優結果。在一般情景,如果記憶體負載較低時,通過分析記憶體物件可能效果更好,不過如果服務負載較高,記憶體空間使用較多時,更推薦基於經驗來做GC調優。

我曾經在一些服務上對G1 GC做過效能測試,不過還沒有全面使用。結果證明G1 GC執行速度比其他任何GC都要快,不過需要把JDK升級到 JDK 7 才能享受到G1帶來的效能提升,另外G1的穩定性目前尚不能完全保證,沒有人知道是否會帶來嚴重的bug。所以大範圍使用 G1 還尚待時日。

當 JDK 7 穩定以後(並不是說它當前不穩定),並且WAS針對JDK 7做過優化之後,G1也許會穩定的執行在伺服器上,到那時也許就不再需要進行GC調優了。

更多GC調優的細節可以在 Slideshare 上搜尋相關材料。我最推薦的是Twitter 工程師 Attila Szegedi寫的這篇 我在Twitter學到的關於JVM調優的一切 ,有時間可以學習一下。

作者:Sangmin Lee, 效能實驗室高階工程師,NHN公司

相關文章