XSocket内存泄漏问题深度分析
大概一个月前在一个数据迁移的过程中,在数据迁移到900多W的时候程序崩溃了,系统最后记录的日志是这样的:
Exceptioninthread"xDispatcher#CLIENT"java.lang.OutOfMemoryError
atsun.misc.Unsafe.allocateMemory(NativeMethod)
atjava.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99)
atjava.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
atorg.xsocket.connection.spi.AbstractMemoryManager.newBuffer(AbstractMemoryManager.java:219)
at.......
从中不难看出这是xsocket的内存管理层程序通过JVM的nio创建DirectByteBuffer时抛出了错误。在这里先要说明一下的是,当时的系统使用的xsocket2.0版,2.0版连接读取数据默认就是使用DirectByteBuffer的,目前2.4.X版已经默认都改为使用ByteBuffer,而不再是DirectByteBuffer了。
DirectByteBuffer是由jvm调用jni程序在操作系统内存中分配的空间的,不需要占用JVM的HeapSize。当程序需要读取BigSize或者HugeSize数据的时候,使用DirectByteBuffer优势尤其明显。但使用DirectByteBuffer会产生一个问题,当JVM的空间还没满但系统内存空间已经被消耗的差不多的时候,gc如何被触发呢。这个问题在一个叫Harmony的开源JavaSE项目的maillist中曾进行过热烈的讨论。参考资料1中有当时讨论过程的链接。
在跟踪JVM的一些源码后,发现,很庆幸,sun的jvm已经解决这个问题了。但是对于尚不清楚其内存处理机制的开发人员来说。在使用DirectByteBuffer是还是很可能把系统置身于莫大的风险中。而很不幸这个问题隐藏的相当的隐秘,对于不明就里的人,最后可能只好采取定时重启系统或者在系统中设定一些条件显式调用gc来曲线解决资源释放的问题。
题外话说了不少,下面我们直接从JVM的一些代码片段去分析文章最开始的Exception是在什么条件下引发的。首先我们来看看DirectByteBuffer的构建函数代码:
DirectByteBuffer(intcap){
super(-1,0,cap,cap,false);
Bits.reserveMemory(cap);
intps=Bits.pageSize();
longbase=0;
try{
base=unsafe.allocateMemory(cap+ps);
}catch(OutOfMemoryErrorx){
Bits.unreserveMemory(cap);
throwx;
}
unsafe.setMemory(base,cap+ps,(byte)0);
if(base%ps!=0){
//Rounduptopageboundary
address=base+ps-(base&(ps-1));
}else{
address=base;
}
cleaner=Cleaner.create(this,newDeallocator(base,cap));
}
注意以上片段中红色加亮的部分,然后我们再来看看Bits.reserveMemory这个方法的代码:
//--Directmemorymanagement--
//Auser-settableupperlimitonthemaximumamountofallocatable
//directbuffermemory.ThisvaluemaybechangedduringVM
//initializationifitislaunchedwith"-XX:MaxDirectMemorySize=<size>".
privatestaticvolatilelongmaxMemory=VM.maxDirectMemory();
privatestaticvolatilelongreservedMemory=0;
privatestaticbooleanmemoryLimitSet=false;
//Thesemethodsshouldbecalledwheneverdirectmemoryisallocatedor
//freed.Theyallowtheusertocontroltheamountofdirectmemory
//whichaprocessmayaccess.Allsizesarespecifiedinbytes.
staticvoidreserveMemory(longsize){
synchronized(Bits.class){
if(!memoryLimitSet&&VM.isBooted()){
maxMemory=VM.maxDirectMemory();
memoryLimitSet=true;
}
if(size<=maxMemory-reservedMemory){
reservedMemory+=size;
return;
}
}
System.gc();
try{
Thread.sleep(100);
}catch(InterruptedExceptionx){
//Restoreinterruptstatus
Thread.currentThread().interrupt();
}
synchronized(Bits.class){
if(reservedMemory+size>maxMemory)
thrownewOutOfMemoryError("Directbuffermemory");
reservedMemory+=size;
}
}
从以上代码我们可以看到,JVM在通过DirectByteBuffer使用OS内存时(无论是分配还是释放),是有统计的,通过跟可使用的最大OS内存(VM.maxDirectMemory())作比较,如果不够用,那显式调用gc,如果经过gc后还是没有足够的空分配内存,那么从Bit抛出Exception。注意:这一步并未真正的像OS申请内存,只是VM通过计算得出的结论。而真正想OS申请内存是在unsafe.allocateMemory这个方法里面通过JNI实现的。显然,文章最初的Exception是由Unsafe抛出而不是Bit抛出。也就是说JVM认为OS还有足够的可分配内存,而当JVM真正向OS申请内存分配的时候却出错了。那么接下来我们就得看看,这个maxdirectmemory值是如何设定的。
现在我们看看VM.java的代码:
privatestaticlongdirectMemory=67108864L;
...
publicstaticlongmaxDirectMemory()
{
if(booted)
returndirectMemory;
PropertieslocalProperties=System.getProperties();
Stringstr=(String)localProperties.remove("sun.nio.MaxDirectMemorySize");
System.setProperties(localProperties);
if(str!=null)
if(str.equals("-1"))
{
directMemory=Runtime.getRuntime().maxMemory();
}else{
longl=Long.parseLong(str);
if(l>-1L)
directMemory=l;
}
returndirectMemory;
}
可以看出来,maxdirectmemory可以有三种值:
1)默认值,64M;
2)maxMemory,也就是通过-Xmx设定的值,默认也是64M;
3)通过-XX:MaxDirectMemorySize=<size>指定的值;
问题到这里基本就是水落石出了,当时系统启动的时候设定-Xmx2048M,未指定MaxDirectMemorySize,也就是说maxdirectmemory被认为是2048M,整个系统的物理内存为4G,除掉系统进程占用的内存,剩下的物理内存加上swap空间也就接近3G。设想JVM的heapsize占用了1.5G,directmemory使用了1.5G,这时候程序申请一200M的direct内存,在这种情况下无论是JVMheapsize还是directmemory不满足触发gc的条件,于是jvm向os申请分配内存,OS无可分配的内存了就会抛出OOM错误。
补充说明一下:在OS已经把物理内存+Swap都耗光都不足够分配内存空间的时候,不同OS可能会有不同的表现。LInux的内核有可能会尝试努力腾出更多的内存空间。可能会杀掉某些进程。(这是参考资料一中IvanVolosyuk所提出来的问题)。而我在使用以下程序进行测试时,出现整个OS系统假死的状况,过一段时间回复过来了。但整个过程可以肯定的一件事是gc始终没有被触发到。
以下是我用来验证我的以上分析的测试程序:
importjava.lang.management.ManagementFactory;
importjava.nio.ByteBuffer;
importjava.util.logging.Logger;
importcom.sun.management.OperatingSystemMXBean;
publicclassTestMemoryLeak{
privatestaticLoggerlogger=Logger.getAnonymousLogger();
publicstaticvoidmain(String[]args)throwsException{
OperatingSystemMXBeanosmb=(OperatingSystemMXBean)ManagementFactory
.getOperatingSystemMXBean();
System.out.println("Totalphysicmemory:"
+osmb.getTotalPhysicalMemorySize()/1024/1024+"MB");
System.out.println("Freephysicmemory:"
+osmb.getFreePhysicalMemorySize()/1024/1024+"MB");
System.out.println("Maxmemory:"+Runtime.getRuntime().maxMemory());
System.out
.println("Totalmemory:"+Runtime.getRuntime().totalMemory());
System.out.println("Freememory:"+Runtime.getRuntime().freeMemory());
System.out.println("====================================");
//testDirectByteBuffer();
testByteBuffer();
System.out.println("====================================");
System.out.println("Freephysicmemory:"
+osmb.getFreePhysicalMemorySize()/1024/1024+"MB");
}
publicstaticvoidtestDirectByteBuffer(){
try{
ByteBufferbb1=ByteBuffer.allocateDirect(1024*100000*4);
bb1=null;
System.out.println("Freememory:"+Runtime.getRuntime().freeMemory());
ByteBufferbb2=ByteBuffer.allocateDirect(1024*100000*5);
bb2=null;
System.out.println("Freememory:"+Runtime.getRuntime().freeMemory());
//System.gc();
//pauseexpectforgc
Thread.sleep(1000*6);
ByteBufferbb3=ByteBuffer.allocateDirect(1024*100000*;
System.out.println("Freememory:"+Runtime.getRuntime().freeMemory());
}catch(Exceptione){
e.printStackTrace();
}
}
publicstaticvoidtestByteBuffer(){
try{
ByteBufferbb1=ByteBuffer.allocate(1024*100000*4);
bb1=ByteBuffer.allocate(1024*10*4);
System.out.println("Freememory:"+Runtime.getRuntime().freeMemory());
ByteBufferbb2=ByteBuffer.allocate(1024*100000*3);
bb1=ByteBuffer.allocate(1024*10*3);
System.out.println("Freememory:"+Runtime.getRuntime().freeMemory());
//System.gc();
//pauseexpectforgc
Thread.sleep(1000*6);
ByteBufferbb3=ByteBuffer.allocate(1024*100000*6);
System.out.println("Freememory:"+Runtime.getRuntime().freeMemory());
}catch(Exceptione){
e.printStackTrace();
}
}
}
程序启动需用如下命令:
java-verbose:gc-Xms64M-Xmx512M-XX:MaxDirectMemorySize=1000MTestMemoryLeak
-verbose:gc用于开启gc运行情况的输出,可以帮助我们了解jvm垃圾收集的运作情况;
其他参数值应该你机器的实际情况设定。
最后我想总结的是,当我们在使用DirectByteBuffer的时候一定要注意:
1)谨慎设定JVM运行参数,最好用-XX:MaxDirectMemorySize进行设定,否则你就得清楚你设定的mx不单单制定了heapsize的最大值,它同时也是directmemory的最大值;
2)在密集型访问中(例如数据迁移工具)适当增加对gc的显式调用,保证资源的释放;
参考资料:
[VM]HowtotrigueGCwhilefreenativememoryislow.
http://mail-archives.apache.org/mod_mbox/harmony-dev/200702.mbox/%[email protected]%3E