上篇文章Native Memory Tracking 詳解(1):基礎(chǔ)介紹中,分享了如何使用NMT,以及NMT內(nèi)存 & OS內(nèi)存概念的差異性,本篇將介紹NMT追蹤區(qū)域的部分內(nèi)存類型——Java heap、Class、Thread、Code 以及 GC。
4.追蹤區(qū)域內(nèi)存類型
在上文中我們打印了 NMT 的相關(guān)報(bào)告,但想必大家初次看到報(bào)告的時(shí)候?qū)ζ渥粉櫟母鱾€(gè)區(qū)域往往都是一頭霧水,下面就讓我們來(lái)簡(jiǎn)單認(rèn)識(shí)下各個(gè)區(qū)域。
查看 JVM 中所設(shè)定的內(nèi)存類型:
#hotspot/src/share/vm/memory/allocation.hpp /* *Memorytypes */ enumMemoryType{ //Memorytypebysubsystems.Itoccupieslowerbyte. mtJavaHeap=0x00,//Javaheap//Java堆 mtClass=0x01,//memoryclassforJavaclasses//Javaclasses使用的內(nèi)存 mtThread=0x02,//memoryforthreadobjects//線程對(duì)象使用的內(nèi)存 mtThreadStack=0x03, mtCode=0x04,//memoryforgeneratedcode//編譯生成代碼使用的內(nèi)存 mtGC=0x05,//memoryforGC//GC使用的內(nèi)存 mtCompiler=0x06,//memoryforcompiler//編譯器使用的內(nèi)存 mtInternal=0x07,//memoryusedbyVM,butdoesnotbelongto//內(nèi)部使用的類型 //anyofabovecategories,andnotusedfor //nativememorytracking mtOther=0x08,//memorynotusedbyVM//不是VM使用的內(nèi)存 mtSymbol=0x09,//symbol//符號(hào)表使用的內(nèi)存 mtNMT=0x0A,//memoryusedbynativememorytracking//NMT自身使用的內(nèi)存 mtClassShared=0x0B,//classdatasharing//共享類使用的內(nèi)存 mtChunk=0x0C,//chunkthatholdscontentofarenas//chunk用于緩存 mtTest=0x0D,//TesttypeforverifyingNMT mtTracing=0x0E,//memoryusedforTracing mtNone=0x0F,//undefined mt_number_of_types=0x10//numberofmemorytypes(mtDontTrack //isnotincludedasvalidatetype) };
除去這上面的部分選項(xiàng),我們發(fā)現(xiàn) NMT 中還有一個(gè) unknown 選項(xiàng),這主要是在執(zhí)行 jcmd 命令時(shí),內(nèi)存類別還無(wú)法確定或虛擬類型信息還沒(méi)有到達(dá)時(shí)的一些內(nèi)存統(tǒng)計(jì)。
4.1 Java heap
[0x00000000c0000000-0x0000000100000000]reserved1048576KBforJavaHeapfrom [0x0000ffff93ea36d8]ReservedHeapSpace::ReservedHeapSpace(unsignedlong,unsignedlong,bool,char*)+0xb8//reserve內(nèi)存的callsites ...... [0x00000000c0000000-0x0000000100000000]committed1048576KBfrom [0x0000ffff938bbe8c]G1PageBasedVirtualSpace::commit_internal(unsignedlong,unsignedlong)+0x14c//commit內(nèi)存的callsites ......
無(wú)需多言,Java 堆使用的內(nèi)存,絕大多數(shù)情況下都是 JVM 使用內(nèi)存的主力,堆內(nèi)存通過(guò) mmap 的方式申請(qǐng)。0x00000000c0000000 - 0x0000000100000000 即是 Java Heap 的虛擬地址范圍,因?yàn)榇藭r(shí)使用的是 G1 垃圾收集器(不是物理意義上的分代),所以無(wú)法看到分代地址,如果使用其他物理分代的收集器(如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 等參數(shù)來(lái)控制初始/最大的大小,其中基于低停頓的考慮可將兩值設(shè)置相等以避免動(dòng)態(tài)擴(kuò)容縮容帶來(lái)的時(shí)間開銷(如果基于彈性節(jié)約內(nèi)存資源則不必)。
可以如上文所述開啟 -XX:+AlwaysPreTouch 參數(shù)強(qiáng)制分配物理內(nèi)存來(lái)減少運(yùn)行時(shí)的停頓(如果想要快速啟動(dòng)進(jìn)程則不必)。
基于節(jié)省內(nèi)存資源還可以啟用 uncommit 機(jī)制等。
4.2 Class
Class 主要是類元數(shù)據(jù)(meta data)所使用的內(nèi)存空間,即虛擬機(jī)規(guī)范中規(guī)定的方法區(qū)。具體到 HotSpot 的實(shí)現(xiàn)中,JDK7 之前是實(shí)現(xiàn)在 PermGen 永久代中,JDK8 之后則是移除了 PermGen 變成了 MetaSpace 元空間。
當(dāng)然以前 PermGen 還有 Interned strings 或者說(shuō) StringTable(即字符串常量池),但是 MetaSpace 并不包含 StringTable,在 JDK8 之后 StringTable 就被移入 Heap,并且在 NMT 中 StringTable 所使用的內(nèi)存被單獨(dú)統(tǒng)計(jì)到了 Symbol 中。
既然 Class 所使用的內(nèi)存用來(lái)存放元數(shù)據(jù),那么想必在啟動(dòng) JVM 進(jìn)程的時(shí)候設(shè)置的 -XX:MaxMetaspaceSize=256M 參數(shù)可以限制住 Class 所使用的內(nèi)存大小。
但是我們?cè)诓榭?NMT 詳情發(fā)現(xiàn)一個(gè)奇怪的現(xiàn)象:
Class(reserved=1056899KB,committed=4995KB) (classes#442)//加載的類的數(shù)目 (malloc=131KB#259) (mmap:reserved=1056768KB,committed=4864KB)
Class 竟然 reserved 了 1056899KB(約 1G ) 的內(nèi)存,這貌似和我們?cè)O(shè)定的(256M)不太一樣。
此時(shí)我們就不得不簡(jiǎn)單補(bǔ)充下相關(guān)的內(nèi)容,我們都知道 JVM 中有一個(gè)參數(shù):-XX:UseCompressedOops (簡(jiǎn)單來(lái)說(shuō)就是在一定情況下開啟指針壓縮來(lái)提升性能),該參數(shù)在非 64 位和手動(dòng)設(shè)定 -XX:-UseCompressedOops 的情況下是不會(huì)開啟的,而只有在64位系統(tǒng)、不是 client VM、并且 max_heap_size <= max_heap_for_compressed_oops(一個(gè)近似32GB的數(shù)值)的情況下會(huì)默認(rèn)開啟(計(jì)算邏輯可以查看 hotspot/src/share/vm/runtime/arguments.cpp 中的 Arguments::set_use_compressed_oops() 方法)。
而如果 -XX:UseCompressedOops 被開啟,并且我們沒(méi)有手動(dòng)設(shè)置過(guò) -XX:-UseCompressedClassPointers 的話,JVM 會(huì)默認(rèn)幫我們開啟 UseCompressedClassPointers(詳情可查看 hotspot/src/share/vm/runtime/arguments.cpp 中的 Arguments::set_use_compressed_klass_ptrs() 方法)。
我們先忽略 UseCompressedOops 不提,在 UseCompressedClassPointers 被啟動(dòng)之后,_metadata 的指針就會(huì)由 64 位的 Klass 壓縮為 32 位無(wú)符號(hào)整數(shù)值 narrowKlass。簡(jiǎn)單看下指向關(guān)系:
JavaobjectInstanceKlass [_mark] [_klass/_narrowKlass]-->[...] [fields][_java_mirror] [...] (heap)(MetaSpace)
如果我們用的是未壓縮過(guò)的 _klass ,那么使用 64 位指針尋址,因此 Klass 可以放置在任意位置;但是如果我們使用壓縮過(guò)的 narrowKlass (32位) 進(jìn)行尋址,那么為了找到該結(jié)構(gòu)實(shí)際的 64 位地址,我們不光需要位移操作(如果以 8 字節(jié)對(duì)齊左移 3 位),還需要設(shè)置一個(gè)已知的公共基址,因此限制了我們需要為 Klass 分配為一個(gè)連續(xù)的內(nèi)存區(qū)域。
所以整個(gè) MetaSpace 的內(nèi)存結(jié)構(gòu)在是否開啟 UseCompressedClassPointers 時(shí)是不同的:
如果未開啟指針壓縮,那么 MetaSpace 只有一個(gè) Metaspace Context(incl chunk freelist) 指向很多不同的 virtual space;
如果開啟了指針壓縮,Klass 和非 Klass 部分分開存放,Klass 部分放一個(gè)連續(xù)的內(nèi)存區(qū)域 Metaspace Context(class) (指向一塊大的連續(xù)的 virtual space),非 Klass 部分則依照未開啟壓縮的模式放在很多不同的 virtual space 中。這塊 Metaspace Context(class) 內(nèi)存,就是傳說(shuō)中的 CompressedClassSpaceSize 所設(shè)置的內(nèi)存。
//未開啟壓縮 +--------++--------++--------++--------+ |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相關(guān)內(nèi)容就不再展開描述了,詳情可以參考官方文檔 Metaspace - Metaspace - OpenJDK Wiki (java.net)[1] 與 Thomas Stüfe 的系列文章 What is Metaspace? | stuefe.de [2]。
我們查看 reserve 的具體日志,發(fā)現(xiàn)大部分的內(nèi)存都是 Metaspace::allocate_metaspace_compressed_klass_ptrs 方法申請(qǐng)的,這正是用來(lái)分配 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 時(shí),調(diào)用鏈路如下:
InitializeJVM ->
Thread::vreate_vm ->
init_globals ->
universe_init ->
MetaSpace::global_initalize ->
Metaspace::allocate_metaspace_compressed_klass_ptrs
查看相關(guān)源碼:
#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); ...... }
我們可以發(fā)現(xiàn)如果開啟了 UseCompressedClassPointers ,那么就會(huì)調(diào)用 allocate_metaspace_compressed_klass_ptrs 方法去 reserve 一個(gè) compressed_class_space_size() 大小的空間(由于我們沒(méi)有顯式地設(shè)置過(guò) -XX:CompressedClassSpaceSize 的大小,所以此時(shí)默認(rèn)值為 1G)。如果我們顯式地設(shè)置 -XX:CompressedClassSpaceSize=256M 再重啟 JVM ,就會(huì)發(fā)現(xiàn) reserve 的內(nèi)存大小已經(jīng)被限制住了:
Thread(reserved=258568KB,committed=258568KB) (thread#127) (stack:reserved=258048KB,committed=258048KB) (malloc=390KB#711) (arena=130KB#234)
但是此時(shí)我們不禁會(huì)有一個(gè)疑問(wèn),那就是既然 CompressedClassSpaceSize 可以 reverse 遠(yuǎn)遠(yuǎn)超過(guò) -XX:MaxMetaspaceSize 設(shè)置的大小,那么 -XX:MaxMetaspaceSize 會(huì)不會(huì)無(wú)法限制住整體 MetaSpace 的大?。繉?shí)際上 -XX:MaxMetaspaceSize 是可以限制住 MetaSpace 的大小的,只是 HotSpot 此處的代碼順序有問(wèn)題容易給大家造成誤解和歧義~
查看相關(guān)代碼:
#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); } } } ...... }
我們可以發(fā)現(xiàn)如果 min_metaspace_sz + CompressedClassSpaceSize > MaxMetaspaceSize 的話,JVM 會(huì)將 CompressedClassSpaceSize 的值設(shè)置為 MaxMetaspaceSize - min_metaspace_sz 的大小,即最后 CompressedClassSpaceSize 的值是小于 MaxMetaspaceSize 的大小的,但是為何之前會(huì) reserve 一個(gè)大的值呢?因?yàn)樵谥匦掠?jì)算 CompressedClassSpaceSize 的值之前,JVM 就先調(diào)用了 set_compressed_class_space_size 方法將 compressed_class_space_size 的大小設(shè)置成了未重新計(jì)算的、默認(rèn)的 CompressedClassSpaceSize 的大小。
還記得 compressed_class_space_size 嗎?沒(méi)錯(cuò),正是我們?cè)谏厦嬲{(diào)用 allocate_metaspace_compressed_klass_ptrs 方法時(shí) reserve 的大小,所以此時(shí) reserve 的其實(shí)是一個(gè)不正確的值,我們只需要將set_compressed_class_space_size 的操作放在重新計(jì)算 CompressedClassSpaceSize 大小的邏輯之后就能修正這種錯(cuò)誤。當(dāng)然因?yàn)槭?reserve 的內(nèi)存,對(duì)真正運(yùn)行起來(lái)的 JVM 并無(wú)太大的負(fù)面影響,所以沒(méi)有人給社區(qū)報(bào)過(guò)這個(gè)問(wèn)題,社區(qū)也沒(méi)有修改過(guò)這一塊邏輯。
如果你使用的 JDK 版本大于等于 10,那么你直接可以通過(guò) NMT 看到更詳細(xì)劃分的 Class 信息(區(qū)分了存放 klass 的區(qū)域即 Class space、存放非 klass 的區(qū)域即 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
線程所使用的內(nèi)存:
Thread(reserved=258568KB,committed=258568KB) (thread#127)//線程個(gè)數(shù) (stack:reserved=258048KB,committed=258048KB)//棧使用的內(nèi)存 (malloc=390KB#711) (arena=130KB#234)//線程句柄使用的內(nèi)存 ...... [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 打印信息,我們可以發(fā)現(xiàn),此時(shí)的 JVM 進(jìn)程共使用了127個(gè)線程,committed 了 258568KB 的內(nèi)存。
繼續(xù)觀察下面各個(gè)線程的分配情況就會(huì)發(fā)現(xiàn),每個(gè)線程 committed 了2048KB(2M)的內(nèi)存空間,這可能和平時(shí)的認(rèn)知不太相同,因?yàn)槠綍r(shí)我們大多數(shù)情況下使用的都是x86平臺(tái),而筆者此時(shí)使用的是 ARM (aarch64)的平臺(tái),所以此處線程默認(rèn)分配的內(nèi)存與 x86 不同。
如果我們不顯式的設(shè)置 -Xss/-XX:ThreadStackSize 相關(guān)的參數(shù),那么 JVM 會(huì)使用默認(rèn)的值。
在 aarch64 平臺(tái)下默認(rèn)為 2M:
#globals_linux_aarch64.hpp define_pd_global(intx,ThreadStackSize,2048);//0=>usesystemdefault define_pd_global(intx,VMThreadStackSize,2048);
而在 x86 平臺(tái)下默認(rèn)為 1M:
#globals_linux_x86.hpp define_pd_global(intx,ThreadStackSize,1024);//0=>usesystemdefault define_pd_global(intx,VMThreadStackSize,1024);
如果我們想縮減此部分內(nèi)存的使用,可以使用參數(shù) -Xss/-XX:ThreadStackSize 設(shè)置適合自身業(yè)務(wù)情況的大小,但是需要進(jìn)行相關(guān)壓力測(cè)試保證不會(huì)出現(xiàn)溢出等錯(cuò)誤。
4.4 Code
JVM 自身會(huì)生成一些 native code 并將其存儲(chǔ)在稱為 codecache 的內(nèi)存區(qū)域中。JVM 生成 native code 的原因有很多,包括動(dòng)態(tài)生成的解釋器循環(huán)、 JNI、即時(shí)編譯器(JIT)編譯 Java 方法生成的本機(jī)代碼 。其中 JIT 生成的 native code 占據(jù)了 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的函數(shù),其中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); }
可以發(fā)現(xiàn) CodeCache::initialize() 時(shí) codecache reserve 的最大內(nèi)存是由我們?cè)O(shè)置的 -XX:ReservedCodeCacheSize 參數(shù)決定的(當(dāng)然 ReservedCodeCacheSize 的值會(huì)做一些對(duì)齊操作),我們可以通過(guò)設(shè)置 -XX:ReservedCodeCacheSize 來(lái)限制 Code 相關(guān)的最大內(nèi)存。
同時(shí)我們發(fā)現(xiàn),初始化時(shí) codecache commit 的內(nèi)存可以由 -XX:InitialCodeCacheSize 參數(shù)來(lái)控制,具體計(jì)算代碼可以查看 VirtualSpace::expand_by 函數(shù)。
我們?cè)O(shè)置 -XX:InitialCodeCacheSize=128M 后重啟 JVM 進(jìn)程,再次查看 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
我們可以通過(guò) -XX:InitialCodeCacheSize 來(lái)設(shè)置 codecache 初始 commit 的內(nèi)存。
除了使用 NMT 打印 codecache 相關(guān)信息,我們還可以通過(guò) -XX:+PrintCodeCache (JVM 關(guān)閉時(shí)輸出codecache的使用情況)和 jcmd pid Compiler.codecache(只有在 JDK 9 及以上版本的 jcmd 才支持該選項(xiàng))來(lái)查看 codecache 相關(guān)的信息。
了解更多 codecache 詳情可以查看 CodeCache 官方文檔[3]。
4.5 GC
GC 所使用的內(nèi)存,就是垃圾收集器使用的數(shù)據(jù)所占據(jù)的內(nèi)存,例如卡表 card tables、記憶集 remembered sets、標(biāo)記棧 marking stack、標(biāo)記位圖 marking bitmaps 等等。其實(shí)不論是 card tables、remembered sets 還是 marking stack、marking bitmaps,都是一種借助額外的空間,來(lái)記錄不同內(nèi)存區(qū)域之間引用關(guān)系的結(jié)構(gòu)(都是基于空間換時(shí)間的思想,否則尋找引用關(guān)系就需要諸如遍歷這種浪費(fèi)時(shí)間的方式)。
簡(jiǎn)單介紹下相關(guān)概念:
更詳細(xì)的信息不深入展開介紹了,可以查看彭成寒老師《JVM G1源碼分析和調(diào)優(yōu)》2.3 章 [4] 與 4.1 章節(jié) [5],還可以查看 R大(RednaxelaFX)對(duì)相關(guān)概念的科普 [6]。
卡表 card tables,在部分收集器(如CMS)中存儲(chǔ)跨代引用(如老年代中對(duì)象指向年輕代的對(duì)象)的數(shù)據(jù)結(jié)構(gòu),精度可以有很多種選擇:
如果精確到機(jī)器字,那么往往描述的區(qū)域太小了,使用的內(nèi)存開銷會(huì)變大,所以 HotSpot 中選擇 512KB 為精度大小。
卡表甚至可以細(xì)到和 bitmap 相同,即使用 1 bit 位來(lái)對(duì)應(yīng)一個(gè)內(nèi)存頁(yè)(512KB),但是因?yàn)?JVM 在操作一個(gè) bit 位時(shí),仍然需要讀取整個(gè)機(jī)器字 word,并且操作 bit 位的開銷有時(shí)反而大于操作 byte 。所以 HotSpot 的 cardTable 選擇使用 byte 數(shù)組代替 bit ,用 1 byte 對(duì)應(yīng) 512KB 的空間,使用 byte 數(shù)組的開銷也可以接受(1G 的堆內(nèi)存使用卡表也只占用2M:1 * 1024 * 1024 / 512 = 2048 KB)。
我們以 cardTableModRefBS 為例,查看其源碼結(jié)構(gòu):
#hotspor/src/share/vm/momery/cardTableModRefBS.hpp //精度為512KB enumSomePublicConstants{ card_shift=9, card_size=1<
可以發(fā)現(xiàn) cardTableModRefBS 通過(guò)枚舉 SomePublicConstants 來(lái)定義對(duì)應(yīng)的內(nèi)存塊 card_size 的大小即:512KB,而 _byte_map 則是用于標(biāo)記的卡表字節(jié)數(shù)組,我們可以看到其對(duì)應(yīng)的類型為 jbyte(typedef signed char jbyte,其實(shí)就是一個(gè)字節(jié)即 1byte)。
當(dāng)然后來(lái)卡表不只記錄跨代引用的關(guān)系,還會(huì)被 CMS 的增量更新之類的操作復(fù)用。
字粒度:精確到機(jī)器字(word),該字包含有跨代指針。
對(duì)象粒度:精確到一個(gè)對(duì)象,該對(duì)象里有字段含有跨代指針。
card粒度:精確到一大塊內(nèi)存區(qū)域,該區(qū)域內(nèi)有對(duì)象含有跨代指針。
記憶集 remembered sets,可以選擇的粒度和卡表差不多,或者你說(shuō)卡表也是記憶集的一種實(shí)現(xiàn)方式也可以(區(qū)別可以查看上面給出的 R大的鏈接)。G1 中引入記憶集 RSet 來(lái)記錄 Region 間的跨代引用,G1 中的卡表的作用并不是記錄引用關(guān)系,而是用于記錄該區(qū)域中對(duì)象垃圾回收過(guò)程中的狀態(tài)信息。
標(biāo)記棧 marking stack,初始標(biāo)記掃描根集合時(shí),會(huì)標(biāo)記所有從根集合可直接到達(dá)的對(duì)象并將它們的字段壓入掃描棧(marking stack)中等待后續(xù)掃描。
標(biāo)記位圖 marking bitmaps,我們常使用位圖來(lái)指示哪塊內(nèi)存已經(jīng)使用、哪塊內(nèi)存還未使用。比如 G1 中的 Mixed GC 混合收集算法(收集所有的年輕代的 Region,外加根據(jù)global concurrent marking 統(tǒng)計(jì)得出的收集收益高的部分老年代 Region)中用到了并發(fā)標(biāo)記,并發(fā)標(biāo)記就引入兩個(gè)位圖 PrevBitMap 和 NextBitMap,用這兩個(gè)位圖來(lái)輔助標(biāo)記并發(fā)標(biāo)記不同階段內(nèi)存的使用狀態(tài)。
查看 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 ......
我們可以發(fā)現(xiàn) JVM 在初始化 heap 堆的時(shí)候(此時(shí)是 G1 收集器所使用的堆 G1CollectedHeap),不僅會(huì)創(chuàng)建 remember set ,還會(huì)有一個(gè) create_aux_memory_mapper 的操作,用來(lái)給 GC 輔助用的數(shù)據(jù)結(jié)構(gòu)(如:card table、prev bitmap、 next bitmap 等)創(chuàng)建對(duì)應(yīng)的內(nèi)存映射,相關(guān)操作可以查看 g1CollectedHeap 初始化部分源代碼:
#hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp jintG1CollectedHeap::initialize(){ ...... //創(chuàng)建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); ...... }
因?yàn)檫@些輔助的結(jié)構(gòu)都是一種空間換時(shí)間的思想,所以不可避免的會(huì)占用額外的內(nèi)存,尤其是 G1 的 RSet 結(jié)構(gòu),當(dāng)我們調(diào)大我們的堆內(nèi)存,GC 所使用的內(nèi)存也會(huì)不可避免的跟隨增長(zhǎng):
#-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)
我們可以看到這個(gè)額外的內(nèi)存開銷一般在 1% - 20%之間,當(dāng)然如果我們不使用 G1 收集器,這個(gè)開銷是沒(méi)有那么大的:
#-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)
我們可以看到,使用最輕量級(jí)的 UseSerialGC,GC 部分占用的內(nèi)存有很明顯的降低(436M -> 26.67M);使用 CMS ,GC 部分從 436M 降低到 163.39M。
GC 這塊內(nèi)存是必須的,也是我們?cè)谑褂眠^(guò)程中無(wú)法壓縮的。停頓、吞吐量、內(nèi)存占用就是 GC 中不可能同時(shí)達(dá)到的三元悖論,不同的垃圾收集器在這三者中有不同的側(cè)重,我們應(yīng)該結(jié)合自身的業(yè)務(wù)情況綜合考量選擇合適的垃圾收集器。
審核編輯:劉清
-
JAVA
+關(guān)注
關(guān)注
19文章
2973瀏覽量
104920 -
cms
+關(guān)注
關(guān)注
0文章
60瀏覽量
10990 -
JVM
+關(guān)注
關(guān)注
0文章
158瀏覽量
12249 -
NMT
+關(guān)注
關(guān)注
0文章
7瀏覽量
3649
原文標(biāo)題:Native Memory Tracking 詳解(2):追蹤區(qū)域分析(一)
文章出處:【微信號(hào):openEulercommunity,微信公眾號(hào):openEuler】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論