jdk二次开发解决日常直接内存问题排查
背景
jvm直接内存越来越被广泛的使用,常见于网络操作和一些内存优化策略,堆内转堆外。但是在实际使用这个区域的时候,带来了问题排查的困难。困难主要来自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种解决方法对应的场景也不一样。
转载自:https://juejin.cn/post/7352075719150927910