深入理解JVM垃圾回收機制 - GC Roots枚舉

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JVM的垃圾回收算法,從如何判定對象消亡的角度可以分爲兩類:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"引用計數式垃圾回收 ( Reference Counting GC )"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"追蹤式垃圾回收 ( Tracing 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":"這兩類也常被稱爲“直接垃圾回收”和“間接垃圾回收”。目前,所有主流JVM所採用的垃圾回收算法均屬於 Tracing GC 範疇。Tracing GC 的基本的思路是給定一個集合的引用作爲根節點,然後從根節點出發,通過引用關係向下搜索,能被遍歷到的 (可到達的) 對象就被判定爲存活,其餘對象 (也就是沒有被遍歷到的) 自然被判定爲死亡,這組引用的集合就被稱爲 GC Roots。"}]},{"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":"想要實現語義正確的 Tracing GC,有一個重要的前提就是要能夠完整枚舉出所有的 GC Roots,否則就可能會漏掉本應存活的對象,導致GC錯誤的回收這些被漏掃的活對象。這裏請注意,Tracing GC的本質是"},{"type":"text","marks":[{"type":"strong"}],"text":"找出活的對象來把其餘空間判定爲“無用”,而不是找出所有死掉的對象並回收它們佔用的空間"},{"type":"text","text":"。所以,漏掃會導致錯誤的回收本應活着的對象,而不是少回收死亡的對象。這部分內容更詳細的講解可以參考:"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/d5dad506a377ed4bc91fcf84a","title":""},"content":[{"type":"text","text":"深入理解JVM垃圾回收機制 - 何爲垃圾?"}]},{"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 ?JVM 是如何找到 GC Roots 的?"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"GC Roots 包含哪些引用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GC Roots 作爲 Tracing GC 的起點,其必須是一組活躍的引用。固定可作爲 GC Roots 的對象主要在全局的引用 ( 例如常量或類靜態屬性 ) 與執行上下文 ( 例如棧楨中的本地變量表 ) 中。簡單來說就是,GC Roots 中包含了所有無須跟蹤引用就可以得到的對象。"}]},{"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":"關於這點應該比較好理解,大多數情況下,都是在類中定義常量與靜態變量,在方法中定義局部變量,這些都是堆中對象的起點。那麼在垃圾回收時,這些引用自然而然就成爲了 Tracing 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 Roots 的引用大致包含:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當前活躍線程的棧楨裏指向堆中對象的引用,即當前所有正在被調用方法的引用類型參數、局部變量等。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類的引用類型靜態變量,這裏指的是引用類型,像 int 等基本數據類型的靜態變量肯定不能作爲 GC Roots。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當前所有已被加載的Java類和類加載器。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JNI 句柄,包括 JNI Local Handles 和 JNI Global Handles。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在方法區中常量引用的對象,譬如字符串常量池 ( String Table ) 裏的引用。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有被同步鎖 ( synchronized關鍵字 ) 持有的對象引用。"}]}]},{"type":"listitem","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 的具體分類,不同的語言以及不同的垃圾回收算法都有些許差異,比如 .NET Framework 中有"},{"type":"codeinline","content":[{"type":"text","text":"Stack reference"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"Static reference"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"Finalizer reference"}]},{"type":"text","text":"、"},{"type":"codeinline","content":[{"type":"text","text":"Handles"}]},{"type":"text","text":" 等類別;再比如,IBM的內存分析工具,對 GC Roots 的分類就更詳盡一些:"},{"type":"link","attrs":{"href":"https://www.ibm.com/support/knowledgecenter/en/SS3KLZ/com.ibm.java.diagnostics.memory.analyzer.doc/gcroots.html","title":""},"content":[{"type":"text","text":"IBM Monitoring And Diagnostic Tools - GC Roots"}]},{"type":"text","text":"。正是由於這些差異,在網上搜索的各種資料可能存在相互衝突的 GC Roots 分類,這都是挺正常的。"}]},{"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,可以藉助一些工具來查看,比如常用的 MAT 和 VisualVM 都提供 GC Roots 的查找功能。"}]},{"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":"在 VisualVM 選擇要監控的進程後,點擊 "},{"type":"codeinline","content":[{"type":"text","text":"Monitor"}]},{"type":"text","text":" -> "},{"type":"codeinline","content":[{"type":"text","text":"Heap Dump"}]},{"type":"text","text":" 後在 "},{"type":"codeinline","content":[{"type":"text","text":"Heap"}]},{"type":"text","text":" 選項卡中可以看到對象概要統計 ( 如下圖所示 ),從圖中可以看到 GC Roots 的總數量是1902,然後可以切換查看具體的對象、線程信息等。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8f/8f6b5b2d0fb1e49229db6ae3b0f660c9.jpeg","alt":"VisualVM Heap Dump","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"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":"切換到 "},{"type":"codeinline","content":[{"type":"text","text":"Objects"}]},{"type":"text","text":" 後,可以查看對應類型的對象信息及其 GC Roots 分類信息,如下圖所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7e/7e088d9956993fdbf67d7081dd221d31.jpeg","alt":"VisualVM Heap Dump Objects","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"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":"除了使用 VisualVM,也可以使用MAT ( Eclipse Memory Analyzer Tool ),具體的使用方法就不贅述了,其大致的界面如下圖所示。"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e4/e4404249cb395946c85c002d0d04f79b.jpeg","alt":"Eclipse Memory Analyzer Tool","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"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 集合外,根據用戶所使用的垃圾收集器以及當前回收的內存區域,還可以有其他對象引用臨時性地加入,共同構成完整的 GC Roots 集合。比如 JVM 的分代回收中,在某個區域中已被判定死亡的對象,可能還被其他區域的對象引用 ( 比如,新生代中的某個對象,可能被老年代的對象引用 ),這時候就需要將這些關聯區域的對象的引用也一併加入 GC Roots 集合中去,才能保證可達性分析的正確性。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"如何查找 GC Roots"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當前,所有的垃圾收集器在 GC Roots 枚舉時都必須暫停用戶線程,這是因爲整個枚舉的分析過程必須在一個能保證一致性的快照中進行。這裏的"},{"type":"codeinline","content":[{"type":"text","text":"一致性"}]},{"type":"text","text":"是指整個枚舉期間,系統看起來就像被凍結在某個時間點上,不會出現 GC Roots 的對象引用關係還在不斷變化的情況,否則,分析結果的準確性也就無法保證。"}]},{"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?"}]},{"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 棧 ) 開始掃描內存,掃描的時候每看到一個數字就看看它是否是一個指向GC堆中的指針( 引用 )。某些保守式垃圾回收器會把所有看起來像是對象引用的數據都當成引用來處理,比如,像1或100這樣值可以簡單地認爲是整型數據,但那些看起來像是指針地址的值就必須要檢查,查看能否在堆中對應位置找到內容,並對內容進行檢查。這種方式存在較大的性能損耗,且還可能存在意外持有垃圾對象和對象移動的問題 ( 比如,在垃圾回收過程中對內存進行整理,那麼就必須更新持有這些對象的應用,將其指向新的位置 ) 。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"OopMap"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"HotSpot 在設計之初就使用準確式垃圾回收,它能夠判斷出所有位置上的數據是不是指向GC堆裏的引用,比如內存中有一個 32bit 的整數123456,HotSpot是可以分辨出它究竟是一個整數還是一個對象的內存地址。至於虛擬機是如何精確的找到棧幀中的引用,可以閱讀參考資料3;如果想更深入的理解這部分的實現原理,比如:如何在對象中精確查找指針,如何在棧和寄存器中精確查找指針等,可以閱讀 "},{"type":"link","attrs":{"href":"https://union-click.jd.com/jdc?e=&p=AyIGZRprFQIbDlMfUhIyVlgNRQQlW1dCFFlQCxxKQgFHREkdSVJKSQVJHFRXFk9FUlpGQUpLCVBaTFhbXQtWVmpSWRtbHAsUA1wca1xKR38obj9zYEd1XFwFcHR1cx5sAGUOHjdUK1sUAxMGVRxaFwIiN1Uca0NsEgZUGloUBxMDVitaJQIWD10ZWxEKEg9VG1wlBRIOZUYfQVBQVWUraxYyIjdVK1glQHwDVhkLFgsUDwEdXRYHE1BTEggSBUUEARlZ","title":""},"content":[{"type":"text","text":"垃圾回收算法手冊"}]},{"type":"text","text":" 的第11章第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":"如今單個Java應用管理的堆內存至少都是GB起步,裏面的類、常量、變量等數不勝數。即使 JVM 可以準確的準確地判斷某個內存中某個位置的數據具體是什麼類型,在GC時,也不可能真的去掃描所有的 JVM 棧、方法區、寄存器等空間,不然耗時就太長了。"}]},{"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 解決辦法也很直接,用空間換時間,即使用額外的數據結構從外部記錄下棧和寄存器中哪些位置是引用,這個數據結構被稱爲 OopMap。"}]},{"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":"在之前已經介紹過,棧幀 ( Stack Frame ) 中局部變量表用於存放方法參數和局部變量。它的容量是以變量槽爲最小單位,更具體的內容請移步:"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/951fa18f83ba9eaafe2e02997","title":""},"content":[{"type":"text","text":"運行時棧幀的內存變化"}]},{"type":"text","text":",比如:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// javac -v 得到編譯後的字節碼\nLocalVariableTable:\n Start Length Slot Name Signature\n 0 10 0 bar Ljava/lang/String;\n 9 1 1 baz Ljava/lang/Integer;"}]},{"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 可以直接使用的。因此,HotSpot 在類的加載過程中,就可以利用這些信息把對象內什麼偏移量是什麼類型的數據計算出來,存放到 OopMap 中。"}]},{"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":"除此之外,在即時編譯過程中,也會在特定的位置記錄下棧裏和寄存器裏哪些位置是引用。"}]},{"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":"這是因爲經過 JIT ( just in time,即時編譯器 ) 編譯後的代碼,其引用的位置可能會發生變化,比如原來需要從主存中讀取數據,經過 JIT 優化後可以直接從寄存器中讀取數據,這時候,就需要把這樣的變化同步到 OopMap 中去。"}]},{"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 枚舉發生的一個非常重要的區域。更多關於 JIT 相關的內容可以閱讀參考資料4,"}]},{"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":"可以在啓動時增加VM參數 "},{"type":"codeinline","content":[{"type":"text","text":"-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly"}]},{"type":"text","text":" 來查看方法編譯後的本地代碼,以此來了解在哪些地方會生存 OopMap,以及裏面的具體內容。比如,下面這段代碼是"},{"type":"codeinline","content":[{"type":"text","text":"String.hashCode()"}]},{"type":"text","text":" 方法編譯後的本地代碼,摘自 "},{"type":"link","attrs":{"href":"https://union-click.jd.com/jdc?e=&p=AyIGZRprFQEVBlMYUx0yVlgNRQQlW1dCFFlQCxxKQgFHREkdSVJKSQVJHFRXFk9FUlpGQUpLCVBaTFhbXQtWVmpSWRtYEgMUBF0Ta1YBR3oCGitvYVkAMGwAb1gMeT5PIGUOHjdUK1sUAxMGVRxaFwIiN1Uca0NsEgZUGloUBxMDVitaJQIWDlcZWhMDGgNTGVslBRIOZUYfQVBQVWUraxYyIjdVK1glQHxTAE8JQgAXV1NIDxUHEQ4GS1lCCxBV","title":""},"content":[{"type":"text","text":"深入理解Java虛擬機"}]},{"type":"text","text":" 第三章4小節。"}]},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"[Verified Entry Point]\n0x026eb730: mov %eax,-0x8000(%esp)\n…………\n;; ImplicitNullCheckStub slow case\n0x026eb7a9: call 0x026e83e0 ; OopMap{ebx=Oop [16]=Oop off=142}\n ; *caload\n ; - java.lang.String::hashCode@48 (line 1489)\n ; {runtime_call}\n 0x026eb7ae: push $0x83c5c18 ; {external_word}\n 0x026eb7b3: call 0x026eb7b8\n 0x026eb7b8: pusha\n 0x026eb7b9: call 0x0822bec0 ; {runtime_call}\n 0x026eb7be: hlt"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OopMap輸出的大致格式是:"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"OopMap{零到多個“數據位置=內容類型”的記錄 off=該OopMap關聯的指令的位置} "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個例子中:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"EBX寄存器有一個普通對象指針 (OOP) 的引用"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"[16] 表示棧頂指針 + 偏移量16 的位置,也有一個普通對象指針的引用"}]}]}]},{"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":"而 "},{"type":"codeinline","content":[{"type":"text","text":"off"}]},{"type":"text","text":" 表示這個 OopMap 記錄關聯的指令在方法的指令流的偏移量,這裏表示這個 OopMap 與偏移量爲142的位置上的指令關聯在一起。"}]},{"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":"我們也可以通過 OopMap 的源碼註釋來了解其大致的作用:爲編譯後的代碼生成 "},{"type":"codeinline","content":[{"type":"text","text":"frame map"}]},{"type":"text","text":",而 frame map 用於描述寄存器和棧幀 slot 中存放數據的數據類型,比如可以是 Oop ( 普通對象指針 ),這裏的註釋其實更直白一些,直接說是當前棧幀的 GC Root;還可以是 Value,即非oop,非浮點數的 int類型的數據;還可以是 Dead,一些用於調試的數據……"}]},{"type":"codeblock","attrs":{"lang":"cpp"},"content":[{"type":"text","text":"// Interface for generating the frame map for compiled code. A frame map\n// describes for a specific pc whether each register and frame stack slot is:\n// Oop - A GC root for current frame\n// Value - Live non-oop, non-float value: int, either half of double\n// Dead - Dead; can be Zapped for debugging\n// CalleeXX - Callee saved; also describes which caller register is saved\n// DerivedXX - A derived oop; original oop is described.\n//\n// OopMapValue describes a single OopMap entry\n\nclass frame;\nclass RegisterMap;\nclass DerivedPointerEntry;\n\nclass OopMapValue: public StackObj {\n}"}]},{"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 利用 OopMap 快速準確地完成 GC Roots 枚舉,從而避免掃描整個棧空間和寄存器。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"安全點和安全區域"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"前面都在說一個問題,那就是得到一致性的快照後,如何枚舉GC Roots?而第二個問題就是 "},{"type":"text","marks":[{"type":"strong"}],"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":"HotSpot 採用 JIT compile 技術來提高性能,大量的指令會導致引用關係發生變化,如果爲每條指令都生成對應的 OopMap,就需要很大的額外空間來存儲。這在理論上可能導致空間成本高昂而無法接受,但實際上,這個成本也沒有那麼離譜,因爲確實有語言是這麼幹的。況且,OopMap都是壓縮了存在內存裏的,在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":"但不管如何,HotSpot 並沒有採用這種方式,而是在特定的位置來記錄 OopMap,這些位置即被稱爲 "},{"type":"text","marks":[{"type":"strong"}],"text":"安全點(Safepoint)"},{"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","marks":[{"type":"strong"}],"text":"達到安全點後,就可以得到一份一致性的快照"},{"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":"至於安全點的選擇標準,大家可以閱讀 "},{"type":"link","attrs":{"href":"https://union-click.jd.com/jdc?e=&p=AyIGZRprFQEVBlMYUx0yVlgNRQQlW1dCFFlQCxxKQgFHREkdSVJKSQVJHFRXFk9FUlpGQUpLCVBaTFhbXQtWVmpSWRtYEgMUBF0Ta1YBR3oCGitvYVkAMGwAb1gMeT5PIGUOHjdUK1sUAxMGVRxaFwIiN1Uca0NsEgZUGloUBxMDVitaJQIWDlcZWhMDGgNTGVslBRIOZUYfQVBQVWUraxYyIjdVK1glQHxTAE8JQgAXV1NIDxUHEQ4GS1lCCxBV","title":""},"content":[{"type":"text","text":"深入理解Java虛擬機"}]},{"type":"text","text":" 第三章3.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":"關於安全點,另外一個需要考慮的問題就是,如何在垃圾收集發生時讓所有線程 ( 不包含執行JNI調用線程 ) 都跑到安全點,然後停頓下來。其有兩種實現方式:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"搶斷式中斷:GC 發生時,先中斷所有線程,若線程未達安全點,則恢復線程讓其繼續執行直到達到安全點。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"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":"Hotspot 採用第二種方式。但這有個前提是,線程一直在運行,如果用戶線程處於 Sleep 或者 Blocked 狀態,它是沒有辦法響應虛擬機的中斷請求的,很長一段時間都不會走到安全點。這種情況下,虛擬機也不能說一直等到線程重新被激活後再進行垃圾回收,就引入了 "},{"type":"text","marks":[{"type":"strong"}],"text":"安全區域(Safe Region)"},{"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":"處於安全區域,對象的引用關係不會發生變化,就比如 Sleep 的線程。在這個區域中任意地方開始垃圾回收都是安全的。"}]},{"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 在垃圾收集時就無須關注這些線程。當線程離開安全區域時,它要檢查 JVM 是否已經完成 GC Roots 的枚舉,如果沒有完成,它需要一直等待,直到收到可以離開安全區域的信號爲止。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"JNI 引用的垃圾回收機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"你應該特別熟悉 Java 中被標記爲 native 的方法,當在 Java 代碼中調用 native 方法時, JVM 將通過 JNI 調用對應的 C/C++ 函數。同樣地,JNI 也提供對應的機制在 C/C++ 代碼中,使用 Java 的語言特性。比如,可以在 C 語言中創建一個 Java 對象。顯然,這些對象會受到垃圾回收器的影響,但 JVM 又不知道 C 語言是如何使用這些對象的,回不回收這些對象,都有問題。因此,JVM 需要一種機制,來告訴垃圾回收器,不要回收這些對象,因爲它還可能正在被使用。"}]},{"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 的局部引用和全局引用,JVM 會將被這兩種引用指向的對象標記爲不可回收。"}]},{"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 函數中傳入的引用類型參數和返回的引用類型對象都屬於局部引用:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"// Java native 方法\npublic native Object bar(String s, Object o);\n// JNI 會將 Java 層面的基本類型以及引用類型映射爲另一套可供 C 代碼使用的數據結構\n// 比如Java 中的 long 映射爲 C 中的 jlong\n// 這裏爲了方便觀察,對 JNI 函數名作了刪減\nJNIEXPORT jobject JNICALL Java_org_example_Foo_bar(JNIEnv *, jobject, jstring, jobject);"}]},{"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":"一旦從 C 函數中返回至 Java 方法中,那麼局部引用將會失效,JVM 在整個 Tracing 過程中就不再考慮這些局部引用,也就是說,一段時間後,局部引用佔用的內存將會被回收。"}]},{"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":"如果想讓某些局部引用在從 C 函數返回後不被 JVM 回收,則可以藉助 JNI 函數 "},{"type":"codeinline","content":[{"type":"text","text":"NewGlobalRef"}]},{"type":"text","text":",將該局部引用轉換爲全局引用。被全局引用的對象,不會被 JVM 回收,只能通過 JNI 函數 "},{"type":"codeinline","content":[{"type":"text","text":"DeleteGlobalRef"}]},{"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":"同樣地,如果 C 函數運行時間很長,導致大量無用的局部引用無法被回收,這時候,可以通過 JNI 函數 "},{"type":"codeinline","content":[{"type":"text","text":"DeleteLocalRef"}]},{"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 可能會移動對象在內存中的位置,JVM 需要另外一種機制來保證局部引用和全局引用能夠正確的指向移動過後的對象。"}]},{"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 虛擬機是通過句柄來完成上述需求的。這裏句柄指的是內存中 Java 對象的指針的指針。當發生垃圾回收時,如果 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":"事實上,所有經過 JNI 調用邊界(調用 JNI 函數傳入的參數、從 JNI 函數傳回的返回值)的引用都必須用句柄包裝起來,也就是說,無論是局部引用還是全局引用,都是句柄。因此在 GC 時,並不需要掃描 JNI 函數的棧幀,而只需要掃描句柄表就可以得到所有從 JNI 函數能訪問到的 GC 堆裏的對象。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"最後"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"關於 GC Roots 枚舉的資料挺少的,大多數資料對這部分內容都是幾句話帶過,而我也不怎麼會看 JVM 源碼,所以請謹慎閱讀本文的內容,特別是關於 OopMap 的部分觀點。JNI 這部分內容在個人工作中鮮有接觸,也不是很熟悉,所以這小節的大部分內容參考鄭雨迪在極客時間的專欄:"},{"type":"link","attrs":{"href":"http://gk.link/a/10l7h","title":""},"content":[{"type":"text","text":"深入拆解 Java 虛擬機 - 第32講"}]},{"type":"text","text":",如果感興趣,可自行訂閱查看。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"深入理解JVM系列的第10篇,完整目錄請移步:"},{"type":"link","attrs":{"href":"https://xie.infoq.cn/article/f2bac733dcd43c28ef5384322","title":""},"content":[{"type":"text","text":"深入理解JVM系列文章目錄"}]}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"參考資料"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.zhihu.com/question/53613423/answer/135743258","title":""},"content":[{"type":"text","text":"Java GC爲什麼要分代?"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://xiao-feng.blogspot.com/2008/01/gc-safe-point-and-safe-region.html","title":""},"content":[{"type":"text","text":"GC safe-point (or safepoint) and safe-region"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.iteye.com/blog/rednaxelafx-1044951","title":""},"content":[{"type":"text","text":"找出棧上的指針/引用"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://developer.ibm.com/zh/technologies/java/articles/j-lo-just-in-time/","title":""},"content":[{"type":"text","text":"深入淺出 JIT 編譯器"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"http://gk.link/a/10l7h","title":""},"content":[{"type":"text","text":"深入拆解 Java 虛擬機 - 第32講"}]}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://union-click.jd.com/jdc?e=&p=AyIGZRprFQEVBlMYUx0yVlgNRQQlW1dCFFlQCxxKQgFHREkdSVJKSQVJHFRXFk9FUlpGQUpLCVBaTFhbXQtWVmpSWRtYEgMUBF0Ta1YBR3oCGitvYVkAMGwAb1gMeT5PIGUOHjdUK1sUAxMGVRxaFwIiN1Uca0NsEgZUGloUBxMDVitaJQIWDlcZWhMDGgNTGVslBRIOZUYfQVBQVWUraxYyIjdVK1glQHxTAE8JQgAXV1NIDxUHEQ4GS1lCCxBV","title":""},"content":[{"type":"text","text":"深入理解Java虛擬機 - 第3章4小節"}]}]}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章