JVM coredump分析系列(3):JNA 5.10.0升级到5.12.1进程core分析原创
0. 前言
笔者近期把JNA从5.10.0升级到5.12.1以后,发现进程会core,且hs_error中的堆栈全部是JDK的内部函数,很难分析出和JNA升级的关联。
以下是堆栈信息:
Stack: [0x00007f5602132000,0x00007f5602233000], sp=0x00007f560222f230, free space=1012k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
V [libjvm.so+0x5f35ec] Dictionary::find(int, unsigned int, Symbol*, ClassLoaderData*, Handle, Thread*)+0x9c
V [libjvm.so+0xbcd8c2] SystemDictionary::resolve_instance_class_or_null(Symbol*, Handle, Handle, Thread*)+0x212
V [libjvm.so+0xbd0aa2] SystemDictionary::resolve_or_fail(Symbol*, Handle, Handle, bool, Thread*)+0x62
V [libjvm.so+0x57c8f6] ConstantPool::klass_at_impl(constantPoolHandle, int, Thread*)+0x266
V [libjvm.so+0x57f894] ConstantPool::resolve_constant_at_impl(constantPoolHandle, int, int, Thread*)+0x7b4
V [libjvm.so+0x5802ba] ConstantPool::resolve_bootstrap_specifier_at_impl(constantPoolHandle, int, Thread*)+0xea
V [libjvm.so+0x932607] LinkResolver::resolve_invokedynamic(CallInfo&, constantPoolHandle, int, Thread*)+0x5f7
V [libjvm.so+0x93b11d] LinkResolver::resolve_invoke(CallInfo&, Handle, constantPoolHandle, int, Bytecodes::Code, Thread*)+0x26d
V [libjvm.so+0x7672ae] InterpreterRuntime::resolve_invokedynamic(JavaThread*)+0x28e
后经过JNA升级的源码走读得以解决,本文主要针对相关源码进行分析。
1. 问题分析
出问题的代码如下:
Memory dbIdPoint = new Memory(Native.getNativeSize(Integer.class));
...
freeMem(dbIdPoint);
// freeMem定义
public static void freeMem(Pointer pointer) {
Native.free(Pointer.nativeValue(pointer));
Pointer.nativeValue(pointer, 0);
}
5.10.0版本中,在Memory
的finalize
函数,在Memory
对象GC时,会先判断内存指针peer
是否未空指针,如果不为空,则释放内存,并把内存指针peer
置零,也就是GC的时候Memory
相关的内存就会释放。
protected void finalize() {
this.dispose();
}
protected synchronized void dispose() {
if (this.peer != 0L) {
try {
free(this.peer);
} finally {
this.peer = 0L;
this.reference.unlink();
}
}
}
到了5.12.0时,JNA为了提升并发性能,把Memory
、CallbackReference
和NativeLibrary
的finilizer
方法去掉了,引入cleaner来释放内存,具体见changelog (https://github.com/java-native-access/jna/blob/master/CHANGES.md)。
-
Clearner是一个单例,维护一个后台线程
cleanerThread
和一个队列referenceQueue
,后台线程从队列中移除对象,并调用这个对象的clean()
方法。public class Cleaner {
private final ReferenceQueue<Object> referenceQueue;
private final Thread cleanerThread;
private Cleaner() {
referenceQueue = new ReferenceQueue<Object>();
cleanerThread = new Thread() {
@Override
public void run() {
while(true) {
try {
Reference<? extends Object> ref = referenceQueue.remove();
if(ref instanceof CleanerRef) {
((CleanerRef) ref).clean();
}
} catch (InterruptedException ex) {
Logger.getLogger(Cleaner.class.getName()).log(Level.SEVERE, null, ex);
break;
} catch (Exception ex) {
Logger.getLogger(Cleaner.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
};
cleanerThread.setName("JNA Cleaner");
cleanerThread.setDaemon(true);
cleanerThread.start();
}
} -
创建
Memory
对象时,调用Cleaner
的register
方法,把自己和释放peer的方法MemoryDisposer传入。public Memory(long size) {
// ...
cleanable = Cleaner.getCleaner().register(this, new MemoryDisposer(peer));
}
private static final class MemoryDisposer implements Runnable {
private long peer;
public MemoryDisposer(long peer) {
this.peer = peer;
}
@Override
public synchronized void run() {
try {
free(peer);
} finally {
allocatedMemory.remove(peer);
peer = 0;
}
}
} -
register
方法会创建一个CleanerRef
,它是一个PhantomReference
对象,并以referenceQueue
作为队列,PhantomReference的特点是,在obj GC的时候不会直接被释放掉,而是放入到referenceQueue
中。public synchronized Cleanable register(Object obj, Runnable cleanupTask) {
// The important side effect is the PhantomReference, that is yielded
// after the referent is GCed
return add(new CleanerRef(this, obj, referenceQueue, cleanupTask));
}
2. 解决思路
把整个Memory申请与释放的过程画出来,如下图:
可以看出来在5.12.0中,有两个地方会释放内存peer:
-
在业务代码里,通过 freeMem
方法主动释放。 -
Memory对象被GC的时候,由cleaner调用 MemoryDisposer#run()
方法释放内存。由于MemoryDisposer
对象的字段peer
是在创建Memory
的时候设置,原来释放内存的方法,是不会修改MemoryDisposer
的peer
的值,因此会导致GC后再次释放该内存,导致进程core。
而5.10.0没有MemoryDisposer这个对象,因此不会发生内存重复释放的问题。
在5.12.0中,可以参考如下思路进行问题解决:
// 错误的做法,只把Pointer中的peer置0
public static void freeMem(Pointer pointer) {
Native.free(Pointer.nativeValue(pointer));
Pointer.nativeValue(pointer, 0);
}
// 正确的做法,会调用MemoryDisposer#run,把Pointer的peer以及MemoryDisposer的peer都置0
public static void freeMem(Memory memory) {
memory.close();
}
新的写法调用Memory#close
方法,会调用到com.sun.jna.Memory$MemoryDisposer#run
方法把Pointer
的peer
以及MemoryDisposer
的peer
都置0,保证内存不会重复释放。
3. 验证
通过上述修改,我们的进程成功拉起,JNA终于成功升级了。
另外为了更充分地验证我们的理论,我们写出了下面的Demo,希望可以给大家一个更直观的认识。
public class JnaDoubleFreeCore {
public static void freeMem(Memory pointer) {
Native.free(Pointer.nativeValue(pointer));
Pointer.nativeValue(pointer, 0);
}
public static void main(String[] args) throws InterruptedException {
while (true) {
Memory memory = new Memory(1024);
freeMem(memory);
memory = new Memory(1024);
System.gc();
Thread.sleep(1000);
memory.setInt(0, 1000);
System.out.println("----");
}
}
}
在linux上运行上面的代码,我们可以看到如下报错,显示内存被重复释放,进程coredump;修复freeMem后,问题解决。
----
free(): double free detected in tcache 2
Aborted (core dumped)
这时候可能有人要问,为什么这个demo的core是double free,而本文开头的堆栈是jvm的堆栈。那是因为业务程序比较复杂,有很多并发,内存被释放后很快被jvm重新申请用于别的用途,再次free不会造成double free,而是把jvm正在使用的内存给释放了,最终造成jvm运行的过程中内存的读写异常引起coredump。
Compiler SIG 专注于编译器领域技术交流探讨和分享,包括 GCC/LLVM/OpenJDK 以及其他的程序优化技术,聚集编译技术领域的学者、专家、学术等同行,共同推进编译相关技术的发展。
扫码添加 SIG 小助手微信,邀请你进 Compiler SIG 微信交流群。