jna(jni的坑),java jni native方法去申请native heap 空间

refert to http://ayufox.iteye.com/blog/723896

个人总结,以下文章主要提到了:

java进程所占用的内存大小,来自于non heap(如stackframe等等),java heap, native heap。

java 做GC的时候会释放 java heap的空间,但不会释放 native heap的空间。

为什么java进程占有4G内存,jmap dump出来的jvm内存只有100m, 那么说剩余的4G- 100m都是来自于 native heap? native heap是怎么如何被申请的?

eg: nio中的ByteBuffer就是申请的native 内存,如何申请的,猜测是通过jni方法调用C++ malloc申请,多说一句 nio通过使用native heap,少了一次jvm heap和内核内存 cp的过程

  1.              File file = new File("F:\\liq.pdf");  
  2. FileInputStream in = new FileInputStream(file);  
  3. FileChannel channel = in.getChannel();  
  4. ByteBuffer buff = ByteBuffer.allocateDirect(1024);   
  5.   
  6. long begin = System.currentTimeMillis();  
  7. while (channel.read(buff) != -1) {  
  8.     buff.flip();  
  9.     buff.clear();  
  10. }  
  11. long end = System.currentTimeMillis();  
  12. System.out.println("time is:" + (end - begin)); 

为什么JVM会去GC Native Heap呢?说起来其实很简单,只不过是ByteBuffer在对象被销毁前,自己会调用函数释放掉分配的内存而已(^_^真的是说穿了不值一提,有兴趣可以看一下ByteBuffer.allocateDirect这个类具体是怎么实现了,并不是使用finalize函数,而是使用更有安全保障的PhantomReference--可以让用户知道其引用对象具体何时从内存中移除)。  所以 做gc的时候,ByteBuffer(DirectMemory)也可以将自己在native heap中所申请的内存释放掉. (具体参考 JAVA NIO之浅谈内存映射文件原理与DirectMemory)


而下文提到的问题是:

