免责声明:本回答基于JDK 18。我所写的大部分内容应该适用于旧版本。如有疑问,请自行测量。
背景:
NMT跟踪热点VM内存使用情况和通过直接字节缓冲器进行的内存使用情况。基本上,它钩取malloc/free和mmap/munmap调用并进行记账。
它没有跟踪其他JDK内存使用情况(热点VM之外)或第三方库使用情况。这一点很重要,因为它使得NMT的行为有些可预测。Hotspot试图避免通过malloc进行细粒度的分配。相反,它依赖于自定义的内存管理器,如Arenas、代码堆或Metaspace。
因此,对于大多数应用程序,hotspot的mallocs/mmaps不是那么“热门”,malloc的数量也不是那么大。
在本文的其余部分中,我将把重点放在malloc/free上,因为它们的数量远远超过了mmaps/munmaps:
内存成本:
- (detail+summary):每个malloc()两个单词
- (仅详细信息):Malloc站点表
- (detail+summary):VM映射和线程堆栈的映射列表
在这里,(1) 完全压倒了 (2) 和 (3)。因此,概述模式和详细模式之间的内存开销并不重要。
请注意,即使 (1) 可能也不那么重要,因为底层的libc分配器已经规定了一个最小的分配大小,可能比(纯分配大小+ 16字节malloc头)要大。因此,NMT内存开销实际上转化为RSS增加量的多少需进行测量。
由于JVM内存成本通常由堆大小主导,因此无法回答这意味着多少内存开销的总量。因此,将RSS与NMT成本进行比较几乎没有意义。但只是举个例子,对于具有1GB预先触碰堆的Spring PetClinic,NMT内存开销约为0.5%。
在我的经验中,NMT内存开销只在病态情况或引起JVM进行大量细粒度分配的角落情况下才会有影响。例如,如果执行大量类加载,则需要启用NMT以查看发生了什么。但通常这些都是你想启用NMT的情况。
性能成本:
NMT确实需要一些同步。它在概述模式下原子地增加每个malloc/free上的计数器。
在详细模式下,它做了更多的工作:
- 捕获malloc站点上的调用堆栈
- 在哈希映射中查找调用堆栈
- 增加哈希映射条目计数器
这需要更多的周期。哈希映射是无锁的,但仍然使用原子操作进行修改。如果hotspot从不同的线程进行许多malloc,它看起来很昂贵。它真的有那么糟糕吗?
最坏情况NMT off: 6 secs
NMT summary: 34 secs
NMT detail: 46 secs
看起来很疯狂。但实际上可能并不重要,因为这不是现实生活中的例子。
Spring宠物诊所启动,平均十次运行:
NMT off: 3.79 secs
NMT summary: 3.79 secs (+0%)
NMT detail: 3.91 secs (+3%)
所以,这里情况还不错。总结模式的成本实际上在测试噪音中消失了。
文艺复兴,哲学家基准
有趣的例子,因为它进行了大量的同步,导致许多对象监视器被膨胀,并且这些对象被动态分配内存:
平均基准分数:
NMT off: 4697
NMT summary: 4599 (-2%)
NMT detail: 4190 (-11%)
在两个其他示例之间。
结论
没有明确的答案。
内存和性能成本都取决于JVM进行了多少次分配。
对于正常的行为良好的应用程序,这个数量很小,但在病理情况(例如JVM错误)以及由用户程序引起的某些边缘情况场景中可能会很大,例如大量的类加载或同步。要真正确定,您需要自己进行测量。