0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

介紹NMT追蹤區(qū)域的部分內(nèi)存類型

openEuler ? 來(lái)源:畢昇編譯 ? 作者:竇義望 ? 2022-10-21 09:26 ? 次閱讀

上篇文章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ù)情況綜合考量選擇合適的垃圾收集器。




審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • JAVA
    +關(guān)注

    關(guān)注

    19

    文章

    2973

    瀏覽量

    104920
  • cms
    cms
    +關(guān)注

    關(guān)注

    0

    文章

    60

    瀏覽量

    10990
  • JVM
    JVM
    +關(guān)注

    關(guān)注

    0

    文章

    158

    瀏覽量

    12249
  • NMT
    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)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    CPU的內(nèi)存區(qū)域詳解

    CPU的內(nèi)存區(qū)域是全系統(tǒng)聲明的區(qū)域。由此,這些區(qū)域不必在邏輯塊中聲明。每個(gè)CPU均提供以下有各自地址范圍的內(nèi)存
    發(fā)表于 08-25 09:26 ?2754次閱讀

    單片機(jī)定義變量字符的那部分內(nèi)存已經(jīng)滿了,怎么才能繼續(xù)定義新的字符?

    我用的比較老的4580芯片,需要定義的字符很多,用于OLED顯示屏幕顯示,但是定義字符的那部分內(nèi)存已經(jīng)滿了,無(wú)法再加入新的字符,請(qǐng)問(wèn)有什么好的解決辦法嗎?請(qǐng)不要告訴我換芯片,因?yàn)殡娐钒迨歉鶕?jù)這個(gè)芯片制作的,有沒(méi)有什么好的容易的解決辦法?跪求
    發(fā)表于 03-06 17:15

    怎樣在Linux內(nèi)核中預(yù)留一部分內(nèi)存空間作特殊用途呢

    有時(shí)我們需要在 Linux 內(nèi)核中預(yù)留一部分內(nèi)存空間用作特殊用途(給安全模塊使用,給其它處理器使用,或是給特定的驅(qū)動(dòng)程序使用等),在 Device Tree 中有提供兩種方法對(duì)預(yù)留內(nèi)存進(jìn)行配置
    發(fā)表于 12-29 07:16

    Native Memory Tracking 詳解(2):追蹤區(qū)域分析(一)

    上篇文章 Native Memory Tracking 詳解(1):基礎(chǔ)介紹中,分享了如何使用NMT,以及NMT內(nèi)存 & OS內(nèi)存
    發(fā)表于 10-28 10:24

    Native Memory Tracking 詳解(1):基礎(chǔ)介紹

    打開 )。這就導(dǎo)致了如果我們沒(méi)有開啟過(guò) NMT ,那就沒(méi)辦法通過(guò)魔改 shutdown 操作逆向打開 NMT ,因?yàn)?NMT 追蹤部分內(nèi)存
    發(fā)表于 10-28 10:29

    Native Memory Tracking 詳解(4):使用 NMT 協(xié)助排查內(nèi)存問(wèn)題案例

    從前面幾篇文章,我們了解了 NMT 的基礎(chǔ)知識(shí)以及 NMT 追蹤區(qū)域分析的相關(guān)內(nèi)容,本篇文章將為大家介紹一下使用
    發(fā)表于 11-24 14:19

    支持內(nèi)存類型

    支持內(nèi)存類型            支持內(nèi)存類型是指主板所支持的具體內(nèi)存
    發(fā)表于 04-26 17:54 ?470次閱讀

    主板支持內(nèi)存類型有哪些?

    主板支持內(nèi)存類型有哪些? 支持內(nèi)存類型是指主板所支持的具體內(nèi)存類型。不同的主板所支持的
    發(fā)表于 12-24 14:48 ?1086次閱讀

    內(nèi)存的傳輸類型有哪些?

    內(nèi)存的傳輸類型有哪些?         傳輸類型內(nèi)存所采用的內(nèi)存
    發(fā)表于 12-25 13:37 ?1692次閱讀

    怎么區(qū)分內(nèi)存條的單面和雙面

    什么是單面和雙面內(nèi)存?它們有些什么樣的特性與區(qū)別呢?你們知道怎么區(qū)分內(nèi)存條單面和雙面嗎?下面小編帶來(lái)怎么區(qū)分內(nèi)存條單面和雙面的內(nèi)容,歡迎閱讀!
    發(fā)表于 05-28 09:51 ?9039次閱讀

    如何使用NMT和pmap來(lái)解決JVM的資源泄漏問(wèn)題

    編者按:筆者使用 JDK 自帶的內(nèi)存跟蹤工具 NMT 和 Linux 自帶的 pmap 解決了一個(gè)非常典型的資源泄漏問(wèn)題。這個(gè)資源泄漏是由于 Java 程序員不正確地使用 Java API 導(dǎo)致
    的頭像 發(fā)表于 09-24 16:00 ?3550次閱讀
    如何使用<b class='flag-5'>NMT</b>和pmap來(lái)解決JVM的資源泄漏問(wèn)題

    介紹追蹤區(qū)域的其它內(nèi)存類型以及NMT無(wú)法追蹤內(nèi)存

    Compiler 就是 JIT 編譯器線程在編譯 code 時(shí)本身所使用的內(nèi)存
    的頭像 發(fā)表于 11-01 10:20 ?1073次閱讀

    Linux如何優(yōu)化部分內(nèi)存

    將kernel往前挪,從而利用前面的內(nèi)存。 修改代碼 路徑: arch/riscv/mm/init.c 注釋原來(lái)的2M對(duì)齊檢查: 對(duì)kernel的前2M頁(yè)表映射由二級(jí)頁(yè)表改為三級(jí)頁(yè)表: //新增一個(gè)
    的頭像 發(fā)表于 10-08 10:59 ?475次閱讀
    Linux如何優(yōu)化<b class='flag-5'>部分內(nèi)存</b>

    jvm運(yùn)行時(shí)內(nèi)存區(qū)域劃分

    內(nèi)存區(qū)域劃分對(duì)于了解Java程序的內(nèi)存使用非常重要,本文將詳細(xì)介紹JVM運(yùn)行時(shí)的內(nèi)存區(qū)域劃分。
    的頭像 發(fā)表于 12-05 14:08 ?556次閱讀

    jvm內(nèi)存區(qū)域由哪幾部分組成

    。下面將詳細(xì)介紹這些部分。 堆內(nèi)存(Heap Memory):堆內(nèi)存是JVM中最大的一塊內(nèi)存區(qū)域
    的頭像 發(fā)表于 12-05 14:10 ?850次閱讀