摘要:本篇將介紹NMT追蹤區域的部分記憶體類型——Java heap、Class、Thread、Code 以及 GC。 本文分享自華為雲社區《Native Memory Tracking 詳解(2):追蹤區域分析(一)》,作者:畢昇小助手。 本篇將介紹NMT追蹤區域的部分記憶體類型——Java heap ...
摘要:本篇將介紹NMT追蹤區域的部分記憶體類型——Java heap、Class、Thread、Code 以及 GC。
本文分享自華為雲社區《Native Memory Tracking 詳解(2):追蹤區域分析(一)》,作者:畢昇小助手。
本篇將介紹NMT追蹤區域的部分記憶體類型——Java heap、Class、Thread、Code 以及 GC。
追蹤區域記憶體類型
在上文中我們列印了 NMT 的相關報告,但想必大家初次看到報告的時候對其追蹤的各個區域往往都是一頭霧水,下麵就讓我們來簡單認識下各個區域。
查看 JVM 中所設定的記憶體類型:
# hotspot/src/share/vm/memory/allocation.hpp /* * Memory types */ enum MemoryType { // Memory type by sub systems. It occupies lower byte. mtJavaHeap = 0x00, // Java heap //Java 堆 mtClass = 0x01, // memory class for Java classes //Java classes 使用的記憶體 mtThread = 0x02, // memory for thread objects //線程對象使用的記憶體 mtThreadStack = 0x03, mtCode = 0x04, // memory for generated code //編譯生成代碼使用的記憶體 mtGC = 0x05, // memory for GC //GC 使用的記憶體 mtCompiler = 0x06, // memory for compiler //編譯器使用的記憶體 mtInternal = 0x07, // memory used by VM, but does not belong to //內部使用的類型 // any of above categories, and not used for // native memory tracking mtOther = 0x08, // memory not used by VM //不是 VM 使用的記憶體 mtSymbol = 0x09, // symbol //符號表使用的記憶體 mtNMT = 0x0A, // memory used by native memory tracking //NMT 自身使用的記憶體 mtClassShared = 0x0B, // class data sharing //共用類使用的記憶體 mtChunk = 0x0C, // chunk that holds content of arenas //chunk用於緩存 mtTest = 0x0D, // Test type for verifying NMT mtTracing = 0x0E, // memory used for Tracing mtNone = 0x0F, // undefined mt_number_of_types = 0x10 // number of memory types (mtDontTrack // is not included as validate type) };
除去這上面的部分選項,我們發現 NMT 中還有一個 unknown 選項,這主要是在執行 jcmd 命令時,記憶體類別還無法確定或虛擬類型信息還沒有到達時的一些記憶體統計。
Java heap
[0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from [0x0000ffff93ea36d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 //reserve 記憶體的 call sites ...... [0x00000000c0000000 - 0x0000000100000000] committed 1048576KB from [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c //commit 記憶體的 call sites ......
無需多言,Java 堆使用的記憶體,絕大多數情況下都是 JVM 使用記憶體的主力,堆記憶體通過 mmap 的方式申請。0x00000000c0000000 - 0x0000000100000000 即是 Java Heap 的虛擬地址範圍,因為此時使用的是 G1 垃圾收集器(不是物理意義上的分代),所以無法看到分代地址,如果使用其他物理分代的收集器(如CMS):
[0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from [0x0000ffffa5cc76d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 [0x0000ffffa5c8bf68] Universe::reserve_heap(unsigned long, unsigned long)+0x2d0 [0x0000ffffa570fa10] GenCollectedHeap::allocate(unsigned long, unsigned long*, int*, ReservedSpace*)+0x160 [0x0000ffffa5711fdc] GenCollectedHeap::initialize()+0x104 [0x00000000d5550000 - 0x0000000100000000] committed 699072KB from [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224 [0x0000ffffa572a450] CardGeneration::CardGeneration(ReservedSpace, unsigned long, int, GenRemSet*)+0xb8 [0x0000ffffa55dc85c] ConcurrentMarkSweepGeneration::ConcurrentMarkSweepGeneration(ReservedSpace, unsigned long, int, CardTableRS*, bool, FreeBlockDictionary::DictionaryChoice)+0x54 [0x0000ffffa572bcdc] GenerationSpec::init(ReservedSpace, int, GenRemSet*)+0xe4 [0x00000000c0000000 - 0x00000000d5550000] committed 349504KB from [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224 [0x0000ffffa5729fe0] Generation::Generation(ReservedSpace, unsigned long, int)+0x98 [0x0000ffffa5612fa8] DefNewGeneration::DefNewGeneration(ReservedSpace, unsigned long, int, char const*)+0x58 [0x0000ffffa5b05ec8] ParNewGeneration::ParNewGeneration(ReservedSpace, unsigned long, int)+0x60
我們可以清楚地看到 0x00000000c0000000 - 0x00000000d5550000 為 Java Heap 的新生代(DefNewGeneration)的範圍,0x00000000d5550000 - 0x0000000100000000 為 Java Heap 的老年代(ConcurrentMarkSweepGeneration)的範圍。
- 我們可以使用 -Xms/-Xmx 或 -XX:InitialHeapSize/-XX:MaxHeapSize 等參數來控制初始/最大的大小,其中基於低停頓的考慮可將兩值設置相等以避免動態擴容縮容帶來的時間開銷(如果基於彈性節約記憶體資源則不必)。
- 可以如上文所述開啟 -XX:+AlwaysPreTouch 參數強制分配物理記憶體來減少運行時的停頓(如果想要快速啟動進程則不必)。
- 基於節省記憶體資源還可以啟用 uncommit 機制等。
Class
Class 主要是類元數據(meta data)所使用的記憶體空間,即虛擬機規範中規定的方法區。具體到 HotSpot 的實現中,JDK7 之前是實現在 PermGen 永久代中,JDK8 之後則是移除了 PermGen 變成了 MetaSpace 元空間。
當然以前 PermGen 還有 Interned strings 或者說 StringTable(即字元串常量池),但是 MetaSpace 並不包含 StringTable,在 JDK8 之後 StringTable 就被移入 Heap,並且在 NMT 中 StringTable 所使用的記憶體被單獨統計到了 Symbol 中。
既然 Class 所使用的記憶體用來存放元數據,那麼想必在啟動 JVM 進程的時候設置的 -XX:MaxMetaspaceSize=256M 參數可以限制住 Class 所使用的記憶體大小。
但是我們在查看 NMT 詳情發現一個奇怪的現象:
Class (reserved=1056899KB, committed=4995KB) (classes #442) //載入的類的數目 (malloc=131KB #259) (mmap: reserved=1056768KB, committed=4864KB)
Class 竟然 reserved 了 1056899KB(約 1G ) 的記憶體,這貌似和我們設定的(256M)不太一樣。
此時我們就不得不簡單補充下相關的內容,我們都知道 JVM 中有一個參數:-XX:UseCompressedOops (簡單來說就是在一定情況下開啟指針壓縮來提升性能),該參數在非 64 位和手動設定 -XX:-UseCompressedOops 的情況下是不會開啟的,而只有在64位系統、不是 client VM、並且 max_heap_size <= max_heap_for_compressed_oops(一個近似32GB的數值)的情況下會預設開啟(計算邏輯可以查看 hotspot/src/share/vm/runtime/arguments.cpp 中的 Arguments::set_use_compressed_oops() 方法)。
而如果 -XX:UseCompressedOops 被開啟,並且我們沒有手動設置過 -XX:-UseCompressedClassPointers 的話,JVM 會預設幫我們開啟 UseCompressedClassPointers(詳情可查看 hotspot/src/share/vm/runtime/arguments.cpp 中的 Arguments::set_use_compressed_klass_ptrs() 方法)。
我們先忽略 UseCompressedOops 不提,在 UseCompressedClassPointers 被啟動之後,_metadata 的指針就會由 64 位的 Klass 壓縮為 32 位無符號整數值 narrowKlass。簡單看下指向關係:
Java object InstanceKlass [ _mark ] [ _klass/_narrowKlass ] --> [ ... ] [ fields ] [ _java_mirror ] [ ... ] (heap) (MetaSpace)
如果我們用的是未壓縮過的 _klass ,那麼使用 64 位指針定址,因此 Klass 可以放置在任意位置;但是如果我們使用壓縮過的 narrowKlass (32位) 進行定址,那麼為了找到該結構實際的 64 位地址,我們不光需要位移操作(如果以 8 位元組對齊左移 3 位),還需要設置一個已知的公共基址,因此限制了我們需要為 Klass 分配為一個連續的記憶體區域。
所以整個 MetaSpace 的記憶體結構在是否開啟 UseCompressedClassPointers 時是不同的:
- 如果未開啟指針壓縮,那麼 MetaSpace 只有一個 Metaspace Context(incl chunk freelist) 指向很多不同的 virtual space;
- 如果開啟了指針壓縮,Klass 和非 Klass 部分分開存放,Klass 部分放一個連續的記憶體區域 Metaspace Context(class) (指向一塊大的連續的 virtual space),非 Klass 部分則依照未開啟壓縮的模式放在很多不同的 virtual space 中。這塊 Metaspace Context(class) 記憶體,就是傳說中的 CompressedClassSpaceSize 所設置的記憶體。
//未開啟壓縮 +--------+ +--------+ +--------+ +--------+ | CLD | | CLD | | CLD | | CLD | +--------+ +--------+ +--------+ +--------+ | | | | | | | | allocates variable-sized, | | | | typically small-tiny metaspace blocks v v v v +--------+ +--------+ +--------+ +--------+ | arena | | arena | | arena | | arena | +--------+ +--------+ +--------+ +--------+ | | | | | | | | allocate and, on death, release-in-bulk | | | | medium-sized chunks (1k..4m) | | | | v v v v +--------------------------------------------+ | | | Metaspace Context | | (incl chunk freelist) | | | +--------------------------------------------+ | | | | | | map/commit/uncommit/release | | | v v v +---------+ +---------+ +---------+ | | | | | | | virtual | | virtual | | virtual | | space | | space | | space | | | | | | | +---------+ +---------+ +---------+ //開啟了指針壓縮 +--------+ +--------+ | CLD | | CLD | +--------+ +--------+ / \ / \ Each CLD has two arenas... / \ / \ / \ / \ v v v v +--------+ +--------+ +--------+ +--------+ | noncl | | class | | noncl | | class | | arena | | arena | | arena | | arena | +--------+ +--------+ +--------+ +--------+ | \ / | | --------\ | Non-class arenas take from non-class context, | / | | class arenas take from class context | /--------- | | v v v v +--------------------+ +------------------------+ | | | | | Metaspace Context | | Metaspace Context | | (nonclass) | | (class) | | | | | +--------------------+ +------------------------+ | | | | | | Non-class context: list of smallish mappings | | | Class context: one large mapping (the class space) v v v +--------+ +--------+ +----------------~~~~~~~-----+ | | | | | | | virtual| | virt | | virt space (class space) | | space | | space | | | | | | | | | +--------+ +--------+ +----------------~~~~~~~-----+
MetaSpace相關內容就不再展開描述了,詳情可以參考官方文檔 Metaspace - Metaspace - OpenJDK Wiki (http://java.net) [1] 與 Thomas Stüfe 的系列文章 What is Metaspace? | http://stuefe.de [2]。
我們查看 reserve 的具體日誌,發現大部分的記憶體都是 Metaspace::allocate_metaspace_compressed_klass_ptrs 方法申請的,這正是用來分配 CompressedClassSpace 空間的方法:
[0x0000000100000000 - 0x0000000140000000] reserved 1048576KB for Class from [0x0000ffff93ea28d0] ReservedSpace::ReservedSpace(unsigned long, unsigned long, bool, char*, unsigned long)+0x90 [0x0000ffff93c16694] Metaspace::allocate_metaspace_compressed_klass_ptrs(char*, unsigned char*)+0x42c [0x0000ffff93c16e0c] Metaspace::global_initialize()+0x4fc [0x0000ffff93e688a8] universe_init()+0x88
JVM 在初始化 MetaSpace 時,調用鏈路如下:
InitializeJVM ->
Thread::vreate_vm ->
init_globals ->
universe_init ->
MetaSpace::global_initalize ->
Metaspace::allocate_metaspace_compressed_klass_ptrs
查看相關源碼:
# hotspot/src/share/vm/memory/metaspace.cpp void Metaspace::allocate_metaspace_compressed_klass_ptrs(char* requested_addr, address cds_base) { ...... ReservedSpace metaspace_rs = ReservedSpace(compressed_class_space_size(), _reserve_alignment, large_pages, requested_addr, 0); ...... metaspace_rs = ReservedSpace(compressed_class_space_size(), _reserve_alignment, large_pages); ...... }
我們可以發現如果開啟了 UseCompressedClassPointers ,那麼就會調用 allocate_metaspace_compressed_klass_ptrs 方法去 reserve 一個 compressed_class_space_size() 大小的空間(由於我們沒有顯式地設置過 -XX:CompressedClassSpaceSize 的大小,所以此時預設值為 1G)。如果我們顯式地設置 -XX:CompressedClassSpaceSize=256M 再重啟 JVM ,就會發現 reserve 的記憶體大小已經被限制住了:
Thread (reserved=258568KB, committed=258568KB) (thread #127) (stack: reserved=258048KB, committed=258048KB) (malloc=390KB #711) (arena=130KB #234)
但是此時我們不禁會有一個疑問,那就是既然 CompressedClassSpaceSize 可以 reverse 遠遠超過 -XX:MaxMetaspaceSize 設置的大小,那麼 -XX:MaxMetaspaceSize 會不會無法限制住整體 MetaSpace 的大小?實際上 -XX:MaxMetaspaceSize 是可以限制住 MetaSpace 的大小的,只是 HotSpot 此處的代碼順序有問題容易給大家造成誤解和歧義~
查看相關代碼:
# hotspot/src/share/vm/memory/metaspace.cpp void Metaspace::ergo_initialize() { ...... CompressedClassSpaceSize = align_size_down_bounded(CompressedClassSpaceSize, _reserve_alignment); set_compressed_class_space_size(CompressedClassSpaceSize); // Initial virtual space size will be calculated at global_initialize() uintx min_metaspace_sz = VIRTUALSPACEMULTIPLIER * InitialBootClassLoaderMetaspaceSize; if (UseCompressedClassPointers) { if ((min_metaspace_sz + CompressedClassSpaceSize) > MaxMetaspaceSize) { if (min_metaspace_sz >= MaxMetaspaceSize) { vm_exit_during_initialization("MaxMetaspaceSize is too small."); } else { FLAG_SET_ERGO(uintx, CompressedClassSpaceSize, MaxMetaspaceSize - min_metaspace_sz); } } } ...... }
我們可以發現如果 min_metaspace_sz + CompressedClassSpaceSize > MaxMetaspaceSize 的話,JVM 會將 CompressedClassSpaceSize 的值設置為 MaxMetaspaceSize - min_metaspace_sz 的大小,即最後 CompressedClassSpaceSize 的值是小於 MaxMetaspaceSize 的大小的,但是為何之前會 reserve 一個大的值呢?因為在重新計算 CompressedClassSpaceSize 的值之前,JVM 就先調用了 set_compressed_class_space_size 方法將 compressed_class_space_size 的大小設置成了未重新計算的、預設的 CompressedClassSpaceSize 的大小。還記得 compressed_class_space_size 嗎?沒錯,正是我們在上面調用 allocate_metaspace_compressed_klass_ptrs 方法時 reserve 的大小,所以此時 reserve 的其實是一個不正確的值,我們只需要將 set_compressed_class_space_size 的操作放在重新計算 CompressedClassSpaceSize 大小的邏輯之後就能修正這種錯誤。當然因為是 reserve 的記憶體,對真正運行起來的 JVM 並無太大的負面影響,所以沒有人給社區報過這個問題,社區也沒有修改過這一塊邏輯。
如果你使用的 JDK 版本大於等於 10,那麼你直接可以通過 NMT 看到更詳細劃分的 Class 信息(區分了存放 klass 的區域即 Class space、存放非 klass 的區域即 Metadata )。
Class (reserved=1056882KB, committed=1053042KB) (classes #483) (malloc=114KB #629) (mmap: reserved=1056768KB, committed=1052928KB) ( Metadata: ) ( reserved=8192KB, committed=4352KB) ( used=3492KB) ( free=860KB) ( waste=0KB =0.00%) ( Class space:) ( reserved=1048576KB, committed=512KB) ( used=326KB) ( free=186KB) ( waste=0KB =0.00%)
Thread
線程所使用的記憶體:
Thread (reserved=258568KB, committed=258568KB) (thread #127) //線程個數 (stack: reserved=258048KB, committed=258048KB) //棧使用的記憶體 (malloc=390KB #711) (arena=130KB #234) //線程句柄使用的記憶體 ...... [0x0000fffdbea32000 - 0x0000fffdbec32000] reserved and committed 2048KB for Thread Stack from [0x0000ffff935ab79c] attach_listener_thread_entry(JavaThread*, Thread*)+0x34 [0x0000ffff93e3ddb4] JavaThread::thread_main_inner()+0xf4 [0x0000ffff93e3e01c] JavaThread::run()+0x214 [0x0000ffff93cb49e4] java_start(Thread*)+0x11c [0x0000fffdbecce000 - 0x0000fffdbeece000] reserved and committed 2048KB for Thread Stack from [0x0000ffff93cb49e4] java_start(Thread*)+0x11c [0x0000ffff944148bc] start_thread+0x19c
觀察 NMT 列印信息,我們可以發現,此時的 JVM 進程共使用了127個線程,committed 了 258568KB 的記憶體。
繼續觀察下麵各個線程的分配情況就會發現,每個線程 committed 了2048KB(2M)的記憶體空間,這可能和平時的認知不太相同,因為平時我們大多數情況下使用的都是x86平臺,而筆者此時使用的是 ARM (aarch64)的平臺,所以此處線程預設分配的記憶體與 x86 不同。
如果我們不顯式的設置 -Xss/-XX:ThreadStackSize 相關的參數,那麼 JVM 會使用預設的值。
在 aarch64 平臺下預設為 2M:
# globals_linux_aarch64.hpp define_pd_global(intx, ThreadStackSize, 2048); // 0 => use system default define_pd_global(intx, VMThreadStackSize, 2048);
而在 x86 平臺下預設為 1M:
# globals_linux_x86.hpp define_pd_global(intx, ThreadStackSize, 1024); // 0 => use system default define_pd_global(intx, VMThreadStackSize, 1024);
如果我們想縮減此部分記憶體的使用,可以使用參數 -Xss/-XX:ThreadStackSize 設置適合自身業務情況的大小,但是需要進行相關壓力測試保證不會出現溢出等錯誤。
Code
JVM 自身會生成一些 native code 並將其存儲在稱為 codecache 的記憶體區域中。JVM 生成 native code 的原因有很多,包括動態生成的解釋器迴圈、 JNI、即時編譯器(JIT)編譯 Java 方法生成的本機代碼 。其中 JIT 生成的 native code 占據了 codecache 絕大部分的空間。
Code (reserved=266273KB, committed=4001KB) (malloc=33KB #309) (mmap: reserved=266240KB, committed=3968KB) ...... [0x0000ffff7c000000 - 0x0000ffff8c000000] reserved 262144KB for Code from [0x0000ffff93ea3c2c] ReservedCodeSpace::ReservedCodeSpace(unsigned long, unsigned long, bool)+0x84 [0x0000ffff9392dcd0] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xc8 [0x0000ffff9374bd64] codeCache_init()+0xb4 [0x0000ffff9395ced0] init_globals()+0x58 [0x0000ffff7c3c0000 - 0x0000ffff7c3d0000] committed 64KB from [0x0000ffff93ea47e0] VirtualSpace::expand_by(unsigned long, bool)+0x1d8 [0x0000ffff9392e01c] CodeHeap::expand_by(unsigned long)+0xac [0x0000ffff9374cee4] CodeCache::allocate(int, bool)+0x64 [0x0000ffff937444b8] MethodHandlesAdapterBlob::create(int)+0xa8
追蹤 codecache 的邏輯:
# codeCache.cpp void CodeCache::initialize() { ...... CodeCacheExpansionSize = round_to(CodeCacheExpansionSize, os::vm_page_size()); InitialCodeCacheSize = round_to(InitialCodeCacheSize, os::vm_page_size()); ReservedCodeCacheSize = round_to(ReservedCodeCacheSize, os::vm_page_size()); if (!_heap->reserve(ReservedCodeCacheSize, InitialCodeCacheSize, CodeCacheSegmentSize)) { vm_exit_during_initialization("Could not reserve enough space for code cache"); } ...... } # virtualspace.cpp //記錄 mtCode 的函數,其中 r_size 由 ReservedCodeCacheSize 得出 ReservedCodeSpace::ReservedCodeSpace(size_t r_size, size_t rs_align, bool large) : ReservedSpace(r_size, rs_align, large, /*executable*/ true) { MemTracker::record_virtual_memory_type((address)base(), mtCode); }
可以發現 CodeCache::initialize() 時 codecache reserve 的最大記憶體是由我們設置的 -XX:ReservedCodeCacheSize 參數決定的(當然 ReservedCodeCacheSize 的值會做一些對齊操作),我們可以通過設置 -XX:ReservedCodeCacheSize 來限制 Code 相關的最大記憶體。
同時我們發現,初始化時 codecache commit 的記憶體可以由 -XX:InitialCodeCacheSize 參數來控制,具體計算代碼可以查看 VirtualSpace::expand_by 函數。
我們設置 -XX:InitialCodeCacheSize=128M 後重啟 JVM 進程,再次查看 NMT detail:
Code (reserved=266273KB, committed=133153KB) (malloc=33KB #309) (mmap: reserved=266240KB, committed=133120KB) ...... [0x0000ffff80000000 - 0x0000ffff88000000] committed 131072KB from [0x0000ffff979e60e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224 [0x0000ffff9746fcfc] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xf4 [0x0000ffff9728dd64] codeCache_init()+0xb4 [0x0000ffff9749eed0] init_globals()+0x58
我們可以通過 -XX:InitialCodeCacheSize 來設置 codecache 初始 commit 的記憶體。
- 除了使用 NMT 列印codecache相關信息,我們還可以通過 -XX:+PrintCodeCache(JVM 關閉時輸出codecache的使用情況)和jcmdpidCompiler.codecache(只有在 JDK 9 及以上版本的jcmd才支持該選項)來查看codecache相關的信息。
- 瞭解更多codecache詳情可以查看CodeCache官方文檔 [3]。
GC
GC 所使用的記憶體,就是垃圾收集器使用的數據所占據的記憶體,例如卡表 card tables、記憶集 remembered sets、標記棧 marking stack、標記點陣圖 marking bitmaps 等等。其實不論是 card tables、remembered sets 還是 marking stack、marking bitmaps,都是一種藉助額外的空間,來記錄不同記憶體區域之間引用關係的結構(都是基於空間換時間的思想,否則尋找引用關係就需要諸如遍歷這種浪費時間的方式)。
簡單介紹下相關概念:
更詳細的信息不深入展開介紹了,可以查看彭成寒老師《JVM G1源碼分析和調優》2.3 章 [4] 與 4.1 章節 [5],還可以查看 R大(RednaxelaFX)對相關概念的科普 [6]。
- 卡表 card tables,在部分收集器(如CMS)中存儲跨代引用(如老年代中對象指向年輕代的對象)的數據結構,精度可以有很多種選擇:
如果精確到機器字,那麼往往描述的區域太小了,使用的記憶體開銷會變大,所以 HotSpot 中選擇 512KB 為精度大小。
卡表甚至可以細到和 bitmap 相同,即使用 1 bit 位來對應一個記憶體頁(512KB),但是因為 JVM 在操作一個 bit 位時,仍然需要讀取整個機器字 word,並且操作 bit 位的開銷有時反而大於操作 byte 。所以 HotSpot 的 cardTable 選擇使用 byte 數組代替 bit ,用 1 byte 對應 512KB 的空間,使用 byte 數組的開銷也可以接受(1G 的堆記憶體使用卡表也只占用2M:1 * 1024 * 1024 / 512 = 2048 KB)。
我們以 cardTableModRefBS 為例,查看其源碼結構:
# hotspor/src/share/vm/momery/cardTableModRefBS.hpp //精度為 512 KB enum SomePublicConstants { card_shift = 9, card_size = 1 << card_shift, card_size_in_words = card_size / sizeof(HeapWord) }; ...... class CardTableModRefBS: public ModRefBarrierSet { ..... size_t _byte_map_size; // in bytes jbyte* _byte_map; // the card marking array ..... }
可以發現 cardTableModRefBS 通過枚舉 SomePublicConstants 來定義對應的記憶體塊 card_size 的大小即:512KB,而 _byte_map 則是用於標記的卡表位元組數組,我們可以看到其對應的類型為 jbyte(typedef signed char jbyte,其實就是一個位元組即 1byte)。
當然後來卡表不只記錄跨代引用的關係,還會被 CMS 的增量更新之類的操作復用。
- 字粒度:精確到機器字(word),該字包含有跨代指針。
- 對象粒度:精確到一個對象,該對象里有欄位含有跨代指針。
- card粒度:精確到一大塊記憶體區域,該區域內有對象含有跨代指針。
記憶集 remembered sets,可以選擇的粒度和卡表差不多,或者你說卡表也是記憶集的一種實現方式也可以(區別可以查看上面給出的 R大的鏈接)。G1 中引入記憶集 RSet 來記錄 Region 間的跨代引用,G1 中的卡表的作用並不是記錄引用關係,而是用於記錄該區域中對象垃圾回收過程中的狀態信息。
標記棧 marking stack,初始標記掃描根集合時,會標記所有從根集合可直接到達的對象並將它們的欄位壓入掃描棧(marking stack)中等待後續掃描。
標記點陣圖 marking bitmaps,我們常使用點陣圖來指示哪塊記憶體已經使用、哪塊記憶體還未使用。比如 G1 中的 Mixed GC 混合收集演算法(收集所有的年輕代的 Region,外加根據global concurrent marking 統計得出的收集收益高的部分老年代 Region)中用到了併發標記,併發標記就引入兩個點陣圖 PrevBitMap 和 NextBitMap,用這兩個點陣圖來輔助標記併發標記不同階段記憶體的使用狀態。
查看 NMT 詳情:
...... [0x0000fffe16000000 - 0x0000fffe17000000] reserved 16384KB for GC from [0x0000ffff93ea2718] ReservedSpace::ReservedSpace(unsigned long, unsigned long)+0x118 [0x0000ffff93892328] G1CollectedHeap::create_aux_memory_mapper(char const*, unsigned long, unsigned long)+0x48 [0x0000ffff93899108] G1CollectedHeap::initialize()+0x368 [0x0000ffff93e68594] Universe::initialize_heap()+0x15c [0x0000fffe16000000 - 0x0000fffe17000000] committed 16384KB from [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c [0x0000ffff938bc08c] G1PageBasedVirtualSpace::commit(unsigned long, unsigned long)+0x11c [0x0000ffff938bf774] G1RegionsLargerThanCommitSizeMapper::commit_regions(unsigned int, unsigned long)+0x5c [0x0000ffff93943f8c] HeapRegionManager::commit_regions(unsigned int, unsigned long)+0xb4 ......
我們可以發現 JVM 在初始化 heap 堆的時候(此時是 G1 收集器所使用的堆 G1CollectedHeap),不僅會創建 remember set ,還會有一個 create_aux_memory_mapper 的操作,用來給 GC 輔助用的數據結構(如:card table、prev bitmap、 next bitmap 等)創建對應的記憶體映射,相關操作可以查看 g1CollectedHeap 初始化部分源代碼:
# hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp jint G1CollectedHeap::initialize() { ...... //創建 G1 remember set // Also create a G1 rem set. _g1_rem_set = new G1RemSet(this, g1_barrier_set()); ...... // Create storage for the BOT, card table, card counts table (hot card cache) and the bitmaps. G1RegionToSpaceMapper* bot_storage = create_aux_memory_mapper("Block offset table", G1BlockOffsetSharedArray::compute_size(g1_rs.size() / HeapWordSize), G1BlockOffsetSharedArray::N_bytes); ReservedSpace cardtable_rs(G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size() / HeapWordSize)); G1RegionToSpaceMapper* cardtable_storage = create_aux_memory_mapper("Card table", G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size() / HeapWordSize), G1BlockOffsetSharedArray::N_bytes); G1RegionToSpaceMapper* card_counts_storage = create_aux_memory_mapper("Card counts table", G1BlockOffsetSharedArray::compute_size(g1_rs.size() / HeapWordSize), G1BlockOffsetSharedArray::N_bytes); size_t bitmap_size = CMBitMap::compute_size(g1_rs.size()); G1RegionToSpaceMapper* prev_bitmap_storage = create_aux_memory_mapper("Prev Bitmap", bitmap_size, CMBitMap::mark_distance()); G1RegionToSpaceMapper* next_bitmap_storage = create_aux_memory_mapper("Next Bitmap", bitmap_size, CMBitMap::mark_distance()); _hrm.initialize(heap_storage, prev_bitmap_storage, next_bitmap_storage, bot_storage, cardtable_storage, card_counts_storage); g1_barrier_set()->initialize(cardtable_storage); // Do later initialization work for concurrent refinement. _cg1r->init(card_counts_storage); ...... }
因為這些輔助的結構都是一種空間換時間的思想,所以不可避免的會占用額外的記憶體,尤其是 G1 的 RSet 結構,當我們調大我們的堆記憶體,GC 所使用的記憶體也會不可避免的跟隨增長:
# -Xmx1G -Xms1G GC (reserved=164403KB, committed=164403KB) (malloc=92723KB #6540) (mmap: reserved=71680KB, committed=71680KB) # -Xmx2G -Xms2G GC (reserved=207891KB, committed=207891KB) (malloc=97299KB #12683) (mmap: reserved=110592KB, committed=110592KB) # -Xmx4G -Xms4G GC (reserved=290313KB, committed=290313KB) (malloc=101897KB #12680) (mmap: reserved=188416KB, committed=188416KB) # -Xmx8G -Xms8G GC (reserved=446473KB, committed=446473KB) (malloc=102409KB #12680) (mmap: reserved=344064KB, committed=344064KB)
我們可以看到這個額外的記憶體開銷一般在 1% - 20%之間,當然如果我們不使用 G1 收集器,這個開銷是沒有那麼大的:
# -XX:+UseSerialGC -Xmx8G -Xms8G GC (reserved=27319KB, committed=27319KB) (malloc=7KB #79) (mmap: reserved=27312KB, committed=27312KB) # -XX:+UseConcMarkSweepGC -Xmx8G -Xms8G GC (reserved=167318KB, committed=167318KB) (malloc=140006KB #373) (mmap: reserved=27312KB, committed=27312KB)
我們可以看到,使用最輕量級的 UseSerialGC,GC 部分占用的記憶體有很明顯的降低(436M -> 26.67M);使用 CMS ,GC 部分從 436M 降低到 163.39M。
GC 這塊記憶體是必須的,也是我們在使用過程中無法壓縮的。停頓、吞吐量、記憶體占用就是 GC 中不可能同時達到的三元悖論,不同的垃圾收集器在這三者中有不同的側重,我們應該結合自身的業務情況綜合考量選擇合適的垃圾收集器。
由於篇幅有限,將在下篇文章繼續給大家分享 追蹤區域的其它記憶體類型(包含Compiler、Internal、Symbol、Native Memory Tracking、Arena Chunk 和 Unknown)以及 NMT 無法追蹤的記憶體,敬請期待!
參考
- https://wiki.openjdk.java.net/display/HotSpot/Metaspace
- https://stuefe.de/posts/metaspace/what-is-metaspace
- https://docs.oracle.com/javase/8/embedded/develop-apps-platforms/codecache.htm
- https://weread.qq.com/web/reader/53032310717f44515302749k3c5327902153c59dc0488e1
- https://weread.qq.com/web/reader/53032310717f44515302749ka1d32a6022aa1d0c6e83eb4
- https://hllvm-group.iteye.com/group/topic/21468#post-272070
歡迎加入Compiler SIG交流群與大家共同交流學習編譯技術相關內容,掃碼添加小助手微信邀請你進入Compiler SIG交流群。