XSocket内存泄漏问题深度分析


大概一个月前在一个数据迁移的过程中,在数据迁移到900多W的时候程序崩溃了,系统最后记录的日志是这样的:

Exception in thread "xDispatcher#CLIENT" java.lang.OutOfMemoryError
         at sun.misc.Unsafe.allocateMemory(Native Method)
         at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:99)
         at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
         at org.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的Heap Size。当程序需要读取Big Size或者Huge Size数据的时候,使用DirectByteBuffer优势尤其明显。但使用DirectByteBuffer会产生一个问题,当JVM的空间还没满 但系统内存空间已经被消耗的差不多的时候,gc如何被触发呢。这个问题在一个叫Harmony的开源Java SE项目的mail list中曾进行过热烈的讨论。参考资料1中有当时讨论过程的链接。
在跟踪JVM的一些源码后,发现,很庆幸,sun的jvm已经解决这个问题 了。但是对于尚不清楚其内存处理机制的开发人员来说。在使用DirectByteBuffer是还是很可能把系统置身于莫大的风险中。而很不幸这个问题隐 藏的相当的隐秘,对于不明就里的人,最后可能只好采取定时重启系统或者在系统中设定一些条件显式调用gc来曲线解决资源释放的问题。

题外话说了不少,下面我们直接从JVM的一些代码片段去分析文章最开始的Exception是在什么条件下引发的。首先我们来看看DirectByteBuffer的构建函数代码:

  

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

注意以上片段中红色加亮的部分,然后我们再来看看Bits.reserveMemory这个方法的代码:

  

// -- Direct memory management --

     // A user-settable upper limit on the maximum amount of allocatable
     // direct buffer memory.  This value may be changed during VM
     // initialization if it is launched with "-XX:MaxDirectMemorySize=<size> ".
     private static volatile long maxMemory = VM.maxDirectMemory(); 
     private static volatile long reservedMemory = 0;
     private static boolean memoryLimitSet = false;

     // These methods should be called whenever direct memory is allocated or
     // freed.  They allow the user to control the amount of direct memory
     // which a process may access.  All sizes are specified in bytes.
     static void reserveMemory(long size) {
         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 (InterruptedException x) {
             // Restore interrupt status
             Thread.currentThread().interrupt();
         }
         synchronized (Bits.class) {
             if (reservedMemory + size > maxMemory)
             throw new OutOfMemoryError("Direct buffer memory");
             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申请内存分配的时候却出错了。那么接下来我们就得看看,这个max direct memory值是如何设定的。

现在我们看看VM.java的代码:

private static long directMemory = 67108864L; 
   ...
   public static long maxDirectMemory()
   {
     if (booted)
       return directMemory;
     Properties localProperties = System.getProperties();
     String str = (String)localProperties.remove("sun.nio.MaxDirectMemorySize");
     System.setProperties(localProperties);
     if (str != null)
       if (str.equals("-1"))
       {
         directMemory = Runtime.getRuntime().maxMemory(); 
       } else {
         long l = Long.parseLong(str); 
         if (l > -1L)
           directMemory = l;
       }
     return directMemory;
   }

可以看出来,max direct memory可以有三种值:
1)默认值,64M;
2)maxMemory,也就是通过-Xmx设定的值,默认也是64M;
3)通过-XX:MaxDirectMemorySize=<size>指定的值;

问 题到这里基本就是水落石出了,当时系统启动的时候设定-Xmx2048M,未指定MaxDirectMemorySize,也就是说max direct memory被认为是2048M,整个系统的物理内存为4G,除掉系统进程占用的内存,剩下的物理内存加上swap空间也就接近3G。设想JVM的 heap size占用了1.5G,direct memory使用了1.5G,这时候程序申请一200M的direct内存,在这种情况下无论是JVM heap size还是direct memory不满足触发gc的条件,于是jvm向os申请分配内存,OS无可分配的内存了就会抛出OOM错误。

补 充说明一下:在OS已经把物理内存+Swap都耗光都不足够分配内存空间的时候,不同OS可能会有不同的表现。LInux的内核有可能会尝试努力腾出更多 的内存空间。可能会杀掉某些进程。(这是参考资料一中Ivan Volosyuk所提出来的问题)。而我在使用以下程序进行测试时,出现整个OS系统假死的状况,过一段时间回复过来了。但整个过程可以肯定的一件事是 gc始终没有被触发到。

以下是我用来验证我的以上分析的测试程序:

import java.lang.management.ManagementFactory;
 import java.nio.ByteBuffer;
 import java.util.logging.Logger;

 import com.sun.management.OperatingSystemMXBean;

 public class TestMemoryLeak {
     private static Logger logger = Logger.getAnonymousLogger();

     public static void main(String[] args) throws Exception{
         OperatingSystemMXBean osmb = (OperatingSystemMXBean) ManagementFactory
                 .getOperatingSystemMXBean();
         System.out.println("Total physic memory:"
                 + osmb.getTotalPhysicalMemorySize() / 1024 / 1024 + "MB");
         System.out.println("Free physic memory:"
                 + osmb.getFreePhysicalMemorySize() / 1024 / 1024 + "MB");
         System.out.println("Max memory:" + Runtime.getRuntime().maxMemory());
         System.out
                 .println("Total memory:" + Runtime.getRuntime().totalMemory());
         System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
         System.out.println("====================================");
         
         //testDirectByteBuffer();
         testByteBuffer();
         
         System.out.println("====================================");
         System.out.println("Free physic memory:"
                 + osmb.getFreePhysicalMemorySize() / 1024 / 1024 + "MB");
     }
     
     public static void testDirectByteBuffer(){
         try{
             ByteBuffer bb1 = ByteBuffer.allocateDirect(1024 * 100000 * 4);
             bb1 = null;
             System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
             ByteBuffer bb2 = ByteBuffer.allocateDirect(1024 * 100000 * 5);
             bb2 = null;
             System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
             //System.gc();
             //pause expect for gc
             Thread.sleep(1000*6);
             ByteBuffer bb3 = ByteBuffer.allocateDirect(1024 * 100000 * 8);
             System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());    
         }catch(Exception e){
             e.printStackTrace();
         }
     }    
     public static void testByteBuffer(){
         try{
             ByteBuffer bb1 = ByteBuffer.allocate(1024 * 100000 * 4);
             bb1 = ByteBuffer.allocate(1024 * 10 * 4);
             System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
             ByteBuffer bb2 = ByteBuffer.allocate(1024 * 100000 * 3);
             bb1 = ByteBuffer.allocate(1024 * 10 * 3);
             System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());
             //System.gc();
             //pause expect for gc
             Thread.sleep(1000*6);
             ByteBuffer bb3 = ByteBuffer.allocate(1024 * 100000 * 6);
             System.out.println("Free memory:" + Runtime.getRuntime().freeMemory());    
         }catch(Exception e){
             e.printStackTrace();
         }        
     }

 }

程序启动需用如下命令:
java -verbose:gc -Xms64M -Xmx512M -XX:MaxDirectMemorySize=1000M TestMemoryLeak
-verbose:gc用于开启gc运行情况的输出,可以帮助我们了解jvm垃圾收集的运作情况;
其他参数值应该你机器的实际情况设定。

最后我想总结的是,当我们在使用DirectByteBuffer的时候一定要注意:
1)谨慎设定JVM运行参数,最好用-XX:MaxDirectMemorySize进行设定,否则你就得清楚你设定的mx不单单制定了heap size的最大值,它同时也是direct memory的最大值;
2)在密集型访问中(例如数据迁移工具)适当增加对gc的显式调用,保证资源的释放