Native Memory Tracking 追踪区域分析详解-程序员宅基地

技术标签: 算法  c++  java  

Compiler

Compiler 就是 JIT 编译器线程在编译 code 时本身所使用的内存。查看 NMT 详情:

[0x0000ffff93e3acc0] Thread::allocate(unsigned long, bool, MemoryType)+0x348
[0x0000ffff9377a498] CompileBroker::make_compiler_thread(char const*, CompileQueue*, CompilerCounters*, AbstractCompiler*, Thread*)+0x120
[0x0000ffff9377ce98] CompileBroker::init_compiler_threads(int, int)+0x148
[0x0000ffff9377d400] CompileBroker::compilation_init()+0xc8
                             (malloc=37KB type=Thread #12)

跟踪调用链路:InitializeJVM ->
Threads::create_vm ->
CompileBroker::compilation_init ->
CompileBroker::init_compiler_threads ->
CompileBroker::make_compiler_thread

发现最后 make_compiler_thread 的线程的个数是在 compilation_init () 中计算的:

# hotspot/src/share/vm/compiler/CompileBroker.cpp
void CompileBroker::compilation_init() {
  ......
 // No need to initialize compilation system if we do not use it.
 if (!UseCompiler) {
 return;
  }
#ifndef SHARK
 // Set the interface to the current compiler(s).
  int c1_count = CompilationPolicy::policy()->compiler_count(CompLevel_simple);
  int c2_count = CompilationPolicy::policy()->compiler_count(CompLevel_full_optimization);
  ......
 // Start the CompilerThreads
 init_compiler_threads(c1_count, c2_count);
  ......
}

追溯 c1_count、c2_count 的计算逻辑,首先在 JVM 初始化的时候(Threads::create_vm -> init_globals -> compilationPolicy_init)要设置编译的策略 CompilationPolicy:

# hotspot/src/share/vm/runtime/arguments.cpp
void Arguments::set_tiered_flags() {
 // With tiered, set default policy to AdvancedThresholdPolicy, which is 3.
 if (FLAG_IS_DEFAULT(CompilationPolicyChoice)) {
    FLAG_SET_DEFAULT(CompilationPolicyChoice, 3);
  }
  ......
}
# hotspot/src/share/vm/runtime/compilationPolicy.cpp
// Determine compilation policy based on command line argument
void compilationPolicy_init() {
 CompilationPolicy::set_in_vm_startup(DelayCompilationDuringStartup);
 switch(CompilationPolicyChoice) {
  ......
 case 3:
#ifdef TIERED
 CompilationPolicy::set_policy(new AdvancedThresholdPolicy());
#else
 Unimplemented();
#endif
 break;
  ......
 CompilationPolicy::policy()->initialize();
}

此时我们默认开启了分层编译,所以 CompilationPolicyChoice 为 3 ,编译策略选用的是 AdvancedThresholdPolicy,查看相关源码(compilationPolicy_init -> AdvancedThresholdPolicy::initialize):

# hotspot/src/share/vm/runtime/advancedThresholdPolicy.cpp
void AdvancedThresholdPolicy::initialize() {
 // Turn on ergonomic compiler count selection
 if (FLAG_IS_DEFAULT(CICompilerCountPerCPU) && FLAG_IS_DEFAULT(CICompilerCount)) {
    FLAG_SET_DEFAULT(CICompilerCountPerCPU, true);
  }
 int count = CICompilerCount;
 if (CICompilerCountPerCPU) {
 // Simple log n seems to grow too slowly for tiered, try something faster: log n * log log n
 int log_cpu = log2_int(os::active_processor_count());
 int loglog_cpu = log2_int(MAX2(log_cpu, 1));
    count = MAX2(log_cpu * loglog_cpu, 1) * 3 / 2;
  }
  set_c1_count(MAX2(count / 3, 1));
  set_c2_count(MAX2(count - c1_count(), 1));
  ......
}

我们可以发现,在未手动设置 -XX:CICompilerCountPerCPU 和 -XX:CICompilerCount 这两个参数的时候,JVM 会启动 CICompilerCountPerCPU ,启动编译线程的数目会根据 CPU 数重新计算而不再使用默认的 CICompilerCount 的值(3),计算公式通常情况下为 log n * log log n * 1.5(log 以 2 为底),此时笔者使用的机器有 64 个 CPU,经过计算得出编译线程的数目为 18。计算出编译线程的总数目之后,再按 1:2 的比例分别分配给 C1、C2,即我们上文所求的 c1_count、c2_count。

使用 jinfo -flag CICompilerCount 来验证此时 JVM 进程的编译线程数目:

jinfo -flag CICompilerCount 
-XX:CICompilerCount=18

所以我们可以通过显式的设置 -XX:CICompilerCount 来控制 JVM 开启编译线程的数目,从而限制 Compiler 部分所使用的内存(当然这部分内存比较小)。

我们还可以通过 -XX:-TieredCompilation 关闭分层编译来降低内存使用,当然是否关闭分层编译取决于实际的业务需求,节省的这点内存实在微乎其微。

编译线程也是线程,所以我们还可以通过 -XX:VMThreadStackSize 设置一个更小的值来节省此部分内存,但是削减虚拟机线程的堆栈大小是危险的操作,并不建议去因为此设置这个参数。

Internal

Internal 包含命令行解析器使用的内存、JVMTI、PerfData 以及 Unsafe 分配的内存等等。

其中命令行解释器就是在初始化创建虚拟机时对 JVM 的命令行参数加以解析并执行相应的操作,如对参数 -XX:NativeMemoryTracking=detail 进行解析。

JVMTI(JVM Tool Interface)是开发和监视 JVM 所使用的编程接口。它提供了一些方法去检查 JVM 状态和控制 JVM 的运行,详情可以查看 JVMTI 官方文档 [1]。

PerfData 是 JVM 中用来记录一些指标数据的文件,如果开启 -XX:+UsePerfData(默认开启),JVM 会通过 mmap 的方式(即使用上文中提到的 os::reserve_memory 和 os::commit_memory)去映射到 {tmpdir}/hsperfdata_/pid 文件中,jstat 通过读取 PerfData 中的数据来展示 JVM 进程中的各种指标信息.

需要注意的是,    {tmpdir}/hsperfdata_/pid    与 {tmpdir}/.java_pid    并不是一个东西,后者是在 Attach 机制中用来通讯的,类似一种 Unix Domain Socket 的思想,不过真正的 Unix Domain Socket(JEP380 [2])在 JDK16 中才支持。

我们在操作 nio 时经常使用 ByteBuffer ,其中 ByteBuffer.allocateDirect/ DirectByteBuffer 会通过 unsafe.allocateMemory 的方式来 malloc 分配 naive memory,虽然 DirectByteBuffer 本身还是存放于 Heap 堆中,但是它对应的 address 映射的却是分配在堆外内存的 native memory,NMT 会将 Unsafe_AllocateMemory 方式分配的内存记录在 Internal 之中(jstat 也是通过 ByteBuffer 的方式来使用 PerfData)。

需要注意的是,Unsafe_AllocateMemory    分配的内存在 JDK11 之前,在 NMT 中都属于 Internal,但是在 JDK11 之后被 NMT 归属到 Other 中。例如相同    ByteBuffer.allocateDirect    在 JDK11 中进行追踪:[0x0000ffff8c0b4a60] Unsafe_AllocateMemory0+0x60 [0x0000ffff6b822fbc] (malloc=393218KB type=Other #3)

简单查看下相关源码:

# ByteBuffer.java
 public static ByteBuffer allocateDirect(int capacity) {
 return new DirectByteBuffer(capacity);
    }
# DirectByteBuffer.java
 DirectByteBuffer(int cap) {                   // package-private
        ......
 long base = 0;
 try {
 base = unsafe.allocateMemory(size);
        }
       ......
# Unsafe.java
 public native long allocateMemory(long bytes);
# hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
 UnsafeWrapper("Unsafe_AllocateMemory");
 size_t sz = (size_t)size;
  ......
 sz = round_to(sz, HeapWordSize);
 void* x = os::malloc(sz, mtInternal);
  ......
UNSAFE_END

一般情况下,命令行解释器、JVMTI 等方式不会申请太大的内存,我们需要注意的是通过 Unsafe_AllocateMemory 方式申请的堆外内存(如业务使用了 Netty ),可以通过一个简单的示例来进行验证,这个示例的 JVM 启动参数为:-Xmx1G -Xms1G -XX:+UseG1GC -XX:MaxMetaspaceSize=256M -XX:ReservedCodeCacheSize=256M -XX:NativeMemoryTracking=detail(去除了 -XX:MaxDirectMemorySize=256M 的限制):

import java.nio.ByteBuffer;
public class ByteBufferTest {
 private static int _1M = 1024 * 1024;
 private static ByteBuffer allocateBuffer_1 = ByteBuffer.allocateDirect(128 * _1M);
 private static ByteBuffer allocateBuffer_2 = ByteBuffer.allocateDirect(256 * _1M);
 public static void main(String[] args) throws Exception {
        System.out.println("MaxDirect memory: " + sun.misc.VM.maxDirectMemory() + " bytes");
        System.out.println("Direct allocation: " + (allocateBuffer_1.capacity() + allocateBuffer_2.capacity()) + " bytes");
        System.out.println("Native memory used: " + sun.misc.SharedSecrets.getJavaNioAccess().getDirectBufferPool().getMemoryUsed() + " bytes");
 Thread.sleep(6000000);
    }
}

查看输出:

MaxDirect memory: 1073741824 bytes
Direct allocation: 402653184 bytes
Native memory used: 402653184 bytes

查看 NMT 详情:

-                  Internal (reserved=405202KB, committed=405202KB)
                            (malloc=405170KB #3605) 
                            (mmap: reserved=32KB, committed=32KB) 
                   ......
                   [0x0000ffffbb599190] Unsafe_AllocateMemory+0x1c0
                   [0x0000ffffa40157a8]
                             (malloc=393216KB type=Internal #2)
                   ......
                   [0x0000ffffbb04b3f8] GenericGrowableArray::raw_allocate(int)+0x188
                   [0x0000ffffbb4339d8] PerfDataManager::add_item(PerfData*, bool) [clone .constprop.16]+0x108
                   [0x0000ffffbb434118] PerfDataManager::create_string_variable(CounterNS, char const*, int, char const*, Thread*)+0x178
                   [0x0000ffffbae9d400] CompilerCounters::CompilerCounters(char const*, int, Thread*) [clone .part.78]+0xb0
                             (malloc=3KB type=Internal #1)
                   ......

可以发现,我们在代码中使用 ByteBuffer.allocateDirect(内部也是使用 new DirectByteBuffer (capacity))的方式,即 Unsafe_AllocateMemory 申请的堆外内存被 NMT 以 Internal 的方式记录了下来:(128 M + 256 M)= 384 M = 393216 KB = 402653184 Bytes。

当然我们可以使用参数 -XX:MaxDirectMemorySize 来限制 Direct Buffer 申请的最大内存。

Symbol

Symbol 为 JVM 中的符号表所使用的内存,HotSpot 中符号表主要有两种:SymbolTable 与 StringTable

大家都知道 Java 的类在编译之后会生成 Constant pool 常量池,常量池中会有很多的字符串常量,HotSpot 出于节省内存的考虑,往往会将这些字符串常量作为一个 Symbol 对象存入一个 HashTable 的表结构中即 SymbolTable,如果该字符串可以在 SymbolTable 中 lookup(SymbolTable::lookup)到,那么就会重用该字符串,如果找不到才会创建新的 Symbol(SymbolTable::new_symbol)。

当然除了 SymbolTable,还有它的双胞胎兄弟 StringTable(StringTable 结构与 SymbolTable 基本是一致的,都是 HashTable 的结构),即我们常说的字符串常量池。平时做业务开发和 StringTable 打交道会更多一些,HotSpot 也是基于节省内存的考虑为我们提供了 StringTable,我们可以通过 String.intern 的方式将字符串放入 StringTable 中来重用字符串。

编写一个简单的示例:

public class StringTableTest {
 public static void main(String[] args) throws Exception {
 while (true){
            String str = new String("StringTestData_" + System.currentTimeMillis());
 str.intern();
        }
    }
}

启动程序后我们可以使用 jcmd VM.native_memory baseline 来创建一个基线方便对比,稍作等待后再使用 jcmd VM.native_memory summary.diff/detail.diff 与创建的基线作对比,对比后我们可以发现:

Total: reserved=2831553KB +20095KB, committed=1515457KB +20095KB
......
-                    Symbol (reserved=18991KB +17144KB, committed=18991KB +17144KB)
                            (malloc=18504KB +17144KB #2307 +2143)
                            (arena=488KB #1)
......
[0x0000ffffa2aef4a8] BasicHashtable<(MemoryType)9>::new_entry(unsigned int)+0x1a0
[0x0000ffffa2aef558] Hashtable::new_entry(unsigned int, oopDesc*)+0x28
[0x0000ffffa2fbff78] StringTable::basic_add(int, Handle, unsigned short*, int, unsigned int, Thread*)+0xe0
[0x0000ffffa2fc0548] StringTable::intern(Handle, unsigned short*, int, Thread*)+0x1a0
                             (malloc=17592KB type=Symbol +17144KB #2199 +2143)
......

JVM 进程这段时间内存一共增长了 20095KB,其中绝大部分都是 Symbol 申请的内存(17144KB),查看具体的申请信息正是 StringTable::intern 在不断的申请内存。

如果我们的程序错误的使用 String.intern () 或者 JDK intern 相关 BUG 导致了内存异常,可以通过这种方式轻松协助定位出来。

需要注意的是,虚拟机提供的参数 -XX:StringTableSize 并不是来限制 StringTable 最大申请的内存大小的,而是用来限制 StringTable 的表的长度的,我们加上 -XX:StringTableSize=10M 来重新启动 JVM 进程,一段时间后查看 NMT 追踪情况:

-                    Symbol (reserved=100859KB +17416KB, committed=100859KB +17416KB)
                            (malloc=100371KB +17416KB #2359 +2177)
                            (arena=488KB #1)
......
[0x0000ffffa30c14a8] BasicHashtable<(MemoryType)9>::new_entry(unsigned int)+0x1a0
[0x0000ffffa30c1558] Hashtable::new_entry(unsigned int, oopDesc*)+0x28
[0x0000ffffa3591f78] StringTable::basic_add(int, Handle, unsigned short*, int, unsigned int, Thread*)+0xe0
[0x0000ffffa3592548] StringTable::intern(Handle, unsigned short*, int, Thread*)+0x1a0
                             (malloc=18008KB type=Symbol +17416KB #2251 +2177)

可以发现 StringTable 的大小是超过 10M 的,查看该参数的作用:

# hotsopt/src/share/vm/classfile/symnolTable.hpp
 StringTable() : RehashableHashtable((int)StringTableSize,
 sizeof (HashtableEntry)) {}
 StringTable(HashtableBucket* t, int number_of_entries)
    : RehashableHashtable((int)StringTableSize, sizeof (HashtableEntry), t,
 number_of_entries) {}

因为 StringTable 在 HotSpot 中是以 HashTable 的形式存储的,所以 -XX:StringTableSize 参数设置的其实是 HashTable 的长度,如果该值设置的过小的话,即使 HashTable 进行 rehash,hash 冲突也会十分频繁,会造成性能劣化并有可能导致进入 SafePoint 的时间增长。如果发生这种情况,可以调大该值。

  • -XX:StringTableSize 在 32 位系统默认为 1009、64 位默认为 60013 :const int defaultStringTableSize = NOT_LP64 (1009) LP64_ONLY (60013);
  • G1 中可以使用 -XX:+UseStringDeduplication 参数来开启字符串自动去重功能(默认关闭),并使用 -XX:StringDeduplicationAgeThreshold 来控制字符串参与去重的 GC 年龄阈值。
  • 与 -XX:StringTableSize 同理,我们可以通过 -XX:SymbolTableSize 来控制 SymbolTable 表的长度。

如果我们使用的是 JDK11 之后的 NMT,我们可以直接通过命令 jcmd VM.stringtable 与 jcmd VM.symboltable 来查看两者的使用情况:

StringTable statistics:
Number of buckets       : 16777216 = 134217728 bytes, each 8
Number of entries       : 39703 =    635248 bytes, each 16
Number of literals      : 39703 =   2849304 bytes, avg  71.765
Total footprsize_t   :           = 137702280 bytes
Average bucket size     : 0.002
Variance of bucket size : 0.002
Std. dev. of bucket size:     0.049
Maximum bucket size     : 2
SymbolTable statistics:
Number of buckets       : 20011 =    160088 bytes, each 8
Number of entries       : 20133 =    483192 bytes, each 24
Number of literals      : 20133 =    753832 bytes, avg  37.443
Total footprint         :           =   1397112 bytes
Average bucket size     : 1.006
Variance of bucket size : 1.013
Std. dev. of bucket size:     1.006
Maximum bucket size     : 9

Native Memory Tracking

Native Memory Tracking 使用的内存就是 JVM 进程开启 NMT 功能后,NMT 功能自身所申请的内存。

查看源码会发现,JVM 会在 MemTracker::init () 初始化的时候,使用 tracking_level () -> init_tracking_level () 获取我们设定的 tracking_level 追踪等级(如:summary、detail),然后将获取到的 level 分别传入 MallocTracker::initialize (level) 与 VirtualMemoryTracker::initialize (level) 进行判断,只有 level >= summary 的情况下,虚拟机才会分配 NMT 自身所用到的内存,如:VirtualMemoryTracker、MallocMemorySummary、MallocSiteTable(detail 时才会创建) 等来记录 NMT 追踪的各种数据。

# /hotspot/src/share/vm/services/memTracker.cpp
void MemTracker::init() {
 NMT_TrackingLevel level = tracking_level();
  ......
}
# /hotspot/src/share/vm/services/memTracker.hpp
static inline NMT_TrackingLevel tracking_level() {
 if (_tracking_level == NMT_unknown) {
 // No fencing is needed here, since JVM is in single-threaded
 // mode.
      _tracking_level = init_tracking_level();
      _cmdline_tracking_level = _tracking_level;
    }
 return _tracking_level;
  }
# /hotspot/src/share/vm/services/memTracker.cpp
NMT_TrackingLevel MemTracker::init_tracking_level() {
 NMT_TrackingLevel level = NMT_off;
  ......
 if (os::getenv(buf, nmt_option, sizeof(nmt_option))) {
 if (strcmp(nmt_option, "summary") == 0) {
      level = NMT_summary;
    } else if (strcmp(nmt_option, "detail") == 0) {
#if PLATFORM_NATIVE_STACK_WALKING_SUPPORTED
      level = NMT_detail;
#else
      level = NMT_summary;
#endif // PLATFORM_NATIVE_STACK_WALKING_SUPPORTED
    } 
   ......
  }
  ......
 if (!MallocTracker::initialize(level) ||
 !VirtualMemoryTracker::initialize(level)) {
    level = NMT_off;
  }
 return level;
}
# /hotspot/src/share/vm/services/memTracker.cpp
bool MallocTracker::initialize(NMT_TrackingLevel level) {
 if (level >= NMT_summary) {
 MallocMemorySummary::initialize();
  }
 if (level == NMT_detail) {
 return MallocSiteTable::initialize();
  }
 return true;
}
void MallocMemorySummary::initialize() {
  assert(sizeof(_snapshot) >= sizeof(MallocMemorySnapshot), "Sanity Check");
 // Uses placement new operator to initialize static area.
 ::new ((void*)_snapshot)MallocMemorySnapshot();
}
# 
bool VirtualMemoryTracker::initialize(NMT_TrackingLevel level) {
 if (level >= NMT_summary) {
 VirtualMemorySummary::initialize();
  }
 return true;
}

我们执行的 jcmd VM.native_memory summary/detail 命令,就会使用 NMTDCmd::report 方法来根据等级的不同获取不同的数据:

  • summary 时使用 MemSummaryReporter::report () 获取 VirtualMemoryTracker、MallocMemorySummary 等储存的数据;
  • detail 时使用 MemDetailReporter::report () 获取 VirtualMemoryTracker、MallocMemorySummary、MallocSiteTable 等储存的数据。
# hotspot/src/share/vm/services/nmtDCmd.cpp
void NMTDCmd::execute(DCmdSource source, TRAPS) {
  ......
 if (_summary.value()) {
 report(true, scale_unit);
  } else if (_detail.value()) {
 if (!check_detail_tracking_level(output())) {
 return;
    }
 report(false, scale_unit);
  }
  ......
}
void NMTDCmd::report(bool summaryOnly, size_t scale_unit) {
 MemBaseline baseline;
 if (baseline.baseline(summaryOnly)) {
 if (summaryOnly) {
 MemSummaryReporter rpt(baseline, output(), scale_unit);
 rpt.report();
    } else {
 MemDetailReporter rpt(baseline, output(), scale_unit);
 rpt.report();
    }
  }
}

一般 NMT 自身占用的内存是比较小的,不需要太过关心。

Arena Chunk

Arena 是 JVM 分配的一些 Chunk(内存块),当退出作用域或离开代码区域时,内存将从这些 Chunk 中释放出来。然后这些 Chunk 就可以在其他子系统中重用。需要注意的是,此时统计的 Arena 与 Chunk ,是 HotSpot 自己定义的 Arena、Chunk,而不是 Glibc 中相关的 Arena 与 Chunk 的概念。

我们会发现 NMT 详情中会有很多关于 Arena Chunk 的分配信息都是:

[0x0000ffff935906e0] ChunkPool::allocate(unsigned long, AllocFailStrategy::AllocFailEnum)+0x158
[0x0000ffff9358ec14] Arena::Arena(MemoryType, unsigned long)+0x18c
......

JVM 中通过 ChunkPool 来管理重用这些 Chunk,比如我们在创建线程时:

# /hotspot/src/share/vm/runtime/thread.cpp
Thread::Thread() {
  ......
 set_resource_area(new (mtThread)ResourceArea());
  ......
 set_handle_area(new (mtThread) HandleArea(NULL));
  ......

其中 ResourceArea 属于给线程分配的一个资源空间,一般 ResourceObj 都存放于此(如 C1/C2 优化时需要访问的运行时信息);HandleArea 则用来存放线程所持有的句柄(handle),使用句柄来关联使用的对象。这两者都会去申请 Arena,而 Arena 则会通过 ChunkPool::allocate 来申请一个新的 Chunk 内存块。除此之外,JVM 进程用到 Arena 的地方还有非常多,比如 JMX、OopMap 等等一些相关的操作都会用到 ChunkPool。

眼尖的读者可能会注意到上文中提到,通常情况下会通过 ChunkPool::allocate 的方式来申请 Chunk 内存块。是的,其实除了 ChunkPool::allocate 的方式, JVM 中还存在另外一种申请 Arena Chunk 的方式,即直接借助 Glibc 的 malloc 来申请内存,JVM 为我们提供了相关的控制参数 UseMallocOnly:

develop(bool, UseMallocOnly, false,                                       \
 "Use only malloc/free for allocation (no resource area/arena)") 

我们可以发现这个参数是一个 develop 的参数,一般情况下我们是使用不到的,因为 VM option 'UseMallocOnly' is develop and is available only in debug version of VM,即我们只能在 debug 版本的 JVM 中才能开启该参数。

这里有的读者可能会有一个疑问,即是不是可以通过使用参数 -XX:+IgnoreUnrecognizedVMOptions(该参数开启之后可以允许 JVM 使用一些在 release 版本中不被允许使用的参数)的方式,在正常 release 版本的 JVM 中使用 UseMallocOnly 参数,很遗憾虽然我们可以通过这种方式开启 UseMallocOnly,但是实际上 UseMallocOnly 却不会生效,因为在源码中其逻辑如下:

# hotspot/src/share/vm/memory/allocation.hpp
void* Amalloc(size_t x, AllocFailType alloc_failmode = AllocFailStrategy::EXIT_OOM) {
    assert(is_power_of_2(ARENA_AMALLOC_ALIGNMENT) , "should be a power of 2");
    x = ARENA_ALIGN(x);
 //debug 版本限制
 debug_only(if (UseMallocOnly) return malloc(x);)
 if (!check_for_overflow(x, "Arena::Amalloc", alloc_failmode))
 return NULL;
    NOT_PRODUCT(inc_bytes_allocated(x);)
 if (_hwm + x > _max) {
 return grow(x, alloc_failmode);
    } else {
 char *old = _hwm;
      _hwm += x;
 return old;
    }
  }

可以发现,即使我们成功开启了 UseMallocOnly,也只有在 debug 版本(debug_only)的 JVM 中才能使用 malloc 的方式分配内存。

我们可以对比下,使用正常版本(release)的 JVM 添加 -XX:+IgnoreUnrecognizedVMOptions -XX:+UseMallocOnly 启动参数的 NMT 相关日志与使用 debug(fastdebug/slowdebug)版本的 JVM 添加 -XX:+UseMallocOnly 启动参数的 NMT 相关日志:

# 正常 JVM ,启动参数添加:-XX:+IgnoreUnrecognizedVMOptions -XX:+UseMallocOnly
......
[0x0000ffffb7d16968] ChunkPool::allocate(unsigned long, AllocFailStrategy::AllocFailEnum)+0x158
[0x0000ffffb7d15f58] Arena::grow(unsigned long, AllocFailStrategy::AllocFailEnum)+0x50
[0x0000ffffb7fc4888] Dict::Dict(int (*)(void const*, void const*), int (*)(void const*), Arena*, int)+0x138
[0x0000ffffb85e5968] Type::Initialize_shared(Compile*)+0xb0
                             (malloc=32KB type=Arena Chunk #1)
......                             

# debug版本 JVM ,启动参数添加:-XX:+UseMallocOnly
......
[0x0000ffff8dfae910] Arena::malloc(unsigned long)+0x74
[0x0000ffff8e2cb3b8] Arena::Amalloc_4(unsigned long, AllocFailStrategy::AllocFailEnum)+0x70
[0x0000ffff8e2c9d5c] Dict::Dict(int (*)(void const*, void const*), int (*)(void const*), Arena*, int)+0x19c
[0x0000ffff8e97c3d0] Type::Initialize_shared(Compile*)+0x9c
                             (malloc=5KB type=Arena Chunk #1)
......                             

我们可以清晰地观察到调用链的不同,即前者还是使用 ChunkPool::allocate 的方式来申请内存,而后者则使用 Arena::malloc 的方式来申请内存,查看 Arena::malloc 代码:

# hotspot/src/share/vm/memory/allocation.cpp
void* Arena::malloc(size_t size) {
 assert(UseMallocOnly, "shouldn't call");
 // use malloc, but save pointer in res. area for later freeing
 char** save = (char**)internal_malloc_4(sizeof(char*));
 return (*save = (char*)os::malloc(size, mtChunk));
}

可以发现代码中通过 os::malloc 的方式来分配内存,同理释放内存时直接通过 os::free 即可,如 UseMallocOnly 中释放内存的相关代码:

# hotspot/src/share/vm/memory/allocation.cpp
// debugging code
inline void Arena::free_all(char** start, char** end) {
 for (char** p = start; p < end; p++) if (*p) os::free(*p);
}

虽然 JVM 为我们提供了两种方式来管理 Arena Chunk 的内存:

  1. 通过 ChunkPool 池化交由 JVM 自己管理;
  2. 直接通过 Glibc 的 malloc/free 来进行管理。

但是通常意义下我们只会用到第一种方式,并且一般 ChunkPool 管理的对象都比较小,整体来看 Arena Chunk 这块内存的使用不会很多。

Unknown

Unknown 则是下面几种情况

  • 当内存类别无法确定时;
  • 当 Arena 用作堆栈或值对象时;
  • 当类型信息尚未到达时。

NMT 无法追踪的内存

需要注意的是,NMT 只能跟踪 JVM 代码的内存分配情况,对于非 JVM 的内存分配是无法追踪到的。

  • 使用 JNI 调用的一些第三方 native code 申请的内存,比如使用 System.Loadlibrary 加载的一些库。
  • 标准的 Java Class Library,典型的,如文件流等相关操作(如:Files.list、ZipInputStream 和 DirectoryStream 等)。

可以使用操作系统的内存工具等协助排查,或者使用 LD_PRELOAD malloc 函数的 hook/jemalloc/google-perftools (tcmalloc) 来代替 Glibc 的 malloc,协助追踪内存的分配。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u012181546/article/details/127949664

智能推荐

Struts1.x工作原理概述_而strutsl.x的底层核心-程序员宅基地

文章浏览阅读1.7k次。一、Struts 的体系结构 二、Struts 的流程三、具体原理1、 Struts的核心是ActionServlet,它本质上就是一个Servlet,在Web.xml中配置ActionServlet为自动启动,指明Struts的配置文件所在位置,并指明Struts中工作条件servlet> servlet-name>strut_而strutsl.x的底层核心

eos虚拟机与智能合约详解与分析_多层级并行智能合约虚拟机架构-程序员宅基地

文章浏览阅读1k次。文章目录1. 相关背景知识1.1 LLVM相关内容1.1.1 LLVM架构概述1.1.2 LLVM IR介绍与分析1.1.2.1 LLVM IR指令集1.1.2.2 LLVM IR类型系统1.1.2.3 LLVM IR内存模型1.1.2.4 LLVM IR函数调用1.1.2.5 LLVM IR示例1.1.3 LLVM JIT介绍与分析1.1.3.1 LLVM JIT实现原理1.1.3.2 LLVM..._多层级并行智能合约虚拟机架构

Calculation of COGM and COGS-程序员宅基地

文章浏览阅读292次。UseYou can use the Product Cost Planning functions to calculate the cost of goods manufactured (COGM) and cost of goods sold (COGS) for products such as materials and services. The costs may then be..._calculation of sales & administration cost and cogs

一个适合于.NET Core的超轻量级工作流引擎:Workflow-Core-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏17次。一、关于Workflow-Core  近期工作上有一个工作流的开发需求,自己基于面向对象和职责链模式捣鼓了一套小框架,后来在github上发现一个轻量级的工作流引擎轮子:Workflow-Core,看完其wiki之后决定放弃之前自己造的轮子,使用这个开源项目来改造,也就有了这一篇博文。    Workflow-Core是一个基于.NET Standard的轻量级工作流引擎,其GitHu..._.net core workflow

懒女人的幸福生活-程序员宅基地

文章浏览阅读176次。懒女人的幸福生活懒女人,哈哈,当我听到这个词的时候,我的眼前立即出现了一幅画面“一个头发乱蓬蓬的女人,面带倦容,衣服穿得一点也不整洁,房间的床上和沙发上丢得都是平日换下的衣服,还有四处可见不用的生活用品……”晕,不要了,还是不要再想象了!   老公常说我是一个小女人,说我是一个喜欢打扮自己的小女人。不过我也喜欢装饰自己的幸福小家,打份自已和装饰自己的家这两件事..._懒女人的幸福生活下一站幸福

typdef:深入理解C语言中typdef关键词的用法-程序员宅基地

文章浏览阅读451次,点赞7次,收藏8次。假设我们有一个复杂的结构体,我们希望在代码中多次使用它,但又不想每次都写那么长的名字。int x;int y;} Point;现在,我们可以简单地使用Point来代替整个结构体的定义。函数指针的定义通常很长,使用typedef可以让代码更加整洁。现在,我们可以使用来代替。枚举类型在C语言中非常有用,但它们的名称可能会很长。现在,我们可以使用Color来代替整个枚举的定义。typedef。

随便推点

maven gatling 运行_gatling+maven-程序员宅基地

文章浏览阅读1.1k次。pom.xml

Springboot计算机毕业设计基于微信小程序的网络办公系统【附源码】开题+论文+mysql+程序+部署-程序员宅基地

文章浏览阅读862次,点赞12次,收藏12次。基于微信小程序的网络办公系统的研究,不仅有助于推动企业的数字化转型,提升工作效率,还能优化员工的办公体验,增强企业的竞争力。通过该系统,企业可以实现员工信息的集中管理、员工打卡的自动化处理、员工工资的电子化核算等功能,从而简化管理流程,减少人力成本。因此,开发一款基于微信小程序的网络办公系统,以满足企业对于高效、便捷办公的需求,具有重要的现实意义和应用价值。本研究的最终目标是帮助企业实现办公流程的数字化、自动化和智能化,提升企业的管理水平和运营效率,推动企业的可持续发展。最后,进行系统的部署和上线。

数据转换:将数据转换为不同的格式-程序员宅基地

文章浏览阅读489次,点赞7次,收藏7次。1.背景介绍1. 背景介绍在现代信息化时代,数据是成为企业竞争力的关键因素之一。数据的质量和可用性直接影响企业的决策能力和运营效率。因此,数据转换成为了一项重要的技术。数据转换的目的是将数据从一个格式转换为另一个格式,以满足不同的应用需求。数据转换的应用场景非常广泛,包括但不限于:数据库迁移:将数据从一个数据库系统迁移到另一个数据库系统。数据清洗:将数据从原始格式转换为有效的数..._数据格式转换软件技术要求怎么写

XTuner 大模型单卡低成本微调实战笔记_llm微调的目的-程序员宅基地

文章浏览阅读909次,点赞17次,收藏24次。大语言模型是在海量的文本内容基础上,以无监督或半监督方式进行训练的,海量的文本内容赋予了大模型各种各样的行业知识,但是如果直接把大模型的知识用于生产实践,会发现回答不大满意,微调的目的就是让LLM在具体的场景或领域中能够得到更好的回答。常用微调模式:增量预训练和指令跟随增量预训练:给模型投喂一些新的知识,新知识是目标领域的文本内容,经过新知识学习,模型即可获得该领域更好的效果。使用场景: 让基座模型学习到一些新知识,如某个垂类领域的常识训练数据:文章、书籍、代码等指令跟随微调。_llm微调的目的

macos安装electron和electron-builder报错问题_command not found: electron-builder-程序员宅基地

文章浏览阅读1.3k次。最近公司需求,需要做一个桌面端,其中需要用到electron-builder这个插件,安装的时候使用指令"vue add electron-builder",出现报错。报这个错是因为这个插件需要Node版本在14+以上,我们升级下Node版本就行了,这里我用的是naode v14.15.1的版本,这个版本也是很稳定的。这个问题出现的原因是电脑没有安装vue脚手架导致,使用vue --version查一下版本,发现还是识别不出这个指令无法检测,这里用“安装一下便可,如果没有效果卸载旧的,使用指令"_command not found: electron-builder

COMP4300/8300 Parallel Systems Assignment 1, 2024-程序员宅基地

文章浏览阅读605次,点赞29次,收藏13次。【代码】COMP4300/8300 Parallel Systems Assignment 1, 2024。