compare java memory profiling with go

179 阅读2分钟

在 go 中,内存分配非常简单,每 512 kb 记录当前调用栈,分配的字节数。

性能分析也很简单:

  • 通过减少调用栈出现的次数来减少 cpu 消耗。
  • 通过减少分配的字节数来减少内存分配。

tlab 内存分配

java 的内存分配有个非常不同的特点是, java 有个叫做 tlab 的区域,从 tlab 中分配内存是不需要加锁的。 java 会发送两类事件回调:

  • ObjectAllocationInNewTLAB, 分配一个新的 tlab。
  • ObjectAllocationOutSideTLAB, 在 tlab 之外分配内存。

在 jdk 11+ 之后,java 支持了 SetHeapSamplingInterval 函数,这个实现和 go 的非常相似,都是每 N KB 去记录一次内存分配事件。

其中,对于 ObjectAllocationInNewTLAB 来说,虽然新分配了一个 tlab, 但是这个 tlab 是会被旧 tlab 的内容填充的, 因此,我们不应该用 tlab size 来计算 N KB, 而应该用 tlab 自从上次采样以来新增的内存 tlab.bytes_since_last_sample_point。

  • ObjectAllocationInNewTLAB, tlab.bytes_since_last_sample_point + alloc_bytes
  • ObjectAllocationOutSideTLAB, alloc_bytes.

由于我们忽略了 tlab 内的内存分配事件,因此可以预见相同的内存分配, java 所产生的性能事件要小于 go。

jdk 11 以下的 jdk

对于 jdk 11 以下的,由于不支持 SetHeapSamplingInterval 函数,我们只能通过接受 send_allocation_in_new_tlab_event 来实现每 N KB 记录一次调用栈的功能:

send_allocation_in_new_tlab_event(KlassHandle klass, size_t tlab_size, size_t alloc_size)

此时我们只能通过 tlab_size 去计算 N KB, 由于 tlab size > tlab.bytes_since_last_sample_point, 因此可以猜测,对于 jdk 11 以下的版本, 真实产生的性能事件会比 jdk 11+ 更多。

并且在 async-profiler 目前的实现中, N KB 的计数不像 SetHeapSamplingInterval 一样是一个柏松分布,而仅仅是一个累加的数字。

非累计分配

java 致命的一点是,它的内存分配不像 go 是累计的。

以 1min 为粒度,一个 java 8 程序产生几十次内存分配,因为样本数过小, 很难从采样的数字去恢复原来真实的结果。