0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

介绍NMT追踪区域的部分内存类型

openEuler 来源:毕昇编译 作者:窦义望 2022-10-21 09:26 次阅读

上篇文章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<< 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]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

    文章

    2904

    浏览量

    103004
  • cms
    cms
    +关注

    关注

    0

    文章

    57

    浏览量

    10875
  • JVM
    JVM
    +关注

    关注

    0

    文章

    152

    浏览量

    12130
  • NMT
    NMT
    +关注

    关注

    0

    文章

    7

    浏览量

    3600

原文标题:Native Memory Tracking 详解(2):追踪区域分析(一)

文章出处:【微信号:openEulercommunity,微信公众号:openEuler】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    CPU的内存区域详解

    CPU的内存区域是全系统声明的区域。由此,这些区域不必在逻辑块中声明。每个CPU均提供以下有各自地址范围的内存
    发表于 08-25 09:26 2014次阅读

    单片机定义变量字符的那部分内存已经满了,怎么才能继续定义新的字符?

    我用的比较老的4580芯片,需要定义的字符很多,用于OLED显示屏幕显示,但是定义字符的那部分内存已经满了,无法再加入新的字符,请问有什么好的解决办法吗?请不要告诉我换芯片,因为电路板是根据这个芯片制作的,有没有什么好的容易的解决办法?跪求
    发表于 03-06 17:15

    怎样在Linux内核中预留一部分内存空间作特殊用途呢

    有时我们需要在 Linux 内核中预留一部分内存空间用作特殊用途(给安全模块使用,给其它处理器使用,或是给特定的驱动程序使用等),在 Device Tree 中有提供两种方法对预留内存进行配置
    发表于 12-29 07:16

    Native Memory Tracking 详解(2):追踪区域分析(一)

    上篇文章 Native Memory Tracking 详解(1):基础介绍中,分享了如何使用NMT,以及NMT内存 & OS内存
    发表于 10-28 10:24

    Native Memory Tracking 详解(1):基础介绍

    打开 )。这就导致了如果我们没有开启过 NMT ,那就没办法通过魔改 shutdown 操作逆向打开 NMT ,因为 NMT 追踪部分内存
    发表于 10-28 10:29

    Native Memory Tracking 详解(4):使用 NMT 协助排查内存问题案例

    从前面几篇文章,我们了解了 NMT 的基础知识以及 NMT 追踪区域分析的相关内容,本篇文章将为大家介绍一下使用
    发表于 11-24 14:19

    主板支持内存类型有哪些?

    主板支持内存类型有哪些? 支持内存类型是指主板所支持的具体内存类型。不同的主板所支持的
    发表于 12-24 14:48 997次阅读

    内存的传输类型有哪些?

    内存的传输类型有哪些?         传输类型内存所采用的内存
    发表于 12-25 13:37 1647次阅读

    怎么区分内存条的单面和双面

    什么是单面和双面内存?它们有些什么样的特性与区别呢?你们知道怎么区分内存条单面和双面吗?下面小编带来怎么区分内存条单面和双面的内容,欢迎阅读!
    发表于 05-28 09:51 7616次阅读

    如何使用NMT和pmap来解决JVM的资源泄漏问题

    编者按:笔者使用 JDK 自带的内存跟踪工具 NMT 和 Linux 自带的 pmap 解决了一个非常典型的资源泄漏问题。这个资源泄漏是由于 Java 程序员不正确地使用 Java API 导致
    的头像 发表于 09-24 16:00 2805次阅读
    如何使用<b class='flag-5'>NMT</b>和pmap来解决JVM的资源泄漏问题

    介绍追踪区域的其它内存类型以及NMT无法追踪内存

    Compiler 就是 JIT 编译器线程在编译 code 时本身所使用的内存
    的头像 发表于 11-01 10:20 772次阅读

    恒讯科技分析:区域网络的类型有哪些?

    覆盖的区域比LAN大,WAN覆盖的区域最大。下面,小编给大家详细介绍下LAN、MAN和WAN这三种区域网络的类型。 一、局域网(LAN) 
    的头像 发表于 06-06 17:27 637次阅读

    jvm运行时内存区域划分

    内存区域划分对于了解Java程序的内存使用非常重要,本文将详细介绍JVM运行时的内存区域划分。
    的头像 发表于 12-05 14:08 261次阅读

    jvm内存区域由哪几部分组成

    方法栈。下面将详细介绍这些部分。 堆内存(Heap Memory):堆内存是JVM中最大的一块内存区域
    的头像 发表于 12-05 14:10 300次阅读

    jvm内存区域中,哪一块是属于线程共享

    JVM(Java虚拟机)是一种计算机软件,用于执行Java字节码。在JVM中,存在多个内存区域,包括线程共享的内存区域。本文将详细介绍JVM
    的头像 发表于 12-05 14:14 576次阅读