掘金 后端 ( ) • 2024-03-31 10:36

背景

jvm直接内存越来越被广泛的使用,常见于网络操作和一些内存优化策略,堆内转堆外。但是在实际使用这个区域的时候,带来了问题排查的困难。困难主要来自2个地方。

  1. 并不是堆内的对象,无法直接使用堆的分析方式。
  2. 这个区域会报oom,如果是第三方框架,可能会吞掉异常,导致无法获取到真实的现场状态。

基于这2个问题,我们看看如何才能解决。

直接内存分析方式

java直接内存设计

java的直接内存从api设计是通过DirectByteBuffer对象来代理堆内,数据放在堆外。


DirectByteBuffer(int cap) {                   // package-private

    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);

    long base = 0;
    try {
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;



}

从java的设计可以看出,DirectByteBuffer在构造的时候,通过unsafe申请堆外的内存,构建Cleaner清理对象。


public class Cleaner extends PhantomReference<Object>


Cleaner是个虚引用。也就是说DirectByteBuffer对象被gc掉之后,可以获取到Cleaner对象的引用。这里也能理解他后面传递的Deallocator。



private Deallocator(long address, long size, int capacity) {
    assert (address != 0);
    this.address = address;
    this.size = size;
    this.capacity = capacity;
}

public void run() {
    if (address == 0) {
        // Paranoia
        return;
    }
    unsafe.freeMemory(address);
    address = 0;
    Bits.unreserveMemory(size, capacity);
}

Deallocator的run方法就是通过unsafe释放内存。

虚引用只有在对象被gc之后才能从Referencequeue里获取。这就导致了直接内存不够用的时候,需要有个担保机制--主动触发一次GC。

static void reserveMemory(long size, int cap) {
    // trigger VM's Reference processing
System.gc();
}

分析方法

在了解了实现机制之后。我们明白了DirectByteBuffer就是直接内存的1:1写照。虽然堆外的字节流无法分析,但是可以分析堆内的DirectByteBuffer引用以及申请空间的大小,可以客观的反映某次的操作,结合日志上下文,基本可以解决问题。

现在就需要刻画DirectByteBuffer的信息,有capacity,有cleaner。只要从heapdump中提出这些信息就可以分析直接内存的分布以及java的引用关系。

分析方法的主要途径就是heapdump。

heapdump获取

第一种是直接用jmap获取。这种就需要观测到现场的时候主动触发。

第二种是自动获取,oom的时候自动获取dump。但其实在jdk中这种方式是不支持的。

虽然有很迷惑性的参数存在。

 -XX:+HeapDumpOnOutOfMemoryError
 -XX:HeapDumpPath=. 

但是这里的HeapDumpOnOutOfMemoryError并不包括直接内存。直接内存抛出的是一个java的oome。

  product(bool, HeapDumpOnOutOfMemoryError, false, MANAGEABLE,              \
          "Dump heap to file when java.lang.OutOfMemoryError is thrown "    \
          "from JVM")    

jdk的定义是从jvm内部抛出的OutOfMemoryError。

解决方案

虽然社区并不支持,如果直接内存申请代码是自己写的,可以自己捕获oome,然后通过jmx触发heapdump也可以实现类似的效果。

但是如果是使用的第三方框架。框架层吞掉了异常,这就无解了,毕竟是第三方的代码,我们使用也只是maven引入的。总不能为了实现这个功能把第三方的源码引入。

这里就引入了另外一种解法,对jdk做二次开发。在直接内存申请抛出oom的时候,产生dump,可以配合HeapDumpOnOutOfMemoryError参数来做。

代码可以参考我的github 提交记录 8318058: Notify the jvm when the direct memory is oom by xpbob · Pull Request #16176 · openjdk/jdk (github.com)

这个代码并没有被社区接受,社区对直接内存的定位有从jdk层面的角度。和我们作为用户使用有区别,详细可以看社上面链接社区的讨论。

总结

直接内存的的分析可以转化为heapdump分析DirectByteBuffer,触发dump的方式可以选择jmap,代码jmx和二次开发jdk。3种解决方法对应的场景也不一样。