摘要:本篇将介绍NMT追踪区域的部分内存类型——Java heap、Class、Thread、Code 以及 GC。

本文分享自华为云社区《Native Memory Tracking 详解(2):追踪区域分析(一)》,作者:毕昇小助手。

本篇将介绍NMT追踪区域的部分内存类型——Java heap、Class、Thread、Code 以及 GC。

追踪区域内存类型

在上文中我们打印了 NMT 的相关报告,但想必大家初次看到报告的时候对其追踪的各个区域往往都是一头雾水,下面就让我们来简单认识下各个区域。

查看 JVM 中所设定的内存类型:

  1. # hotspot/src/share/vm/memory/allocation.hpp
  2. /*
  3. * Memory types
  4. */
  5. enum MemoryType {
  6. // Memory type by sub systems. It occupies lower byte.
  7. mtJavaHeap = 0x00, // Java heap //Java 堆
  8. mtClass = 0x01, // memory class for Java classes //Java classes 使用的内存
  9. mtThread = 0x02, // memory for thread objects //线程对象使用的内存
  10. mtThreadStack = 0x03,
  11. mtCode = 0x04, // memory for generated code //编译生成代码使用的内存
  12. mtGC = 0x05, // memory for GC //GC 使用的内存
  13. mtCompiler = 0x06, // memory for compiler //编译器使用的内存
  14. mtInternal = 0x07, // memory used by VM, but does not belong to //内部使用的类型
  15. // any of above categories, and not used for
  16. // native memory tracking
  17. mtOther = 0x08, // memory not used by VM //不是 VM 使用的内存
  18. mtSymbol = 0x09, // symbol //符号表使用的内存
  19. mtNMT = 0x0A, // memory used by native memory tracking //NMT 自身使用的内存
  20. mtClassShared = 0x0B, // class data sharing //共享类使用的内存
  21. mtChunk = 0x0C, // chunk that holds content of arenas //chunk用于缓存
  22. mtTest = 0x0D, // Test type for verifying NMT
  23. mtTracing = 0x0E, // memory used for Tracing
  24. mtNone = 0x0F, // undefined
  25. mt_number_of_types = 0x10 // number of memory types (mtDontTrack
  26. // is not included as validate type)
  27. };

除去这上面的部分选项,我们发现 NMT 中还有一个 unknown 选项,这主要是在执行 jcmd 命令时,内存类别还无法确定或虚拟类型信息还没有到达时的一些内存统计。

