Tencent Kona JDK11無暫停內存管理-ZGC生產實踐

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"騰訊大數據JVM團隊基於OpenJDK11自研的Tencent Kona JDK11,目前已將ZGC特性孵化成熟,性能優於OpenJDK所提供的版本,使Java能夠輕鬆構建響應時間在ms級別的強實時性在線服務,極大提高研發和運維效率,目前在騰訊內部多業務場景生產落地,實現業務延遲SLA 提升2-3個數量級。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨着2021年4月30日Tencent Kona JDK 11.0.10-GA 正式對外發布,生產可用的ZGC也正式對外開源。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過二十多年的發展,Java語言的生態已經龐大無比,應用範圍覆蓋了從嵌入式設備到大型數據中心等場景,形成了各色各樣的業務形態。不同的業務關注點不盡相同,如部分離線應用關注整個系統的吞吐率,而不太關注單個進程的停頓時間;另外一些應用則對於GC停頓的時間有嚴格的要求,比如以下業務形態:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在線服務交互。和用戶交互的UI線程,需要按照特定的頻率進行屏幕的刷新,比方說普通60HZ刷新率的屏幕,在播放動畫時,需要在1s內刷新60次,才能保持屏幕畫面的連續性,即需要在15ms內完成一次刷新,如果此時由於GC停頓導致UI線程掛起,則會導致畫面出現撕裂感,最終導致用戶體驗的下降。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"競價廣告。在競價廣告應用場景下,如Real Time Bidding中的廣告競價投放,一個廣告欄請求播放廣告時,不同的廣告主則要根據當前的用戶價值進行交易競價,通常來說需要在規定的時間內(一般爲100ms到200ms)達成交易,否則就會錯失一次廣告曝光機會,此時GC停頓的控制就顯得非常重要。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"量化交易。在交易機會出現時,交易機構需要以最快的速度達成交易,對於實時性的要求就更爲嚴苛。如果出現由於GC停頓造成的延遲,輕則錯失交易機會,重則導致虧損。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了滿足不同的業務需求,Java的GC算法也在不停迭代,對於特定的應用,選擇其最適合的GC算法,才能更高效的幫助業務實現其業務目標。對於這些延遲敏感的應用來說,GC停頓已經成爲阻礙Java廣泛應用的一大頑疾,需要更適合的GC算法以滿足這些業務的需求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"近些年來,服務器的性能越來越強勁,各種應用可使用的堆內存也越來越大,常見的堆大小從10G到百G級別,部分機型甚至可以到達TB級別,在這類大堆應用上,傳統的GC,如CMS、G1的停頓時間也跟隨着堆大小的增長而同步增加,即堆大小指數級增長時,停頓時間也會指數級增長。特別是當觸發Full GC時,停頓可達分鐘級別。當業務應用需要提供高服務級別協議(Service Level Agreement,SLA),例如99.99%的響應時間不能超過100ms,此時CMS、G1等就無法滿足業務的需求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲滿足當前應用對於超低停頓、高SLA的需求,並應對大堆和超大堆帶來的挑戰,伴隨着2018年發佈的JDK 11,A Scalable Low-Latency Garbage Collector - ZGC應運而生。騰訊大數據JVM團隊的Tencent Kona JDK作爲OpenJDK Hotspot VM下游分支,也致力於在LTS的JDK11版本上提供Production Ready的ZGC功能,滿足公司內部客戶的需求。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1.GC停頓"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.1 停頓之由來"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Hotspot虛擬機上,GC算法均是基於Mark Sweep或者Mark Compact實現的,也可以稱爲Tracing GC。對於這類標記掃描的GC算法來說,需要通過Mark找到所有活着的對象,然後將死對象清除,或者把所有的活對象拷貝到另外一塊區域,以達到清理內存的目的。因此,所有的Tracing GC均需要以下三個步驟,其整體過程如下面動圖所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/3e\/a8\/3e4e01b36aaaa696fa141330106489a8.gif","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"找出所有的GC Roots集合:這是Tracing GC算法的起點,GC Roots主要爲運行時的關鍵數據結構中存放的指向堆對象的指針,如線程棧上的堆對象指針等。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"標記過程:從GC Roots開始遍歷整個對象圖,找出所有存貨的對象。而剩餘未被標記的對象則爲死對象。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"清理過程:將死掉的對象清理掉,釋放其佔用的內存。當然清理時可以直接釋放對象內存,也可以將所有的活對象移動到一塊連續的區域裏,並將原來的內存空間釋放。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,上面三個步驟均需要一致的信息,如GC Roots需要是完整的,不能在掃描時被隨意修改;標記過程,需要掃描到所有活着的對象,其他線程不能隨意修改對象圖;清理過程如果需要搬移對象,則需要更新所有指向該對象的地方,如對象A指向B,B被搬動之後,A中的指針需要同步更新。因此要求在這三步中,採取同步措施,而最簡單的同步措施就是暫停所有的Java線程,即Stop-The-World(STW),在STW期間,GC線程就可以安全的訪問各種運行時數據、對象圖、更新對象指針等。如果需要降低STW的時間,則需要將GC的不同階段的任務移出STW,和Java線程進行併發執行,這個時候就需要算法和數據結構方面的更改,以滿足GC線程和Java線程對當前GC的一致性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不同的GC算法實現,其STW階段需要完成的任務大相徑庭,造成不同GC算法STW時長的不同。所有的任務均在STW階段完成時,這類GC就不需要和應用線程搶佔CPU,從應用整體來看,最終的吞吐率是比較高的,如Parallel GC;而當STW階段的任務減少時,則需要在併發階段增加相應的任務——即部分GC任務需要和業務線程一起運行,相互搶佔CPU,這類GC根據不同的任務劃分,最終在吞吐率和停頓之間達到一個平衡,如CMS和G1致力於以較小的吞吐率損失換取較小的停頓和較高的響應;ZGC和ShenandoahGC則關注極致停頓,盡一切可能減少STW的工作量,從而實現ms級別的停頓時間。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"1.2 業界解決方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了與Apple的iOS系統進行競爭,Google主導的Android系統需要解決的一大問題就是顯示卡頓問題,通過對GC算法的不斷演進,實現基於Baker Barrier的Concurrent Copy GC算法,停頓時間控制在幾個ms級別,小於15ms的刷新約束,補全了Java在嵌入式設備中的短板,使得鬆散的Java生態能夠實現和嚴格控制的Apple生態一樣流暢的系統。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在ZGC和ShenandoahGC出現之前,Hotspot JVM的GC停頓很難平穩的控制在百毫秒以下,極大地阻礙了OpenJDK在金融行業等延遲敏感的場景的應用。但是作爲OpenJDK的下游分支,Azul的Zing虛擬機憑藉其閉源的C4 GC,實現了近乎“無停頓”的低延遲,在前十幾年中大放異彩,頻繁出現在各類交易系統中。雲上的各類系統和普通的桌面應用,則面臨着無低延遲GC可用的窘境。爲滿足業務需求,一般會採用C++等Native語言重寫一些重要模塊,如騰訊的廣告系統採用C++實現,或者購置Zing等實現低延遲GC的VM,抑或在CMS和G1上八仙過海、各顯神通,利用經驗調整參數來滿足基本的業務需求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Hotspot上的停頓,導致備選方案面臨C++的高開發門檻、經費成本和調參經驗等各種問題,以及大堆管理問題,使得停頓成爲Java開發者心中的夢魘,阻礙了Java在低延遲需求業務的應用,此乃當前Hotspot JVM上的停頓之殤、開發者之痛。爲了解決這一問題,ZGC採用了和Azul的Zing VM相似的GC算法,從JDK11開始開源孵化,直到JDK15補全各類功能,成爲真正可以商用的正式版本,保證了Java停頓時間不會隨着堆大小和業務規模的增加而增長。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JDK11在2018年下半年發佈,是最新的Long-Term Support版本,而後續LTS版本爲JDK17,將於2021年下半年發佈,JDK12到JDK16屬於中間過渡開發版本,不會像JDK11和JDK17一樣提供持續的更新和修復。ZGC在OpenJDK11上屬於Experimental實驗特性,無法滿足業務的商用需求,騰訊JVM團隊爲了提前滿足業務的需求,在Tencent Kona JDK11持續的更新和修復的同時,將ZGC的各項功能補全,並進行了長期的驗證落地,使得Tencent Kona JDK11上的ZGC能夠達到商用水平,讓停頓敏感的業務應用在JDK11這個LTS版本上實現超低GC延遲。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2. ZGC簡介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZGC是由提議JEP 333(https:\/\/openjdk.java.net\/jeps\/333)引入Hotspot Runtime,其目標是爲了徹底解決GC停頓帶來的延遲問題,總的設計目標爲:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每次GC總的停頓時間控制在10ms以下"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相對於G1,應用的吞吐率降低不超過15%"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"支持大堆和特大堆(8MB~16TB),並且停頓時間不隨堆大小的增長而增長"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/9e\/b1\/9e28c60d3bcc77f92439839fcba018b1.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由設計目標可知,ZGC主要是爲現在及未來大堆的管理問題服務,致力於以最小的性能損失換取最大的停頓優勢。從Oracle發佈的測試數據來看(參見[1]),上圖中SPECjbb2015上ZGC的吞吐率(max-JOPS)和Parallel GC、G1GC相差無幾,而體現停頓影響的指標critical-JOPS則提升了20%+;在暫停時間上,ZGC則不會超過10ms,而Parallel GC和G1GC則高達100ms+,如下圖所示。因此,ZGC尤其適合對延遲比較敏感的大堆任務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/7b\/be\/7b4d4bd405c1b5e5eb4f0e8b1f7d15be.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 ZGC算法實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲減少停頓,需要減少STW中執行的任務,ZGC主要在以下三個方面進行推進:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"GC Roots的掃描。將能夠移除到STW以外的Roots掃描外移到併發階段,Roots掃描的併發外移需要對Roots的數據結構進行改造,以支持GC線程和Java線程同時操作。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"Runtime數據結構的處理。在Runtime中維護了很多張表來記錄Meta(class、method、jit code等),並且Java存在一類特殊的弱引用,即java.lang.ref.Reference及其子類,需要額外處理。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"對象移動的併發化改造。爲了能夠讓移動對象和Java線程同時運行,需要增加Read barrier來保證每次對象field讀取的正確性。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZGC在GC算法的處理邏輯上有很大的變更,但是在整體邏輯上,與其前輩GC算法一樣,都是Mark&Compact形式。具體實現上,ZGC下面六個階段通過來實現低延遲的GC算法,如下圖所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/0e\/95\/0e47dc93713f2d08061a8a3300b38e95.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一個階段是Pause Mark Start:主要做一些全局狀態的設置和全局數據結構的初始化這類輕量化的任務,標明後續併發階段需要做GC的Concurrent Mark。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二個階段是Concurrent Mark & Remap:將耗時佔比最大的GC Roots進行併發化改造,支持併發Roots標記。從GC Roots進行對象圖的併發標記。上一輪GC的指針更新(Remap)通過Piggyback,放到當前階段執行,從而減少對對象圖的遍歷。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三個階段是Pause Mark End:這一階段做Concurrent Mark的同步,結束併發標記階段,同時設置部分全局變量。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第四個階段是Concurrent Prepare:這一階段主要做java.lang.ref.Reference等弱引用的處理,並選擇出需要Compact的ZGC Region。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第五個階段是Pause Relocate Start:這一階段和第三階段比較類似,主要是全局同步,設置全局變量,並指示Relocate階段的開始。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第六個階段是Concurrent Relocate:併發的搬移對象。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"相對於其他GC,ZGC需要三個STW階段來做全局的同步,但每個STW中的任務都很明確,需要完成的任務的時間和CPU的處理速度正相關,因此可以做到ms級別的停頓。相對於G1GC,ZGC的難點在於如何進行GC Roots的併發化改造和對象搬移的併發化改造。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/14\/13\/14e23cf8ee1b1557fd929d953e8c7213.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於對象搬遷的併發化改造,ZGC則採用Colored Pointer來實現輕量級的Read Barrier,如上圖所示。對於64bit的系統,高位bit中拿出4個bit來指示不同的處理狀態,兩個Mark位表明該對象指針是否已經被標記,採用兩個Mark bit可以在前後不同的GC時使用不同的Mark bit;Remapped位表示當前對象指針是否已經調整爲搬移之後的對象指針;Finalizable位主要是爲Finalizable對象服務,用來表示該對象指針是否僅經Finalize對象標記,主要供Mark階段和弱引用處理階段使用。通過Colored指針,不同的GC階段,當前Runtime的正確的指針顏色僅爲一種顏色(Marked或者Remapped),就可以通過下圖所示,測試對象指針是否爲bad color即可,在x86上最終實現爲一條test指令和一條jne跳轉指令。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/27\/b4\/270d1f29503f3b6f548a9e9354e755b4.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Colored Pointer導致不同的時期,對象的指針的高位是不同,如下圖中的對象指針0x0000000012345678,在程序運行過程中,可能以下面三種狀態被Java線程感知到:Remapped狀態、Mark1狀態、Mark0狀態。爲了使得這幾種不同的狀態(不同值的指針),指向同一份對象,ZGC完全利用了操作系統的虛擬地址和物理地址轉換,使得這三種狀態的虛擬地址指針指向同一份物理地址,因此ZGC的Java堆需要在虛擬地址中佔用三份地址。ZGC通過內存文件來佔用實際的物理內存,然後將這個內存文件映射到Remapped、Mark0和Mark1指向的虛擬地址。可以看出,雖然表面上ZGC的Java Heap佔用了三份虛擬地址,但是實際的物理地址只有一份。這也是linux的命令top或者ps看到啓用ZGC的Java進程RSS內存膨脹三倍的原因,但開啓ZGC之後觀察到的RSS消耗並非實際物理內存消耗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/6b\/05\/6bdb1c8b75aa57045168367116c89905.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 ZGC算法的開銷"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZGC對於業務線程的影響主要集中在以下五個方面:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":1,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"Read barrier的開銷。在Java程序中,對象指針的讀取次數要遠超於對象指針的寫入次數,Read Barrier的插入點要遠多於Write Barrier的插入點,因此ZGC的Read Barrier會對程序的性能產生較大的負面影響。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"JIT方法的entry barrier開銷。如果JIT之後的代碼包含了已經死掉的java對象,那麼該方法就應該丟棄掉,因此JIT的代碼需要在進入時利用一個entry barrier來保證自身和其包含的meta信息的有效性。ZGC對每個JIT代碼都生成nmethod entry barrier,會對JIT方法產生輕微的性能損失。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"Frame barrier開銷。爲併發進行Java棧幀的掃描,降低Stack Roots掃描對STW時間的影響,當前Hotspot採用StackWaterMark來進行併發掃棧。同時爲了降低業務線程掃描棧幀的工作量,Hotspot中採用單個棧幀掃描的方式,即在回棧時如果超過當前stack water mark,就會陷入stack mark barrier,修復caller的java對象指針。參見https:\/\/openjdk.java.net\/jeps\/376"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"其他Runtime改造產生的鎖結構帶來的開銷。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"ZGC中大部分的GC工作放在併發階段,因此併發階段GC線程和Java業務線程搶佔CPU,導致的對業務線程的搶佔開銷。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出ZGC爲了降低STW造成的停頓影響,採取的措施是極致的併發化改造,也就是以輕微的性能損失換取最低的停頓影響。當前最新的ZGC實現停頓已經達到ms級別,低於Linux內核的背景噪聲,即調度開銷和系統調用開銷,也有可能造成10ms級別的影響,可以說ZGC使得Java不能服務實時業務的古板印象得到徹底的顛覆。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3. ZGC使用與調參"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 ZGC典型應用場景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此之蜜糖、彼之砒霜,不同的GC算法都有其長短處,ZGC出現的最大優勢是能夠在保證停頓時間控制10ms以下,但爲了實現這種高SLA的停頓時間,其代價是性能的損失和內存消耗。從前面介紹可以看出,爲了降低STW中的工作,很多GC任務做了併發化改造,而併發化改造的代價則散亂在各種運行細節中,通過整個OpenJDK社區的持續投入,當前ZGC在性能損失場景中的性能下降已經控制在很小的範圍內。對於性能來說,不同的配置對性能的影響是不同的,如充足的內存下即大堆場景,ZGC在各類Benchmark中能夠超過G1大約5%到20%,而在小堆情況下,則要低於G1大約10%;不同的配置對於應用的影響不盡相同,開發者需要根據使用場景來合理判斷。當前ZGC不支持壓縮指針和分代GC,其內存佔用相對於G1來說要稍大,在小堆情況下較爲明顯,而在大堆情況下,這些多佔用的內存則顯得不那麼突出。因此,以下兩類應用強烈建議使用ZGC來提升業務體驗:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"超大堆應用。超大堆(百G以上)下,CMS或者G1如果發生Full GC,停頓會在分鐘級別,可能會造成業務的終端,強烈推薦使用ZGC。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"高SLA需求的應用。如對響應時間有P999時限要求的實時和軟實時應用,此類應用無論堆大小,均推薦採用低停頓的ZGC。"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2 ZGC參數設置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZGC之美不僅在於其超低的STW停頓,也在於其參數的簡單,絕大部分生產場景都可以自適應。當然,極端情況下,還是有可能需要對ZGC個別參數做個調整,大致可以分爲三類:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"堆大小:Xmx。ZGC能夠通過極致的低延遲滿足業務高標準SLA的服務准入條件,但是與所有編程語言的concurrent GC類似,延遲是以內存空間作爲trade-off的。當分配速率過高,超過回收速率,造成堆內存不夠時,會觸發Allocation Stall,這類Stall會減緩當前的用戶線程。因此,當我們在GC日誌中看到Allocation Stall,通常可以認爲堆空間偏小或者concurrent gc threads數偏小。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GC觸發時機:ZAllocationSpikeTolerance, ZCollectionInterval。ZAllocationSpikeTolerance用來估算當前的堆內存分配速率,在當前剩餘的堆內存下,ZAllocationSpikeTolerance越大,估算的達到OOM的時間越快,ZGC就會更早地進行觸發GC。ZCollectionInterval用來指定GC發生的間隔,以秒爲單位觸發GC。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GC線程:ParallelGCThreads, ConcGCThreads。ParallelGCThreads是設置STW任務的GC線程數目,默認爲CPU個數的60%;ConcGCThreads是併發階段GC線程的數目,默認爲CPU個數的12.5%。增加GC線程數目,可以加快GC完成任務,減少各個階段的時間,但也會增加CPU的搶佔開銷,可根據生產情況調整。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由上可以看出ZGC需要調整的參數十分簡單,通常設置Xmx即可滿足業務的需求,大大減輕Java開發者的負擔。當前Tencent Kona JDK11上開啓ZGC的參數爲:“-XX:+UnlockExperimentalVMOptions -XX:+UseZGC”。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"4. ZGC生產注意事項"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.1 RSS內存異常現象"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由前面ZGC原理可知,ZGC採用多映射multi-mapping的方法實現了三份虛擬內存指向同一份物理內存。而Linux統計進程RSS內存佔用的算法是比較脆弱的,這種多映射的方式並沒有考慮完整,因此根據當前Linux採用大頁和小頁時,其統計的開啓ZGC的Java進程的內存表現是不同的。在內核使用小頁的Linux版本上,這種三映射的同一塊物理內存會被linux的RSS佔用算法統計3次,因此通常可以看到使用ZGC的Java進程的RSS內存膨脹了三倍左右,但是實際佔用只有統計數據的三分之一,會對運維或者其他業務造成一定的困擾。而在內核使用大頁的Linux版本上,這部分三映射的物理內存則會統計到hugetlbfs inode上,而不是當前Java進程上。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.2 共享內存調整"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZGC需要在share memory中建立一個內存文件來作爲實際物理內存佔用,因此當要使用的Java的堆大小大於\/dev\/shm的大小時,需要對\/dev\/shm的大小進行調整。通常來說,命令如下(下面是將\/dev\/shm調整爲64G):"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"vi \/etc\/fstab"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"tmpfs \/dev\/shm tmpfs defaults,size=65536M 0 0"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先修改fstab中shm配置的大小,size的值根據需求進行修改,然後再進行shm的mount和umount。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"umount \/dev\/shm"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"mount \/dev\/shm"}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"4.3 mmap節點上限調整"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ZGC的堆申請和傳統的GC有所不同,需要佔用的memory mapping數目更多,即每個ZPage需要mmap映射三次,這樣系統中僅Java Heap所佔用的mmap個數爲(Xmx \/ zpage_size) * 3,默認情況下zpage_size的大小爲2M。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了給JNI等native模塊中的mmap映射數目留出空間,內存映射的數目應該調整爲(Xmx \/ zpage_size) * 3 * 1.2。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"默認的系統memory mapping數目由文件 \/proc\/sys\/vm\/max_map_count 指定,通常數目爲65536,當給JVM配置一個很大的堆時,需要調整該文件的配置,使得其大於(Xmx \/ zpage_size) * 3 * 1.2。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"5. ZGC在騰訊的大規模生產實踐"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前Tencent Kona JDK11的ZGC已經在騰訊廣告大數據場景,騰訊雲VPC、WAF等業務場景上長期穩定運行,並協助業務取得了優異的性能表現。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.1 支持廣告海量數據查詢"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Hermes是騰訊自研的大數據實時分析系統,具有海量數據實時接入和存儲、低延遲查詢分析的特性,支持千級維度的多維分析,以及日增量萬億的海量日誌接入和查詢分析。在廣告業務實時OLAP分析業務中,要求Hermes系統上99%的SQL查詢端到端延遲不超過3s,而採用默認配置的G1 GC時僅98.1%的SQL查詢端到端延遲不超過3s。通過切換Kona 11 ZGC,SQL端到端延遲滿足率上升爲99.5%,同時單個SQL查詢中GC造成的延遲不超過20ms。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.2 超大堆支持"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"騰訊VPC團隊爲騰訊雲提供網絡控制服務,該服務主要存儲用於雲上資源通信的網絡配置等信息,並提供配置信息的查詢、修改和下發等服務。業務要求在512G內存的機器上儘可能多的存儲配置信息(最大支持800M的監聽數目),並且保證壓力場景下讀寫延遲不超過1s。採用G1GC則會高頻率出現大量的延遲超過10s,通過騰訊大數據JVM團隊配合切換ZGC,並解決ZGC在業務遇到的Mark Stack Overflow、進入Safepoint緩慢、ZGC Mark假死等問題,最終使得業務能夠在壓力場景下,將預期的業務存儲容量提升12.5%,同時讀寫延遲不超過50ms。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.3 助力提升SLA"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"騰訊WAF團隊採用Java來快速實現產品功能迭代及上線,其中,旁路安全服務是一個基於Netty框架的Http服務,此服務對時延要求很嚴格,需要達到99.99%端到端請求時延小於80ms的SLA目標。因此GC的STW對此服務有一定負面影響,需要進一步降低“世界暫停”時間。在使用ZGC之前,WAF團隊使用的是G1GC,前期花費了大量時間對G1 GC進行選項調試,並進行了代碼層面的修改。但由於G1GC本身的不足,仍然存在請求抖動延遲,無法達到既定的SLA目標。在騰訊大數據JVM團隊的配合下,切換ZGC之後,該業務的P9999請求延遲穩定小於80ms,爲用戶提供了更快速、穩定的服務。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"6.社區回饋"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"騰訊大數據JVM團隊在支持業務切換ZGC的同時,將遇到的相關問題和修復積極向社區報告和回饋,爭做OpenJDK社區好公民。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6.1 ZGC與VectorAPI聯合使用問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在廣告某業務中,上線VectorAPI以提升機器學習效率,同時打開ZGC以滿足服務SLA,在業務運行過程中出現結果非預期現象,並且社區存在類似的錯誤報告。通過對JIT生產的彙編代碼進行分析,發現存在load barrier缺失現象。經分析,在C2的Vector優化階段需要對Vector節點進行Unbox操作,該優化階段會新生成一個load節點,並且又未考慮GC Barrier對load操作的影響,即ZGC需要對load操作生成load barrier,從而導致這個新生成的load節點缺少load barrier信息,最終未能生成相關barrier指令。通過對Vector優化階段新生產的load操作增加GC barrier處理流程,使得該階段能夠生成帶gc barrier信息的load節點,從而在不同GC選項下均能生成對應正確的barrier代碼。該修復貢獻給社區後以P2優先級合入JDK16。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/0a\/52\/0a076b65e0a12800663e779e9bbyyb52.jpg","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6.2 ZGC Mark Stack Overflow問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"騰訊雲某業務對ZGC進行灰度時,出現JVM進程崩潰現象,相關日誌顯示是由於ZGC標記階段使用的Mark Stack超過預先設定的8G內存導致,而通常情況下Mark Stack的使用不會超過32M。經過業務全力配合,拿到一個可復現的場景進行深入分析,發現在這種場景下ZGC對於Mark Stack的使用存在兩個缺陷:第一,大量的Mark Stack未使用滿就塞入全局隊列,造成單個Stack內存碎片問題;第二,大量的對象被多次壓入Mark Stack中,造成Stack中的Entry重複率很高,浪費Stack空間。騰訊大數據JVM團隊作出快速修復,驗證Mark Stack Overflow問題可解後,將該問題和修復報告OpenJDK社區,社區基於提交的patch給出了更爲優雅的修復方案,並將騰訊大數據JVM團隊作爲co-author聯合提交代碼入庫,目前兩個問題的修復均已入庫JDK17。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/d8\/09\/d8efc6d05e7a03aaf5f272d888493b09.gif","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"6.3 ZGC Mark假死問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在分析ZGC在業務上的表現時,需要打開gc debug log選項,在啓動後不久,出現進程卡死現象。分析發現絕大部分gc worker線程處在“Concurrent Mark Try Terminate”階段,並且在等待“Concurrent Mark Idle”階段的log文件讀寫鎖,另外一個gc worker線程處於寫log過程中,由此可以分析出由於gc worker線程均在搶log文件鎖,導致gc worker線程最終形成一種動態死鎖狀態,即所有的gc worker線程均處於“等鎖->拿鎖->釋放鎖”這種無限循環中。這種假死現象是由於ZGC的Concurrent Mark退出機制導致的,在退出機制中所有的gc worker線程會等待1ms來進行狀態同步,而等待結束後會進行相關log打印,這個打印需要前述log文件鎖,從而導致動態假死現象出現。騰訊大數據JVM團隊快速修復該問題,並提交社區,目前該貢獻已合入JDK17中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.infoq.cn\/resource\/image\/71\/fc\/71dd191e5a5c5f25bde34c9e10e75ffc.gif","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"7. Tencent Kona JDK 開源"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"騰訊大數據JVM團隊的Tencent Kona JDK最新版本已經正式對外發布,大家可以使用Tencent Kona JDK 11.0.10-GA享受到ZGC帶來的好處。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Tencent Kona JDK 8.0.5-GA 同步更新 OpenJDK 8u282ga"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/github.com\/Tencent\/TencentKona-8","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/github.com\/Tencent\/TencentKona-8"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Tencent Kona JDK 11.0.10-GA 同步更新 OpenJDK 11.0.10-ga"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/github.com\/Tencent\/TencentKona-11","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/github.com\/Tencent\/TencentKona-11"}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"8. 參考文獻"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[1] The Design of ZGC. "},{"type":"link","attrs":{"href":"http:\/\/cr.openjdk.java.net\/~pliden\/slides\/ZGC-PLMeetup-2019.pdf","title":null,"type":null},"content":[{"type":"text","text":"http:\/\/cr.openjdk.java.net\/~pliden\/slides\/ZGC-PLMeetup-2019.pdf"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[2] How JavaScript works: memory management + how to handle 4 common memory leaks. "},{"type":"link","attrs":{"href":"https:\/\/blog.sessionstack.com\/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/blog.sessionstack.com\/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章