最近因爲系統業務量比較大,從生產的GC日誌(結合Pinpoint)來看,須要對部分系統進行GC調優。可是鑑於以往不是專門作這一塊,可是一直都有零散的積累,這裏作一個相對全面的總結。本文只針對HotSpot VM
也就是Oracle Hotspot VM
或者OpenJDK Hotspot VM
,版本爲Java8,其餘VM不必定適用。java
Garbage Collection
能夠翻譯爲「垃圾收集」 -- 通常主觀上會認爲作法是:找到垃圾,而後把垃圾扔掉。在VM中,GC的實現過程偏偏相反,GC的目的是爲了追蹤全部正在使用的對象,而且將剩餘的對象標記爲垃圾,隨後標記爲垃圾的對象會被清除,回收這些垃圾對象佔據的內存,從而實現內存的自動管理。算法
名稱 | 具體內容 |
---|---|
弱分代假說(Weak Generational Hypothesis) | 大多數對象在年輕時死亡 |
強分代假說(Strong Generational Hypothesis) | 越老的對象越不容易死亡 |
弱分代假說已經在各類不一樣類型的編程範式或者編程語言中獲得證明,而強分代假說目前提供的證據並不充足,觀點還存在爭論。編程
分代垃圾回收器的主要設計目的是減小回收過程的停頓時間,同時提高空間吞吐量。若是採用複製算法對年輕代對象進行回收,那麼指望的停頓時間很大程度取決於次級回收(Minor Collection
)以後存活的對象總量,而這一數值又取決於年輕代的總體空間。數組
若是年輕代的總體空間過小,雖然一次回收的過程比較快,可是因爲兩次回收之間的間隔過短,年輕代對象有可能沒有足夠的時間「到達死亡」,於是致使回收的內存很少,有可能引起下面的狀況:緩存
mutation
)會給賦值器的寫屏障帶來比較大的壓力。分代垃圾回收器的設計師對上面幾個方面進行平衡的一門藝術:數據結構
Major Collection
)。基於弱分代假說,JVM中把堆內存分爲年輕代(Young Generation)和老年代(Old Generation),而老年代有些時候也稱爲Tenured。多線程
JVM對不一樣分代提供了不一樣的垃圾回收算法。實際上,不一樣分代之間的對象有可能相互引用,這些被引用的對象在分代垃圾回收的時候也會被視爲GC Roots
(見下一節分析)。弱分代假說有可能在特定場景中對某些應用是不適用的;而GC算法針對年輕代或者老年代的對象進行了優化,對於具有「中等」預期壽命的對象,JVM的垃圾回收表現是相對劣勢的。併發
JVM中是經過可達性算法(Reachability Analysis
)來斷定對象是否存活的。這個算法的基本思路就是:經過一些列的稱爲GC Roots(GC根集合)
的活躍引用爲起始點,從這些集合節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain
),當一個對象到GC Roots
沒有任何引用鏈相連時,說明該對象是不可達的。jvm
GC Roots
具體是指什麼?這一點能夠從HotSpot VM
的Parallel Scavenge
源碼實現總結出來,參考jdk9分支的psTasks.hpp
和psTasks.cpp
:編程語言
// psTasks.hpp class ScavengeRootsTask : public GCTask { public: enum RootType { universe = 1, jni_handles = 2, threads = 3, object_synchronizer = 4, flat_profiler = 5, system_dictionary = 6, class_loader_data = 7, management = 8, jvmti = 9, code_cache = 10 }; private: RootType _root_type; public: ScavengeRootsTask(RootType value) : _root_type(value) {} char* name() { return (char *)"scavenge-roots-task"; } virtual void do_it(GCTaskManager* manager, uint which); }; // psTasks.cpp void ScavengeRootsTask::do_it(GCTaskManager* manager, uint which) { assert(ParallelScavengeHeap::heap()->is_gc_active(), "called outside gc"); PSPromotionManager* pm = PSPromotionManager::gc_thread_promotion_manager(which); PSScavengeRootsClosure roots_closure(pm); PSPromoteRootsClosure roots_to_old_closure(pm); switch (_root_type) { case universe: Universe::oops_do(&roots_closure); break; case jni_handles: JNIHandles::oops_do(&roots_closure); break; case threads: { ResourceMark rm; Threads::oops_do(&roots_closure, NULL); } break; case object_synchronizer: ObjectSynchronizer::oops_do(&roots_closure); break; case flat_profiler: FlatProfiler::oops_do(&roots_closure); break; case system_dictionary: SystemDictionary::oops_do(&roots_closure); break; case class_loader_data: { PSScavengeKlassClosure klass_closure(pm); ClassLoaderDataGraph::oops_do(&roots_closure, &klass_closure, false); } break; case management: Management::oops_do(&roots_closure); break; case jvmti: JvmtiExport::oops_do(&roots_closure); break; case code_cache: { MarkingCodeBlobClosure each_scavengable_code_blob(&roots_to_old_closure, CodeBlobToOopClosure::FixRelocations); CodeCache::scavenge_root_nmethods_do(&each_scavengable_code_blob); AOTLoader::oops_do(&roots_closure); } break; default: fatal("Unknown root type"); } // Do the real work pm->drain_stacks(false); }
因爲HotSpot VM
的源碼裏面註釋比較少,因此只能參考一些資料和源碼方法的具體實現猜想GC Roots
的具體組成:
Universe::oops_do
:VM的一些靜態數據結構裏指向GC堆裏的對象的活躍引用等等。JNIHandles::oops_do
:全部的JNI handle,包括全部的global handle和local handle。Threads::oops_do
:全部線程的虛擬機棧,具體應該是全部Java線程當前活躍的棧幀裏指向GC堆裏的對象的引用,或者換句話說,當前全部正在被調用的方法的引用類型的參數/局部變量/臨時值。ObjectSynchronizer::oops_do
:全部被對象同步器關聯的對象,看源碼應該是ObjectMonitor
中處於Block狀態的對象,從Java代碼層面應該是經過synchronized
關鍵字加鎖或者等待加鎖的對象。FlatProfiler::oops_do
:全部線程的中的ThreadProfiler
。SystemDictionary::oops_do
:System Dictionary
,也就是系統字典,是記錄了指向Klass
,KEY是一個Entry,由KalssName
和Classloader
組成,實際上,YGC不會處理System Dictionary
,可是會掃描System Dictionary
,某些GC可能觸發類卸載功能,能夠這樣理解:System Dictionary
包含了全部的類加載器。ClassLoaderDataGraph::oops_do
:全部已加載的類或者已加載的系統類。Management::oops_do
:MBean
所持有的對象。JvmtiExport::oops_do
:JVMTI
導出的對象,斷點或者對象分配事件收集器相關的對象。CodeCache::scavenge_root_nmethods_do
:代碼緩存(Code Cache)。AOTLoader::oops_do
:AOT加載器相關,包括了AOT相關代碼緩存。還有其餘有可能的引用:
StringTable::oops_do
:全部駐留的字符串(StringTable
中的)。
JVM把內存池劃分爲多個區域,下面分別介紹每一個區域的組成和基本功能,方便下面介紹GC算法的時候去理解垃圾收集如何在不一樣的內存池空間中發揮其職責。
伊甸園是地上的樂園,根據《聖經·舊約·創世紀》記載,神·耶和華照本身的形像造了人類的祖先男人亞當,再用亞當的一個肋骨創造了女人夏娃,並安置第一對男女住在伊甸園中。
Eden,也就是伊甸園,是一塊普通的在建立對象的時候進行對象分配的內存區域。而Eden進一步劃分爲駐留在Eden空間中的一個或者多個Thread Local Allocation Buffer(線程本地分配緩衝區,簡稱TLAB),TLAB是線程獨佔的。JVM容許線程在建立大多數對象的時候直接在相應的TLAB中進行分配,這樣能夠避免多線程之間進行同步帶來的性能開銷。
當沒法在TLAB中進行對象分配的時候(通常是緩衝區沒有足夠的空間),那麼對象分配操做將會在Eden中共享的空間(Common Area)中進行。若是整個Eden都沒有足夠的空間,則會觸發YGC(Young Generation Garbage Collection),以釋放更多的Eden中的空間。觸發YGC後依然沒有足夠的內存,那麼對象就會在老年代中分配(通常這種狀況稱爲分配擔保(Handle Promotion),是有前置條件的)。
當垃圾回收器收集Eden的時候,會遍歷全部相對於GC Roots
可達的對象,而且標記它們是活對象,這一階段稱爲標記階段。
這裏還有一點須要注意的是:堆中的對象有可能跨代連接,也就是有可能年輕代中的對象被老年代中的對象持有(注:老年代中的對象被年輕代中的對象持有這種狀況在YGC中不須要考慮),這個時候若是不遍歷老年代的對象,那麼就沒法經過可達性算法分析這種被被老年代中的對象持有的年輕代對象是否可達。JVM中採用了Card Marking(卡片標記)的方式解決了這個問題,這裏不對卡片標記的細節實現進行展開。
標記階段完成後,Eden中全部存活的對象會被複制到倖存者空間(Survivor Spaces) 的其中一塊空間。複製階段完成後,整個Eden被認爲是空的,能夠從新用於分配更多其餘的對象。這裏採用的GC算法稱爲標記-複製(Mark and Copy) 算法:標記存活的對象,而後複製它們到倖存者空間(Survivor Spaces) 的其中一塊空間,注意這裏是複製,不是移動。
關於Eden就介紹這麼多,其中TLAB和Card Marking是JVM中的相對底層實現,大概知道便可。
Survivor Spaces也就是倖存者空間,倖存者空間最經常使用的名稱是from和to。最重要的一點是:倖存者空間中的兩個區域總有一個區域是空的。
下一次YGC觸發以後,空閒的那一塊倖存者空間纔會入駐對象。年輕代的全部存活的對象(包括Eden和非空的from倖存者區域中的存活對象),都會被複制到to倖存者區域,這個過程完成以後,to倖存者區域會存放着活躍的對象,而from倖存者區域會被清空。接下來,from倖存者區域和to倖存者區域的角色會交換,也就是下一輪YGC觸發以後存活的對象會複製到from倖存者區域,而to倖存者區域會被清空,如此循環往復。
上面提到的存活對象的複製過程在兩個倖存者空間之間屢次往復以後,某些存活的對象「年齡足夠大」(通過屢次複製還存活下來),則這些「年紀大的」對象就會晉升到老年代中,這些對象會從倖存者空間移動到老年代空間中,而後它們就駐留在老年代中,直到自身變爲不可達。
若是對象在Eden中出生而且通過了第一次YGC以後依然存活,而且可以被Survivor Spaces容納的話,對象將會被複制到Survivor Spaces而且對象年齡被設定爲1。對象在Survivor Spaces中每經歷一次YGC以後還能存活下來,則對象年齡就會增長1,當它的年齡增長到晉升老年代的年齡閾值,那麼它就會晉升到老年代也就是被移動到老年代中。晉升老年代的年齡閾值的JVM參數是-XX:MaxTenuringThreshold=n
:
VM參數 | 功能 | 可選值 | 默認值 |
---|---|---|---|
-XX:MaxTenuringThreshold=n |
Survivor Spaces存活對象晉升老年代的年齡閾值 | 1<= n <= 15 | 15 |
值得注意的是:JVM中設置-XX:MaxTenuringThreshold
的默認值爲最大可選值,也就是15。
JVM還具有動態對象年齡判斷的功能,JVM並非永遠地要求存活對象的年齡必須達到MaxTenuringThreshold
才能晉升到老年代,若是在Survivor Spaces中相同年齡的全部對象的大小總和大於Survivor Spaces的一半,那麼年齡大於或者等於該年齡的對象能夠直接晉升到老年代,不須要等待對象年齡到達MaxTenuringThreshold
,例如:
類型 | 佔比 | 年齡 | 動做(MaxTenuringThreshold=15 ) |
---|---|---|---|
ObjectType-1 |
60% | 5 | 下一次YGC若是存活直接晉升到老年代 |
ObjectType-2 |
1% | 6 | 下一次YGC若是存活直接晉升到老年代 |
ObjectType-3 |
10% | 4 | 下一次YGC若是存活對象年齡增長1 |
能夠簡單總結一下對象進入老年代的幾種狀況:
-XX:MaxTenuringThreshold=n
致使對象晉升。老年代(Old Generation)更多時候被稱爲Tenured,它的內存空間的實現通常會更加複雜。老年代空間通常要比年輕大大得多,它裏面承載的對象通常不會是「內存垃圾」,側面也說明老年代中的對象的回收率通常比較低。
老年代發生GC的頻率通常狀況下會比年輕代低,而且老年代中的大多數對象都被指望爲存活的對象(也就是對象經歷GC以後存活率比較高),所以標記和複製算法並不適用於老年代。老年代的GC算法通常是移動對象以最小化內存碎片。老年代的GC算法通常規則以下:
GC Roots
遍歷和標記全部可達的對象。GC Roots
不可達的對象。在Java8以前JVM內存池中還定義了一塊空間叫永久代(Permanent Generation),這塊內存空間主要用於存放元數據例如Class
信息等等,它還存放其餘數據內容,例如駐留的字符串(字符串常量池)。實際上永久代曾經給Java開發者帶來了不少麻煩,由於大多數狀況下很難預測永久代須要設定多大的空間,由於開發者也很難預測元數據或者字符串常量池的具體大小,一旦分配的元數據等內容出現了失敗就會遇到java.lang.OutOfMemoryError: Permgen space
異常。排除內存溢出致使的java.lang.OutOfMemoryError
異常,若是是正常狀況下致使的異常,惟一的解決手段就是經過VM參數-XX:MaxPermSize=XXXXm
增大永久代的內存,不過這樣也是治標不治本。
由於元數據等內容是難以預測的,Java8中已經移除了永久代,新增了一塊內存區域Metaspace(元空間),不少其餘雜項(例如字符串常量池)都移動了Java堆中。Class
定義信息等元數據目前是直接加載到元空間中。元空間是一片分配在機器本地內存(native memory
)的內存區,它和承載Java對象的堆內存是隔離的。默認狀況下,元空間的大小僅僅受限於機器本地內存能夠分配給Java程序的極限值,這樣基本能夠避免由於添加新的類致使java.lang.OutOfMemoryError: Permgen space
異常發生的場景。
VM參數 | 功能 | 可選值 | 默認值 |
---|---|---|---|
XX:MetaspaceSize=Xm |
Metaspace擴容時觸發FullGC的初始化閾值 | - | - |
XX:MaxMetaspaceSize=Ym |
Metaspace的內存上限 | - | 接近於無窮大 |
VM參數 | 功能 | 可選值 | 默認值 |
---|---|---|---|
-Xmx |
設置最大堆內存大小 | 有下限控制,視VM版本 | - |
-Xms |
設置最小堆內存大小 | 有下限控制,視VM版本 | - |
VM參數 | 功能 | 可選值 | 默認值 |
---|---|---|---|
-Xmn |
設置年輕代內存大小 | - | - |
-XX:NewRatio= |
設置老年代和年輕代的內存大小比值,設置爲4表示年輕代佔堆內存的1/5 | - | 4 |
-XX:SurvivorRatio= |
設置Eden和倖存者區域的內存大小比值,設置爲8表示from:to:Eden=1:1:8 | - | 8 |
參考R大(RednaxelaFX
)的知乎回答,其實在HotSpot VM
的GC分類只有兩大種:
Partial GC
:也就是部分GC,不收集整個GC堆。
Young GC
:只收集young gen的GC。Old GC
:只收集old gen的GC,目前只有CMS的concurrent collection是這個模式。Mixed GC
:收集整個young gen以及部分old gen的GC,目前只有G1有這個模式。Full GC
:收集整個堆,包括young gen、old gen、perm gen(若是存在的話)等全部部分的模式。由於HotSpot VM
發展多年,外界對GC的名詞解讀已經混亂,因此纔出現了Minor GC
、Major GC
和Full GC
。
Minor GC,也就是Minor Garbage Collection,直譯爲次級垃圾回收,它的定義相對清晰:發生在年輕代的垃圾回收就叫作Minor GC。Minor Garbage Collection處理過程當中會發生:
Major GC(Major Garbage Collection,能夠直譯爲主垃圾收集)和Full GC目前是兩個沒有正式定義的術語,具體來講就是:JVM規範中或者垃圾收集研究論文中都沒有明肯定義Major GC或者Full GC。不過按照民間或者約定俗成,二者區別以下:
實際上,GC過程是十分複雜的,並且不少Major GC都是由Minor GC觸發的,因此要嚴格分割Major GC或者Minor GC幾乎是不可能的。另外一方面,如今垃圾收集算法像G1收集算法提供部分垃圾回收功能,側面說明並不能單純按照收集什麼區域來劃分GC的類型。
上面的一些理論或者資料指明:與其討論或者擔憂GC究竟是Major GC或者是Minor GC,不如花更多精力去關注GC過程是否會致使應用的線程停頓或者GC過程是否可以和應用線程併發執行。
下面分析一下目前Hotspot VM
中比較常見的GC算法,由於G1算法相對複雜,這裏暫時沒有能力分析。
GC算法的目的主要有兩個:
尋找存活的對象主要是基於GC Roots
的可達性算法,關於標記階段有幾點注意事項:
標記階段完成後的下一個階段就是移除全部無用的對象,按照處理方式分爲三種常見的算法:
Mark and Sweep
,標記-清理。Mark-Sweep-Compact
,標記-清理-壓縮。Mark and Copy
,標記-複製。Mark-Sweep算法,也就是標記-清理算法,是一種間接回收算法(Indirect Collection),它並不是直接檢測垃圾對象自己,而是先肯定全部存活的對象,而後反過來判斷其餘對象是垃圾對象。主要包括標記和清理兩個階段,它是最簡單和最基礎的收集算法,主要包括兩個階段:
GC Roots
開始遍歷全部可達對象,而且對這些存活的對象進行標記(mark)。內存碎片化是非移動式收集算法沒法解決的一個問題之一:儘管堆中有可用空間,可是內存管理器卻沒法找到一塊連續內存塊來知足較大對象的分配需求,或者花費較長時間才能找到合適的空閒內存空間。
Mark-Sweep-Compact算法,也就是標記-清理-壓縮算法,也是一種間接回收算法(Indirect Collection),它主要包括三個階段:
GC Roots
開始遍歷全部可達對象,而且對這些存活的對象進行標記。對堆內存進行壓縮整理能夠有效地下降內存外部碎片化(External Fragmentation)問題,這個是標記-清理-壓縮算法的一個優點。
Mark-Copy算法,也就是標記-複製算法,和標記-清理-壓縮算法十分類似,重要的區別在於:標記-複製算法在標記和清理完成以後,全部存活的對象會被複制到一個不一樣的內存區域 -- 倖存者空間。主要包括三個階段:
GC Roots
開始遍歷全部可達對象,而且對這些存活的對象進行標記。標記-複製算法能夠避免內存碎片化的問題,可是它的代價比較大,由於用的是半區複製回收,區域可用內存爲原來的一半。
JVM和GC是Java開發者必須掌握的內容,包含的知識其實仍是挺多的,本文也只是簡單介紹了一些基本概念:
後面會分析一下GC收集器搭配和GC日誌查看、JVM提供的工具等等。
參考資料:
OpenJDK HotSpot VM
部分源碼