Java heap

  1. [0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from
  2. [0x0000ffff93ea36d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 //reserve 内存的 call sites
  3. ......
  4. [0x00000000c0000000 - 0x0000000100000000] committed 1048576KB from
  5. [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c //commit 内存的 call sites
  6. ......

无需多言,Java 堆使用的内存,绝大多数情况下都是 JVM 使用内存的主力,堆内存通过 mmap 的方式申请。0x00000000c0000000 - 0x0000000100000000 即是 Java Heap 的虚拟地址范围,因为此时使用的是 G1 垃圾收集器(不是物理意义上的分代),所以无法看到分代地址,如果使用其他物理分代的收集器(如CMS):

  1. [0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from
  2. [0x0000ffffa5cc76d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8
  3. [0x0000ffffa5c8bf68] Universe::reserve_heap(unsigned long, unsigned long)+0x2d0
  4. [0x0000ffffa570fa10] GenCollectedHeap::allocate(unsigned long, unsigned long*, int*, ReservedSpace*)+0x160
  5. [0x0000ffffa5711fdc] GenCollectedHeap::initialize()+0x104
  6. [0x00000000d5550000 - 0x0000000100000000] committed 699072KB from
  7. [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224
  8. [0x0000ffffa572a450] CardGeneration::CardGeneration(ReservedSpace, unsigned long, int, GenRemSet*)+0xb8
  9. [0x0000ffffa55dc85c] ConcurrentMarkSweepGeneration::ConcurrentMarkSweepGeneration(ReservedSpace, unsigned long, int, CardTableRS*, bool, FreeBlockDictionary::DictionaryChoice)+0x54
  10. [0x0000ffffa572bcdc] GenerationSpec::init(ReservedSpace, int, GenRemSet*)+0xe4
  11. [0x00000000c0000000 - 0x00000000d5550000] committed 349504KB from
  12. [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224
  13. [0x0000ffffa5729fe0] Generation::Generation(ReservedSpace, unsigned long, int)+0x98
  14. [0x0000ffffa5612fa8] DefNewGeneration::DefNewGeneration(ReservedSpace, unsigned long, int, char const*)+0x58
  15. [0x0000ffffa5b05ec8] ParNewGeneration::ParNewGeneration(ReservedSpace, unsigned long, int)+0x60

我们可以清楚地看到 0x00000000c0000000 - 0x00000000d5550000 为 Java Heap 的新生代(DefNewGeneration)的范围,0x00000000d5550000 - 0x0000000100000000 为 Java Heap 的老年代(ConcurrentMarkSweepGeneration)的范围。

  • 我们可以使用 -Xms/-Xmx 或 -XX:InitialHeapSize/-XX:MaxHeapSize 等参数来控制初始/最大的大小,其中基于低停顿的考虑可将两值设置相等以避免动态扩容缩容带来的时间开销(如果基于弹性节约内存资源则不必)。
  • 可以如上文所述开启 -XX:+AlwaysPreTouch 参数强制分配物理内存来减少运行时的停顿(如果想要快速启动进程则不必)。
  • 基于节省内存资源还可以启用 uncommit 机制等。

Class

Class 主要是类元数据(meta data)所使用的内存空间,即虚拟机规范中规定的方法区。具体到 HotSpot 的实现中,JDK7 之前是实现在 PermGen 永久代中,JDK8 之后则是移除了 PermGen 变成了 MetaSpace 元空间。

当然以前 PermGen 还有 Interned strings 或者说 StringTable(即字符串常量池),但是 MetaSpace 并不包含 StringTable,在 JDK8 之后 StringTable 就被移入 Heap,并且在 NMT 中 StringTable 所使用的内存被单独统计到了 Symbol 中。

既然 Class 所使用的内存用来存放元数据,那么想必在启动 JVM 进程的时候设置的 -XX:MaxMetaspaceSize=256M 参数可以限制住 Class 所使用的内存大小。

但是我们在查看 NMT 详情发现一个奇怪的现象:

  1. Class (reserved=1056899KB, committed=4995KB)
  2. (classes #442) //加载的类的数目
  3. (malloc=131KB #259)
  4. (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。简单看下指向关系:

  1. Java object InstanceKlass
  2. [ _mark ]
  3. [ _klass/_narrowKlass ] --> [ ... ]
  4. [ fields ] [ _java_mirror ]
  5. [ ... ]
  6. 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 所设置的内存。
  1. //未开启压缩
  2. +--------+ +--------+ +--------+ +--------+
  3. | CLD | | CLD | | CLD | | CLD |
  4. +--------+ +--------+ +--------+ +--------+
  5. | | | |
  6. | | | | allocates variable-sized,
  7. | | | | typically small-tiny metaspace blocks
  8. v v v v
  9. +--------+ +--------+ +--------+ +--------+
  10. | arena | | arena | | arena | | arena |
  11. +--------+ +--------+ +--------+ +--------+
  12. | | | |
  13. | | | | allocate and, on death, release-in-bulk
  14. | | | | medium-sized chunks (1k..4m)
  15. | | | |
  16. v v v v
  17. +--------------------------------------------+
  18. | |
  19. | Metaspace Context |
  20. | (incl chunk freelist) |
  21. | |
  22. +--------------------------------------------+
  23. | | |
  24. | | | map/commit/uncommit/release
  25. | | |
  26. v v v
  27. +---------+ +---------+ +---------+
  28. | | | | | |
  29. | virtual | | virtual | | virtual |
  30. | space | | space | | space |
  31. | | | | | |
  32. +---------+ +---------+ +---------+
  33. //开启了指针压缩
  34. +--------+ +--------+
  35. | CLD | | CLD |
  36. +--------+ +--------+
  37. / \ / \ Each CLD has two arenas...
  38. / \ / \
  39. / \ / \
  40. v v v v
  41. +--------+ +--------+ +--------+ +--------+
  42. | noncl | | class | | noncl | | class |
  43. | arena | | arena | | arena | | arena |
  44. +--------+ +--------+ +--------+ +--------+
  45. | \ / |
  46. | --------\ | Non-class arenas take from non-class context,
  47. | / | | class arenas take from class context
  48. | /--------- | |
  49. v v v v
  50. +--------------------+ +------------------------+
  51. | | | |
  52. | Metaspace Context | | Metaspace Context |
  53. | (nonclass) | | (class) |
  54. | | | |
  55. +--------------------+ +------------------------+
  56. | | |
  57. | | | Non-class context: list of smallish mappings
  58. | | | Class context: one large mapping (the class space)
  59. v v v
  60. +--------+ +--------+ +----------------~~~~~~~-----+
  61. | | | | | |
  62. | virtual| | virt | | virt space (class space) |
  63. | space | | space | | |
  64. | | | | | |
  65. +--------+ +--------+ +----------------~~~~~~~-----+

MetaSpace相关内容就不再展开描述了,详情可以参考官方文档 Metaspace - Metaspace - OpenJDK Wiki (http://java.net) [1] 与 Thomas Stüfe 的系列文章 What is Metaspace? | http://stuefe.de [2]。

我们查看 reserve 的具体日志,发现大部分的内存都是 Metaspace::allocate_metaspace_compressed_klass_ptrs 方法申请的,这正是用来分配 CompressedClassSpace 空间的方法:

  1. [0x0000000100000000 - 0x0000000140000000] reserved 1048576KB for Class from
  2. [0x0000ffff93ea28d0] ReservedSpace::ReservedSpace(unsigned long, unsigned long, bool, char*, unsigned long)+0x90
  3. [0x0000ffff93c16694] Metaspace::allocate_metaspace_compressed_klass_ptrs(char*, unsigned char*)+0x42c
  4. [0x0000ffff93c16e0c] Metaspace::global_initialize()+0x4fc
  5. [0x0000ffff93e688a8] universe_init()+0x88

JVM 在初始化 MetaSpace 时,调用链路如下:

InitializeJVM ->
Thread::vreate_vm ->
init_globals ->
universe_init ->
MetaSpace::global_initalize ->
Metaspace::allocate_metaspace_compressed_klass_ptrs

查看相关源码:

  1. # hotspot/src/share/vm/memory/metaspace.cpp
  2. void Metaspace::allocate_metaspace_compressed_klass_ptrs(char* requested_addr, address cds_base) {
  3. ......
  4. ReservedSpace metaspace_rs = ReservedSpace(compressed_class_space_size(),
  5. _reserve_alignment,
  6. large_pages,
  7. requested_addr, 0);
  8. ......
  9. metaspace_rs = ReservedSpace(compressed_class_space_size(),
  10. _reserve_alignment, large_pages);
  11. ......
  12. }

我们可以发现如果开启了 UseCompressedClassPointers ,那么就会调用 allocate_metaspace_compressed_klass_ptrs 方法去 reserve 一个 compressed_class_space_size() 大小的空间(由于我们没有显式地设置过 -XX:CompressedClassSpaceSize 的大小,所以此时默认值为 1G)。如果我们显式地设置 -XX:CompressedClassSpaceSize=256M 再重启 JVM ,就会发现 reserve 的内存大小已经被限制住了:

  1. Thread (reserved=258568KB, committed=258568KB)
  2. (thread #127)
  3. (stack: reserved=258048KB, committed=258048KB)
  4. (malloc=390KB #711)
  5. (arena=130KB #234)

但是此时我们不禁会有一个疑问,那就是既然 CompressedClassSpaceSize 可以 reverse 远远超过 -XX:MaxMetaspaceSize 设置的大小,那么 -XX:MaxMetaspaceSize 会不会无法限制住整体 MetaSpace 的大小?实际上 -XX:MaxMetaspaceSize 是可以限制住 MetaSpace 的大小的,只是 HotSpot 此处的代码顺序有问题容易给大家造成误解和歧义~

查看相关代码:

  1. # hotspot/src/share/vm/memory/metaspace.cpp
  2. void Metaspace::ergo_initialize() {
  3. ......
  4. CompressedClassSpaceSize = align_size_down_bounded(CompressedClassSpaceSize, _reserve_alignment);
  5. set_compressed_class_space_size(CompressedClassSpaceSize);
  6. // Initial virtual space size will be calculated at global_initialize()
  7. uintx min_metaspace_sz =
  8. VIRTUALSPACEMULTIPLIER * InitialBootClassLoaderMetaspaceSize;
  9. if (UseCompressedClassPointers) {
  10. if ((min_metaspace_sz + CompressedClassSpaceSize) > MaxMetaspaceSize) {
  11. if (min_metaspace_sz >= MaxMetaspaceSize) {
  12. vm_exit_during_initialization("MaxMetaspaceSize is too small.");
  13. } else {
  14. FLAG_SET_ERGO(uintx, CompressedClassSpaceSize,
  15. MaxMetaspaceSize - min_metaspace_sz);
  16. }
  17. }
  18. }
  19. ......
  20. }

我们可以发现如果 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 )。

  1. Class (reserved=1056882KB, committed=1053042KB)
  2. (classes #483)
  3. (malloc=114KB #629)
  4. (mmap: reserved=1056768KB, committed=1052928KB)
  5. ( Metadata: )
  6. ( reserved=8192KB, committed=4352KB)
  7. ( used=3492KB)
  8. ( free=860KB)
  9. ( waste=0KB =0.00%)
  10. ( Class space:)
  11. ( reserved=1048576KB, committed=512KB)
  12. ( used=326KB)
  13. ( free=186KB)
  14. ( waste=0KB =0.00%)

Thread

线程所使用的内存:

  1. Thread (reserved=258568KB, committed=258568KB)
  2. (thread #127) //线程个数
  3. (stack: reserved=258048KB, committed=258048KB) //栈使用的内存
  4. (malloc=390KB #711)
  5. (arena=130KB #234) //线程句柄使用的内存
  6. ......
  7. [0x0000fffdbea32000 - 0x0000fffdbec32000] reserved and committed 2048KB for Thread Stack from
  8. [0x0000ffff935ab79c] attach_listener_thread_entry(JavaThread*, Thread*)+0x34
  9. [0x0000ffff93e3ddb4] JavaThread::thread_main_inner()+0xf4
  10. [0x0000ffff93e3e01c] JavaThread::run()+0x214
  11. [0x0000ffff93cb49e4] java_start(Thread*)+0x11c
  12. [0x0000fffdbecce000 - 0x0000fffdbeece000] reserved and committed 2048KB for Thread Stack from
  13. [0x0000ffff93cb49e4] java_start(Thread*)+0x11c
  14. [0x0000ffff944148bc] start_thread+0x19c

观察 NMT 打印信息,我们可以发现,此时的 JVM 进程共使用了127个线程,committed 了 258568KB 的内存。

继续观察下面各个线程的分配情况就会发现,每个线程 committed 了2048KB(2M)的内存空间,这可能和平时的认知不太相同,因为平时我们大多数情况下使用的都是x86平台,而笔者此时使用的是 ARM (aarch64)的平台,所以此处线程默认分配的内存与 x86 不同。

如果我们不显式的设置 -Xss/-XX:ThreadStackSize 相关的参数,那么 JVM 会使用默认的值。

在 aarch64 平台下默认为 2M:

  1. # globals_linux_aarch64.hpp
  2. define_pd_global(intx, ThreadStackSize, 2048); // 0 => use system default
  3. define_pd_global(intx, VMThreadStackSize, 2048);

而在 x86 平台下默认为 1M:

  1. # globals_linux_x86.hpp
  2. define_pd_global(intx, ThreadStackSize, 1024); // 0 => use system default
  3. define_pd_global(intx, VMThreadStackSize, 1024);

如果我们想缩减此部分内存的使用,可以使用参数 -Xss/-XX:ThreadStackSize 设置适合自身业务情况的大小,但是需要进行相关压力测试保证不会出现溢出等错误。

Code

JVM 自身会生成一些 native code 并将其存储在称为 codecache 的内存区域中。JVM 生成 native code 的原因有很多,包括动态生成的解释器循环、 JNI、即时编译器(JIT)编译 Java 方法生成的本机代码 。其中 JIT 生成的 native code 占据了 codecache 绝大部分的空间。

  1. Code (reserved=266273KB, committed=4001KB)
  2. (malloc=33KB #309)
  3. (mmap: reserved=266240KB, committed=3968KB)
  4. ......
  5. [0x0000ffff7c000000 - 0x0000ffff8c000000] reserved 262144KB for Code from
  6. [0x0000ffff93ea3c2c] ReservedCodeSpace::ReservedCodeSpace(unsigned long, unsigned long, bool)+0x84
  7. [0x0000ffff9392dcd0] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xc8
  8. [0x0000ffff9374bd64] codeCache_init()+0xb4
  9. [0x0000ffff9395ced0] init_globals()+0x58
  10. [0x0000ffff7c3c0000 - 0x0000ffff7c3d0000] committed 64KB from
  11. [0x0000ffff93ea47e0] VirtualSpace::expand_by(unsigned long, bool)+0x1d8
  12. [0x0000ffff9392e01c] CodeHeap::expand_by(unsigned long)+0xac
  13. [0x0000ffff9374cee4] CodeCache::allocate(int, bool)+0x64
  14. [0x0000ffff937444b8] MethodHandlesAdapterBlob::create(int)+0xa8

追踪 codecache 的逻辑:

  1. # codeCache.cpp
  2. void CodeCache::initialize() {
  3. ......
  4. CodeCacheExpansionSize = round_to(CodeCacheExpansionSize, os::vm_page_size());
  5. InitialCodeCacheSize = round_to(InitialCodeCacheSize, os::vm_page_size());
  6. ReservedCodeCacheSize = round_to(ReservedCodeCacheSize, os::vm_page_size());
  7. if (!_heap->reserve(ReservedCodeCacheSize, InitialCodeCacheSize, CodeCacheSegmentSize)) {
  8. vm_exit_during_initialization("Could not reserve enough space for code cache");
  9. }
  10. ......
  11. }
  12. # virtualspace.cpp
  13. //记录 mtCode 的函数,其中 r_size 由 ReservedCodeCacheSize 得出
  14. ReservedCodeSpace::ReservedCodeSpace(size_t r_size,
  15. size_t rs_align,
  16. bool large) :
  17. ReservedSpace(r_size, rs_align, large, /*executable*/ true) {
  18. MemTracker::record_virtual_memory_type((address)base(), mtCode);
  19. }

可以发现 CodeCache::initialize() 时 codecache reserve 的最大内存是由我们设置的 -XX:ReservedCodeCacheSize 参数决定的(当然 ReservedCodeCacheSize 的值会做一些对齐操作),我们可以通过设置 -XX:ReservedCodeCacheSize 来限制 Code 相关的最大内存。

同时我们发现,初始化时 codecache commit 的内存可以由 -XX:InitialCodeCacheSize 参数来控制,具体计算代码可以查看 VirtualSpace::expand_by 函数。

我们设置 -XX:InitialCodeCacheSize=128M 后重启 JVM 进程,再次查看 NMT detail:

  1. Code (reserved=266273KB, committed=133153KB)
  2. (malloc=33KB #309)
  3. (mmap: reserved=266240KB, committed=133120KB)
  4. ......
  5. [0x0000ffff80000000 - 0x0000ffff88000000] committed 131072KB from
  6. [0x0000ffff979e60e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224
  7. [0x0000ffff9746fcfc] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xf4
  8. [0x0000ffff9728dd64] codeCache_init()+0xb4
  9. [0x0000ffff9749eed0] init_globals()+0x58

我们可以通过 -XX:InitialCodeCacheSize 来设置 codecache 初始 commit 的内存。

  • 除了使用 NMT 打印codecache相关信息,我们还可以通过 -XX:+PrintCodeCache(JVM 关闭时输出codecache的使用情况)和jcmdpidCompiler.codecache(只有在 JDK 9 及以上版本的jcmd才支持该选项)来查看codecache相关的信息。
  • 了解更多codecache详情可以查看CodeCache官方文档 [3]。

GC

GC 所使用的内存,就是垃圾收集器使用的数据所占据的内存,例如卡表 card tables、记忆集 remembered sets、标记栈 marking stack、标记位图 marking bitmaps 等等。其实不论是 card tables、remembered sets 还是 marking stack、marking bitmaps,都是一种借助额外的空间,来记录不同内存区域之间引用关系的结构(都是基于空间换时间的思想,否则寻找引用关系就需要诸如遍历这种浪费时间的方式)。

简单介绍下相关概念:

更详细的信息不深入展开介绍了,可以查看彭成寒老师《JVM G1源码分析和调优》2.3 章 [4] 与 4.1 章节 [5],还可以查看 R大(RednaxelaFX)对相关概念的科普 [6]。

  • 卡表 card tables,在部分收集器(如CMS)中存储跨代引用(如老年代中对象指向年轻代的对象)的数据结构,精度可以有很多种选择:

如果精确到机器字,那么往往描述的区域太小了,使用的内存开销会变大,所以 HotSpot 中选择 512KB 为精度大小。

卡表甚至可以细到和 bitmap 相同,即使用 1 bit 位来对应一个内存页(512KB),但是因为 JVM 在操作一个 bit 位时,仍然需要读取整个机器字 word,并且操作 bit 位的开销有时反而大于操作 byte 。所以 HotSpot 的 cardTable 选择使用 byte 数组代替 bit ,用 1 byte 对应 512KB 的空间,使用 byte 数组的开销也可以接受(1G 的堆内存使用卡表也只占用2M:1 * 1024 * 1024 / 512 = 2048 KB)。

我们以 cardTableModRefBS 为例,查看其源码结构:

  1. # hotspor/src/share/vm/momery/cardTableModRefBS.hpp
  2. //精度为 512 KB
  3. enum SomePublicConstants {
  4. card_shift = 9,
  5. card_size = 1 << card_shift,
  6. card_size_in_words = card_size / sizeof(HeapWord)
  7. };
  8. ......
  9. class CardTableModRefBS: public ModRefBarrierSet {
  10. .....
  11. size_t _byte_map_size; // in bytes
  12. jbyte* _byte_map; // the card marking array
  13. .....
  14. }

可以发现 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 详情:

  1. ......
  2. [0x0000fffe16000000 - 0x0000fffe17000000] reserved 16384KB for GC from
  3. [0x0000ffff93ea2718] ReservedSpace::ReservedSpace(unsigned long, unsigned long)+0x118
  4. [0x0000ffff93892328] G1CollectedHeap::create_aux_memory_mapper(char const*, unsigned long, unsigned long)+0x48
  5. [0x0000ffff93899108] G1CollectedHeap::initialize()+0x368
  6. [0x0000ffff93e68594] Universe::initialize_heap()+0x15c
  7. [0x0000fffe16000000 - 0x0000fffe17000000] committed 16384KB from
  8. [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c
  9. [0x0000ffff938bc08c] G1PageBasedVirtualSpace::commit(unsigned long, unsigned long)+0x11c
  10. [0x0000ffff938bf774] G1RegionsLargerThanCommitSizeMapper::commit_regions(unsigned int, unsigned long)+0x5c
  11. [0x0000ffff93943f8c] HeapRegionManager::commit_regions(unsigned int, unsigned long)+0xb4
  12. ......

我们可以发现 JVM 在初始化 heap 堆的时候(此时是 G1 收集器所使用的堆 G1CollectedHeap),不仅会创建 remember set ,还会有一个 create_aux_memory_mapper 的操作,用来给 GC 辅助用的数据结构(如:card table、prev bitmap、 next bitmap 等)创建对应的内存映射,相关操作可以查看 g1CollectedHeap 初始化部分源代码:

  1. # hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp
  2. jint G1CollectedHeap::initialize() {
  3. ......
  4. //创建 G1 remember set
  5. // Also create a G1 rem set.
  6. _g1_rem_set = new G1RemSet(this, g1_barrier_set());
  7. ......
  8. // Create storage for the BOT, card table, card counts table (hot card cache) and the bitmaps.
  9. G1RegionToSpaceMapper* bot_storage =
  10. create_aux_memory_mapper("Block offset table",
  11. G1BlockOffsetSharedArray::compute_size(g1_rs.size() / HeapWordSize),
  12. G1BlockOffsetSharedArray::N_bytes);
  13. ReservedSpace cardtable_rs(G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size() / HeapWordSize));
  14. G1RegionToSpaceMapper* cardtable_storage =
  15. create_aux_memory_mapper("Card table",
  16. G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size() / HeapWordSize),
  17. G1BlockOffsetSharedArray::N_bytes);
  18. G1RegionToSpaceMapper* card_counts_storage =
  19. create_aux_memory_mapper("Card counts table",
  20. G1BlockOffsetSharedArray::compute_size(g1_rs.size() / HeapWordSize),
  21. G1BlockOffsetSharedArray::N_bytes);
  22. size_t bitmap_size = CMBitMap::compute_size(g1_rs.size());
  23. G1RegionToSpaceMapper* prev_bitmap_storage =
  24. create_aux_memory_mapper("Prev Bitmap", bitmap_size, CMBitMap::mark_distance());
  25. G1RegionToSpaceMapper* next_bitmap_storage =
  26. create_aux_memory_mapper("Next Bitmap", bitmap_size, CMBitMap::mark_distance());
  27. _hrm.initialize(heap_storage, prev_bitmap_storage, next_bitmap_storage, bot_storage, cardtable_storage, card_counts_storage);
  28. g1_barrier_set()->initialize(cardtable_storage);
  29. // Do later initialization work for concurrent refinement.
  30. _cg1r->init(card_counts_storage);
  31. ......
  32. }

因为这些辅助的结构都是一种空间换时间的思想,所以不可避免的会占用额外的内存,尤其是 G1 的 RSet 结构,当我们调大我们的堆内存,GC 所使用的内存也会不可避免的跟随增长:

  1. # -Xmx1G -Xms1G
  2. GC (reserved=164403KB, committed=164403KB)
  3. (malloc=92723KB #6540)
  4. (mmap: reserved=71680KB, committed=71680KB)
  5. # -Xmx2G -Xms2G
  6. GC (reserved=207891KB, committed=207891KB)
  7. (malloc=97299KB #12683)
  8. (mmap: reserved=110592KB, committed=110592KB)
  9. # -Xmx4G -Xms4G
  10. GC (reserved=290313KB, committed=290313KB)
  11. (malloc=101897KB #12680)
  12. (mmap: reserved=188416KB, committed=188416KB)
  13. # -Xmx8G -Xms8G
  14. GC (reserved=446473KB, committed=446473KB)
  15. (malloc=102409KB #12680)
  16. (mmap: reserved=344064KB, committed=344064KB)

我们可以看到这个额外的内存开销一般在 1% - 20%之间,当然如果我们不使用 G1 收集器,这个开销是没有那么大的:

  1. # -XX:+UseSerialGC -Xmx8G -Xms8G
  2. GC (reserved=27319KB, committed=27319KB)
  3. (malloc=7KB #79)
  4. (mmap: reserved=27312KB, committed=27312KB)
  5. # -XX:+UseConcMarkSweepGC -Xmx8G -Xms8G
  6. GC (reserved=167318KB, committed=167318KB)
  7. (malloc=140006KB #373)
  8. (mmap: reserved=27312KB, committed=27312KB)

我们可以看到,使用最轻量级的 UseSerialGC,GC 部分占用的内存有很明显的降低(436M -> 26.67M);使用 CMS ,GC 部分从 436M 降低到 163.39M。

GC 这块内存是必须的,也是我们在使用过程中无法压缩的。停顿、吞吐量、内存占用就是 GC 中不可能同时达到的三元悖论,不同的垃圾收集器在这三者中有不同的侧重,我们应该结合自身的业务情况综合考量选择合适的垃圾收集器。

由于篇幅有限,将在下篇文章继续给大家分享 追踪区域的其它内存类型(包含Compiler、Internal、Symbol、Native Memory Tracking、Arena Chunk 和 Unknown)以及 NMT 无法追踪的内存,敬请期待!

参考

  1. https://wiki.openjdk.java.net/display/HotSpot/Metaspace
  2. https://stuefe.de/posts/metaspace/what-is-metaspace
  3. https://docs.oracle.com/javase/8/embedded/develop-apps-platforms/codecache.htm
  4. https://weread.qq.com/web/reader/53032310717f44515302749k3c5327902153c59dc0488e1
  5. https://weread.qq.com/web/reader/53032310717f44515302749ka1d32a6022aa1d0c6e83eb4
  6. https://hllvm-group.iteye.com/group/topic/21468#post-272070

欢迎加入Compiler SIG交流群与大家共同交流学习编译技术相关内容,扫码添加小助手微信邀请你进入Compiler SIG交流群。

点击关注,第一时间了解华为云新鲜技术~

详解Native Memory Tracking 追踪区域分析的更多相关文章

  1. 详解Native Memory Tracking之追踪区域分析

    摘要:本篇图文将介绍追踪区域的内存类型以及 NMT 无法追踪的内存. 本文分享自华为云社区<[技术剖析]17. Native Memory Tracking 详解(3)追踪区域分析(二)> ...

  2. 带你认识JDK8中超nice的Native Memory Tracking

    摘要:从 OpenJDK8 起有了一个很 nice 的虚拟机内部功能: Native Memory Tracking (NMT). 本文分享自华为云社区<Native Memory Tracki ...

  3. 全网最硬核 JVM 内存解析 - 1.从 Native Memory Tracking 说起

    个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...

  4. TCP/IP详解卷1 - wireshark抓包分析

    TCP/IP详解卷1 - 系列文 TCP/IP详解卷1 - 思维导图(1) TCP/IP详解卷1 - wireshark抓包分析 引言 在初学TCP/IP协议时,会觉得协议是一种很抽象的东西,通过wi ...

  5. 八大排序算法详解(动图演示 思路分析 实例代码java 复杂度分析 适用场景)

    一.分类 1.内部排序和外部排序 内部排序:待排序记录存放在计算机随机存储器中(说简单点,就是内存)进行的排序过程. 外部排序:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需 ...

  6. WindowsPhone自定义控件详解(二) - 模板类库分析

    转自:http://blog.csdn.net/mr_raptor/article/details/7251948 WindowsPhone自定义控件详解(一) - 控件类库分析 上一节主要分析了控件 ...

  7. GC之详解CMS收集过程和日志分析

    2016-08-23   关于GC的算法和垃圾收集器的种类就暂且不说了,网上有大把的资料供参考 话题引入 让我们先简单的看下整个堆年轻代和年老代的垃圾收集器组合(以下配合java8完美支持,其他版本可 ...

  8. 阿里P7架构师详解微服务链路追踪原理

    背景介绍 在微服务横行的时代,服务化思维逐渐成为了程序员的基本思维模式,但是,由于绝大部分项目只是一味地增加服务,并没有对其妥善管理,当接口出现问题时,很难从错综复杂的服务调用网络中找到问题根源,从而 ...

  9. 【java】详解native方法的使用

    目录结构: contents structure [+] 关于native关键字 使用native关键字 使用步骤 案例 编写.java文件 编译.java文件 获得.h文件 编写hello.cpp文 ...

  10. [转载]AMOLED结构详解,BOE专家给你分析驱动补偿

    关键词: AMOLED, 驱动补偿 有机发光显示二极管(OLED)作为一种电流型发光器件已越来越多地被应用于高性能显示中.由于它自发光的特性,与LCD相比,AMOLED具有高对比度.超轻薄.可弯曲等诸 ...

随机推荐

  1. Vue之键盘事件

    1.使用keydown触发事件 <!DOCTYPE html> <html lang="en"> <head> <meta charset ...

  2. Gitlab仓库代码更新时Jenkins自动构建

    环境说明 1.Jenkins和gitlab已经都已经安装完毕 2.Jenkins能连接到gitlab获取项目并能手动创建项目 3.Jenkins和gitlab能相互访问的到(gitlab要能连接到Je ...

  3. 用Python实现将txt中的中文和中文标点保留下来

    用正则表达式就可以实现这个操作. 中文标点的正则表达式: [\u3002\uff1b\uff0c\uff1a\u201c\u201d\uff08\uff09\u3001\uff1f\u300a\u30 ...

  4. 字符串表达式计算(a+b/(a-b))的思路与实践

    前言 为满足业务需要,需要为项目中自定义模板添加一个计算字段的组件,通过设置字符串表达式,使用时在改变表达式其中一个字段的数据时,自动计算另外一个字段的值. 本篇为上篇,介绍原理,简单实现一个工具,输 ...

  5. JUC并发编程学习笔记(十二)Stream流式计算

    Stream流式计算 什么是Stream流式计算 大数据:存储+计算 集合.MySql这些的本质都是存储东西的: 计算都应该交给流来操作! 一个案例说明:函数式接口.lambda表达式.链式编程.St ...

  6. C#判断字符串的显示宽度

    C#判断字符串的显示宽度 起因: 公司有一个使用项目使用HTML转换为PDF,其中有一个表格,表格的最后一列中的单元格,其字符串超长后会被丢弃,而不是换行到下一行展示(HtmlToPdf渲染引擎导致的 ...

  7. P2360 地下城主

    题目大意 背景是逃离\(3D\)地下监狱,也就是三维样例,你可以前往所在小格的前方,后方,左方,右方,上层,下层的小格,'.'表示可走,'x'表示墙壁,'S'表示起点,'E'表示终点.每走一小格花费一 ...

  8. 伯俊ERP与金蝶云星空对接集成连通应收单新增

    伯俊ERP与金蝶云星空对接集成表头表体组合查询连通应收单新增(应收单-标准应收单(KD应收单销售退) 数据源系统:伯俊ERP 未来,伯俊科技也会砥砺前行,不断为品牌提供更全面的零售终端致胜利器.伯俊科 ...

  9. 如何搭建园区3d可视化大屏

    搭建园区3D可视化大屏需要考虑多个方面,包括硬件设备.数据源获取.软件工具和可视化设计. 以下是一个详细的步骤指南,帮助您了解如何搭建园区3D可视化大屏. 1. 硬件设备选择: - 显示屏幕:选择适合 ...

  10. 吉特日化MES & HttpClient基础连接已经关闭: 连接被意外关闭

    在吉特日化MES调用某公司AGV平台下发任务的时候,使用HttpClient 进行POST请求,出现如下异常: HttpClient基础连接已经关闭: 连接被意外关闭  , 之前已经使用HTTPCli ...