摘要:本篇將介紹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交流群,
點擊關注,第一時間了解華為云新鮮技術~
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/519116.html
標籤:其他