Java代码   收藏代码
  1. public class Memory extends Pointer  
  2. {  
  3.     public Memory(long size)  
  4.     {  
  5.         this.size = size;  
  6.         if(size <= 0L)  
  7.             throw new IllegalArgumentException("Allocation size must be >= 0");  
  8.         peer = malloc(size);  
  9.         if(peer == 0L)  
  10.             throw new OutOfMemoryError("Cannot allocate " + size + " bytes");  
  11.         else  
  12.             return;  
  13. }  
  14.     protected void finalize()  
  15.     {  
  16.         if(peer != 0L)  
  17.         {  
  18.             free(peer);  
  19.             peer = 0L;  
  20.         }  
  21. }  
  22.   
  23.     static native long malloc(long l);  
  24.   
  25.     static native void free(long l);  

memory对象只有在GC时触发到了 finalize 方法才会去 free 内存,而java heap size在当下场景很少,gc被触发的很少 所以,native heap 内存不断增加却无法释放


  




———————引用文章——————

  最近有一个同事碰到一个很诡异的问题,一个JVM使用默认的启动参数(suse linux 64),内存竟然会一直增涨到4G,而通过jmap dump出来的heap空间只有80多M,jmap dump出来的Alive heap空间则竟然只有几M,到底内存是怎么被吃掉的呢?
      惯例,在目标JVM上启动BlackStar的JMX Proxy(维持现场,不需要重启动目标JVM),我们先用JConsole-ext看看,如下图,很奇怪,不管是Heap空间和NonHeap空间,即使是为这些空间分配的内存,都是在100M级别的。
 jna(jni的坑),java jni native方法去申请native heap 空间_第1张图片
      jna(jni的坑),java jni native方法去申请native heap 空间_第2张图片

      Top/free一下,确实,内存是被吃掉的。
 jna(jni的坑),java jni native方法去申请native heap 空间_第3张图片
     我们知道,Java进程的内存包括Java NonHeap空间、Java Heap空间和Native Heap空间和,既然内存不是被Java NonHeap/Heap空间吃掉的,那么只能怀疑是被Native Heap空间吃掉的,问过同事,该服务主要是作为一个MQ Consumer(此MQ非Java MQ,公司自己构建,C++实现),接受MQ消息处理,会不会是被这个这个consumer分配的Native堆吃掉的呢?
     我们试着GC一下,发现很奇怪,不仅Java Heap被GC了,Native Heap也被GC了,这里似乎不合常理,Native Heap怎么可能会被GC掉呢?
jna(jni的坑),java jni native方法去申请native heap 空间_第4张图片
    求助于万能的Google大神,万能的Google大神给出了一个提示——MaxDirectMemorySize,这个限制了nio的Native Heap的大小,但搜索出来的关于这个关键字的信息似乎与我们的场景背道相驰,基本上都是关于MaxDirectMemorySize默认设置太小导致的OutOfMemory。虽然实际并不相关,但搜索到的相关信息为我们重现问题创建了良好的环境,我们使用如下代码,可以大概地在window环境下“大概”地重现一下问题(注意,Window环境下默认值只有几百M,所以如下的循环不能太大)。

Java代码   收藏代码
  1. package ray.test;  
  2.   
  3. import java.nio.ByteBuffer;  
  4.   
  5. public class Test  
  6. {  
  7.     public static void main(String[] args) throws Exception  
  8.     {  
  9.         System.out.println("start");  
  10.         for (int i = 0; i < 200; i++)  
  11.         {  
  12.             final ByteBuffer bb = ByteBuffer.allocateDirect(1024 * 1024);//1M  
  13.             bb.clear();  
  14.             System.out.println("allocat:" + i);  
  15.         }  
  16.         System.out.println("before sleep");  
  17.          
  18.         Thread.sleep(60 * 1000);  
  19.     }  
  20. }  

    运行一下,我们会看到JVM内存占了200多M

jna(jni的坑),java jni native方法去申请native heap 空间_第5张图片
    GC一下,内存下降下来了
jna(jni的坑),java jni native方法去申请native heap 空间_第6张图片
      为什么JVM会去GC Native Heap呢?说起来其实很简单,只不过是ByteBuffer在对象被销毁前,自己会调用函数释放掉分配的内存而已(^_^真的是说穿了不值一提,有兴趣可以看一下ByteBuffer.allocateDirect这个类具体是怎么实现了,并不是使用finalize函数,而是使用更有安全保障的PhantomReference)。
     我们回到开头的问题,虽然不知道具体MQConsumer是如何处理的,当大概可以知道会在Native Heap上分配了大量内存,而在GC的时候再释放掉,大概可以想象MQConsumer的实现上会在引用Java对象被销毁时处理这个问题。而至于为什么占那么大的Native Heap而JVM GC还不启动呢,这里其实Sun JVM  GC的标准针对的是Java Heap,而在同事出现的那个场景里,Java Heap占用不多一直没有到GC边界,因此GC很久才会运行一次,导致出现了大量的Native Heap被占用而GC却不运行。
      实际上又怎么样的呢?
      跟MQConsumer(c++)实现的同事沟通了关于MQConsumer导致的Native Heap比较大的问题,反馈的结果却是MQ Consumer的so中,并没有动态申请内存的情况,并且在上面的分析中我们知道,其实GC的时候也会将这块Native Heap释放掉,因此我们可以排除C++代码本身导致的内存泄露问题。既然在MQ Consumer的so代码中没有这样的代码,那么内存到底是谁占去的呢?从MQ Consumer的java端代码上看,可以知道,我们目前使用的是JNA框架——Sun基于JNI的扩展,使地JNI的开发更加简单——那么是不是JNA框架本身实现的原因导致的呢?
      我们把目光投向JNA框架,先证实一下
      jmap一下,看看对象大概有多少

jmap –histo 25361
num #instances #bytes class name
----------------------------------------------
1: 6700 52136840 [B
2: 5402 28981680 [I
3: 13173 2232160 [C
4: 16911 2103160
5: 16911 2038712
6: 1506 1610216
7: 27150 1412992
8: 20561 1315904 java.lang.ref.Finalizer
9: 1506 1079128
10: 1391 1038176
11: 20728 663296 java.util.concurrent.LinkedBlockingQueue$Node
12: 20430 653760 com.sun.jna.Memory
13: 10677 427080 java.lang.String
14: 737 358328

      里面很值得我们注意的是com.sun.jna.Memory这个类,我们反汇编一下看看这个类Sun是如何实现的,下面代码代码中,大概的,猜测malloc和free是直接在Native堆上分配对象的,我们会发现,该类在构造函数中申请Native Heap内存,而在GC的时候释放掉

Java代码   收藏代码
  1. public class Memory extends Pointer  
  2. {  
  3.     public Memory(long size)  
  4.     {  
  5.         this.size = size;  
  6.         if(size <= 0L)  
  7.             throw new IllegalArgumentException("Allocation size must be >= 0");  
  8.         peer = malloc(size);  
  9.         if(peer == 0L)  
  10.             throw new OutOfMemoryError("Cannot allocate " + size + " bytes");  
  11.         else  
  12.             return;  
  13. }  
  14.     protected void finalize()  
  15.     {  
  16.         if(peer != 0L)  
  17.         {  
  18.             free(peer);  
  19.             peer = 0L;  
  20.         }  
  21. }  
  22.   
  23.     static native long malloc(long l);  
  24.   
  25.     static native void free(long l);  
  26. }  

      再dump一下堆空间,我们看一下这些Memory对象中的size是多少,我们基本上可以猜测其会占掉多少内存
 
      里面实际存活的对象只有219个,我们可以知道大量的Memory对象实际上处于等待GC回收的状态。从上面Memory的实现上,我们也可以知道,只要对象不被GC,则其向Native Heap分配的内存就不会释放。我们随机看看里面的size值是多少。见下图,大概都是256K。

jna(jni的坑),java jni native方法去申请native heap 空间_第7张图片
     大概我们知道这些Memory对象,每个会 向Native Heap申请256K的空间,而只要这些等待GC的对象不被GC,则这些空间则不会被释放(见Memory的finalize方法)。从上面目前处于等待被GC的Memroy对象数量来看(当然,并非每个都会是256K这么大),这个内存数是非常大的。
     非常遗憾的是,Memory对象本身并没有提供接口可以让我们显示地去释放掉这块内存,我们只能坐等着JVM的GC动作,而更让人郁闷的是,由于实际Java Heap占用不大,导致Native Heap一致不断地增长而得不到回收。
     当然,自己看一下JNA的实现,实际上我们还是有办法的,虽然实现方式并不是那么直截了当,但好歹还是可以实现,至于具体怎么处理,有兴趣地可以研究一下JNA的实现^_^



你可能感兴趣的:(jvm,nio,nativeheap)