上篇文章Native Memory Tracking 詳解(1):基礎介紹中,分享了如何使用NMT,以及NMT內存 & OS內存概念的差異性,本篇將介紹NMT追蹤區域的部分內存類型——Java heap、Class、Thread、Code 以及 GC。
4.追蹤區域內存類型
在上文中我們打印了 NMT 的相關報告,但想必大家初次看到報告的時候對其追蹤的各個區域往往都是一頭霧水,下面就讓我們來簡單認識下各個區域。
查看 JVM 中所設定的內存類型:
#hotspot/src/share/vm/memory/allocation.hpp /* *Memorytypes */ enumMemoryType{ //Memorytypebysubsystems.Itoccupieslowerbyte. mtJavaHeap=0x00,//Javaheap//Java堆 mtClass=0x01,//memoryclassforJavaclasses//Javaclasses使用的內存 mtThread=0x02,//memoryforthreadobjects//線程對象使用的內存 mtThreadStack=0x03, mtCode=0x04,//memoryforgeneratedcode//編譯生成代碼使用的內存 mtGC=0x05,//memoryforGC//GC使用的內存 mtCompiler=0x06,//memoryforcompiler//編譯器使用的內存 mtInternal=0x07,//memoryusedbyVM,butdoesnotbelongto//內部使用的類型 //anyofabovecategories,andnotusedfor //nativememorytracking mtOther=0x08,//memorynotusedbyVM//不是VM使用的內存 mtSymbol=0x09,//symbol//符號表使用的內存 mtNMT=0x0A,//memoryusedbynativememorytracking//NMT自身使用的內存 mtClassShared=0x0B,//classdatasharing//共享類使用的內存 mtChunk=0x0C,//chunkthatholdscontentofarenas//chunk用于緩存 mtTest=0x0D,//TesttypeforverifyingNMT mtTracing=0x0E,//memoryusedforTracing mtNone=0x0F,//undefined mt_number_of_types=0x10//numberofmemorytypes(mtDontTrack //isnotincludedasvalidatetype) };
除去這上面的部分選項,我們發現 NMT 中還有一個 unknown 選項,這主要是在執行 jcmd 命令時,內存類別還無法確定或虛擬類型信息還沒有到達時的一些內存統計。
4.1 Java heap
[0x00000000c0000000-0x0000000100000000]reserved1048576KBforJavaHeapfrom [0x0000ffff93ea36d8]ReservedHeapSpace::ReservedHeapSpace(unsignedlong,unsignedlong,bool,char*)+0xb8//reserve內存的callsites ...... [0x00000000c0000000-0x0000000100000000]committed1048576KBfrom [0x0000ffff938bbe8c]G1PageBasedVirtualSpace::commit_internal(unsignedlong,unsignedlong)+0x14c//commit內存的callsites ......
無需多言,Java 堆使用的內存,絕大多數情況下都是 JVM 使用內存的主力,堆內存通過 mmap 的方式申請。0x00000000c0000000 - 0x0000000100000000 即是 Java Heap 的虛擬地址范圍,因為此時使用的是 G1 垃圾收集器(不是物理意義上的分代),所以無法看到分代地址,如果使用其他物理分代的收集器(如CMS):
[0x00000000c0000000-0x0000000100000000]reserved1048576KBforJavaHeapfrom [0x0000ffffa5cc76d8]ReservedHeapSpace::ReservedHeapSpace(unsignedlong,unsignedlong,bool,char*)+0xb8 [0x0000ffffa5c8bf68]Universe::reserve_heap(unsignedlong,unsignedlong)+0x2d0 [0x0000ffffa570fa10]GenCollectedHeap::allocate(unsignedlong,unsignedlong*,int*,ReservedSpace*)+0x160 [0x0000ffffa5711fdc]GenCollectedHeap::initialize()+0x104 [0x00000000d5550000-0x0000000100000000]committed699072KBfrom [0x0000ffffa5cc80e4]VirtualSpace::initialize(ReservedSpace,unsignedlong)+0x224 [0x0000ffffa572a450]CardGeneration::CardGeneration(ReservedSpace,unsignedlong,int,GenRemSet*)+0xb8 [0x0000ffffa55dc85c]ConcurrentMarkSweepGeneration::ConcurrentMarkSweepGeneration(ReservedSpace,unsignedlong,int,CardTableRS*,bool,FreeBlockDictionary::DictionaryChoice)+0x54 [0x0000ffffa572bcdc]GenerationSpec::init(ReservedSpace,int,GenRemSet*)+0xe4 [0x00000000c0000000-0x00000000d5550000]committed349504KBfrom [0x0000ffffa5cc80e4]VirtualSpace::initialize(ReservedSpace,unsignedlong)+0x224 [0x0000ffffa5729fe0]Generation::Generation(ReservedSpace,unsignedlong,int)+0x98 [0x0000ffffa5612fa8]DefNewGeneration::DefNewGeneration(ReservedSpace,unsignedlong,int,charconst*)+0x58 [0x0000ffffa5b05ec8]ParNewGeneration::ParNewGeneration(ReservedSpace,unsignedlong,int)+0x60
我們可以清楚地看到 0x00000000c0000000 - 0x00000000d5550000 為 Java Heap 的新生代(DefNewGeneration)的范圍,0x00000000d5550000 - 0x0000000100000000 為 Java Heap 的老年代(ConcurrentMarkSweepGeneration)的范圍。
我們可以使用 -Xms/-Xmx 或 -XX:InitialHeapSize/-XX:MaxHeapSize 等參數來控制初始/最大的大小,其中基于低停頓的考慮可將兩值設置相等以避免動態擴容縮容帶來的時間開銷(如果基于彈性節約內存資源則不必)。
可以如上文所述開啟 -XX:+AlwaysPreTouch 參數強制分配物理內存來減少運行時的停頓(如果想要快速啟動進程則不必)。
基于節省內存資源還可以啟用 uncommit 機制等。
4.2 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。簡單看下指向關系:
JavaobjectInstanceKlass [_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| +--------++--------++--------++--------+ |||| ||||allocatesvariable-sized, ||||typicallysmall-tinymetaspaceblocks vvvv +--------++--------++--------++--------+ |arena||arena||arena||arena| +--------++--------++--------++--------+ |||| ||||allocateand,ondeath,release-in-bulk ||||medium-sizedchunks(1k..4m) |||| vvvv +--------------------------------------------+ || |MetaspaceContext| |(inclchunkfreelist)| || +--------------------------------------------+ ||| |||map/commit/uncommit/release ||| vvv +---------++---------++---------+ |||||| |virtual||virtual||virtual| |space||space||space| |||||| +---------++---------++---------+ //開啟了指針壓縮 +--------++--------+ |CLD||CLD| +--------++--------+ //EachCLDhastwoarenas... // // vvvv +--------++--------++--------++--------+ |noncl||class||noncl||class| |arena||arena||arena||arena| +--------++--------++--------++--------+ |/| |--------|Non-classarenastakefromnon-classcontext, |/||classarenastakefromclasscontext |/---------|| vvvv +--------------------++------------------------+ |||| |MetaspaceContext||MetaspaceContext| |(nonclass)||(class)| |||| +--------------------++------------------------+ ||| |||Non-classcontext:listofsmallishmappings |||Classcontext:onelargemapping(theclassspace) vvv +--------++--------++----------------~~~~~~~-----+ |||||| |virtual||virt||virtspace(classspace)| |space||space||| |||||| +--------++--------++----------------~~~~~~~-----+
MetaSpace相關內容就不再展開描述了,詳情可以參考官方文檔 Metaspace - Metaspace - OpenJDK Wiki (java.net)[1] 與 Thomas Stüfe 的系列文章 What is Metaspace? | stuefe.de [2]。
我們查看 reserve 的具體日志,發現大部分的內存都是 Metaspace::allocate_metaspace_compressed_klass_ptrs 方法申請的,這正是用來分配 CompressedClassSpace 空間的方法:
[0x0000000100000000-0x0000000140000000]reserved1048576KBforClassfrom [0x0000ffff93ea28d0]ReservedSpace::ReservedSpace(unsignedlong,unsignedlong,bool,char*,unsignedlong)+0x90 [0x0000ffff93c16694]Metaspace::allocate_metaspace_compressed_klass_ptrs(char*,unsignedchar*)+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 voidMetaspace::allocate_metaspace_compressed_klass_ptrs(char*requested_addr,addresscds_base){ ...... ReservedSpacemetaspace_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 voidMetaspace::ergo_initialize(){ ...... CompressedClassSpaceSize=align_size_down_bounded(CompressedClassSpaceSize,_reserve_alignment); set_compressed_class_space_size(CompressedClassSpaceSize); //Initialvirtualspacesizewillbecalculatedatglobal_initialize() uintxmin_metaspace_sz= VIRTUALSPACEMULTIPLIER*InitialBootClassLoaderMetaspaceSize; if(UseCompressedClassPointers){ if((min_metaspace_sz+CompressedClassSpaceSize)>MaxMetaspaceSize){ if(min_metaspace_sz>=MaxMetaspaceSize){ vm_exit_during_initialization("MaxMetaspaceSizeistoosmall."); }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%) (Classspace:) (reserved=1048576KB,committed=512KB) (used=326KB) (free=186KB) (waste=0KB=0.00%)
4.3 Thread
線程所使用的內存:
Thread(reserved=258568KB,committed=258568KB) (thread#127)//線程個數 (stack:reserved=258048KB,committed=258048KB)//棧使用的內存 (malloc=390KB#711) (arena=130KB#234)//線程句柄使用的內存 ...... [0x0000fffdbea32000-0x0000fffdbec32000]reservedandcommitted2048KBforThreadStackfrom [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]reservedandcommitted2048KBforThreadStackfrom [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=>usesystemdefault define_pd_global(intx,VMThreadStackSize,2048);
而在 x86 平臺下默認為 1M:
#globals_linux_x86.hpp define_pd_global(intx,ThreadStackSize,1024);//0=>usesystemdefault define_pd_global(intx,VMThreadStackSize,1024);
如果我們想縮減此部分內存的使用,可以使用參數 -Xss/-XX:ThreadStackSize 設置適合自身業務情況的大小,但是需要進行相關壓力測試保證不會出現溢出等錯誤。
4.4 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]reserved262144KBforCodefrom [0x0000ffff93ea3c2c]ReservedCodeSpace::ReservedCodeSpace(unsignedlong,unsignedlong,bool)+0x84 [0x0000ffff9392dcd0]CodeHeap::reserve(unsignedlong,unsignedlong,unsignedlong)+0xc8 [0x0000ffff9374bd64]codeCache_init()+0xb4 [0x0000ffff9395ced0]init_globals()+0x58 [0x0000ffff7c3c0000-0x0000ffff7c3d0000]committed64KBfrom [0x0000ffff93ea47e0]VirtualSpace::expand_by(unsignedlong,bool)+0x1d8 [0x0000ffff9392e01c]CodeHeap::expand_by(unsignedlong)+0xac [0x0000ffff9374cee4]CodeCache::allocate(int,bool)+0x64 [0x0000ffff937444b8]MethodHandlesAdapterBlob::create(int)+0xa8
追蹤 codecache 的邏輯:
#codeCache.cpp voidCodeCache::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("Couldnotreserveenoughspaceforcodecache"); } ...... } #virtualspace.cpp //記錄mtCode的函數,其中r_size由ReservedCodeCacheSize得出 ReservedCodeSpace::ReservedCodeSpace(size_tr_size, size_trs_align, boollarge): 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]committed131072KBfrom [0x0000ffff979e60e4]VirtualSpace::initialize(ReservedSpace,unsignedlong)+0x224 [0x0000ffff9746fcfc]CodeHeap::reserve(unsignedlong,unsignedlong,unsignedlong)+0xf4 [0x0000ffff9728dd64]codeCache_init()+0xb4 [0x0000ffff9749eed0]init_globals()+0x58
我們可以通過 -XX:InitialCodeCacheSize 來設置 codecache 初始 commit 的內存。
除了使用 NMT 打印 codecache 相關信息,我們還可以通過 -XX:+PrintCodeCache (JVM 關閉時輸出codecache的使用情況)和 jcmd pid Compiler.codecache(只有在 JDK 9 及以上版本的 jcmd 才支持該選項)來查看 codecache 相關的信息。
了解更多 codecache 詳情可以查看 CodeCache 官方文檔[3]。
4.5 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 //精度為512KB enumSomePublicConstants{ card_shift=9, card_size=1<
可以發現 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]reserved16384KBforGCfrom [0x0000ffff93ea2718]ReservedSpace::ReservedSpace(unsignedlong,unsignedlong)+0x118 [0x0000ffff93892328]G1CollectedHeap::create_aux_memory_mapper(charconst*,unsignedlong,unsignedlong)+0x48 [0x0000ffff93899108]G1CollectedHeap::initialize()+0x368 [0x0000ffff93e68594]Universe::initialize_heap()+0x15c [0x0000fffe16000000-0x0000fffe17000000]committed16384KBfrom [0x0000ffff938bbe8c]G1PageBasedVirtualSpace::commit_internal(unsignedlong,unsignedlong)+0x14c [0x0000ffff938bc08c]G1PageBasedVirtualSpace::commit(unsignedlong,unsignedlong)+0x11c [0x0000ffff938bf774]G1RegionsLargerThanCommitSizeMapper::commit_regions(unsignedint,unsignedlong)+0x5c [0x0000ffff93943f8c]HeapRegionManager::commit_regions(unsignedint,unsignedlong)+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 jintG1CollectedHeap::initialize(){ ...... //創建G1rememberset //AlsocreateaG1remset. _g1_rem_set=newG1RemSet(this,g1_barrier_set()); ...... //CreatestoragefortheBOT,cardtable,cardcountstable(hotcardcache)andthebitmaps. G1RegionToSpaceMapper*bot_storage= create_aux_memory_mapper("Blockoffsettable", G1BlockOffsetSharedArray::compute_size(g1_rs.size()/HeapWordSize), G1BlockOffsetSharedArray::N_bytes); ReservedSpacecardtable_rs(G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size()/HeapWordSize)); G1RegionToSpaceMapper*cardtable_storage= create_aux_memory_mapper("Cardtable", G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size()/HeapWordSize), G1BlockOffsetSharedArray::N_bytes); G1RegionToSpaceMapper*card_counts_storage= create_aux_memory_mapper("Cardcountstable", G1BlockOffsetSharedArray::compute_size(g1_rs.size()/HeapWordSize), G1BlockOffsetSharedArray::N_bytes); size_tbitmap_size=CMBitMap::compute_size(g1_rs.size()); G1RegionToSpaceMapper*prev_bitmap_storage= create_aux_memory_mapper("PrevBitmap",bitmap_size,CMBitMap::mark_distance()); G1RegionToSpaceMapper*next_bitmap_storage= create_aux_memory_mapper("NextBitmap",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); //Dolaterinitializationworkforconcurrentrefinement. _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 中不可能同時達到的三元悖論,不同的垃圾收集器在這三者中有不同的側重,我們應該結合自身的業務情況綜合考量選擇合適的垃圾收集器。
審核編輯:劉清
-
JAVA
+關注
關注
19文章
2958瀏覽量
104544 -
cms
+關注
關注
0文章
59瀏覽量
10956 -
JVM
+關注
關注
0文章
157瀏覽量
12207 -
NMT
+關注
關注
0文章
7瀏覽量
3626
原文標題:Native Memory Tracking 詳解(2):追蹤區域分析(一)
文章出處:【微信號:openEulercommunity,微信公眾號:openEuler】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論