最近有一个同事碰到一个很诡异的问题,一个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级别的。
Top/free一下,确实,内存是被吃掉的。
我们知道,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掉呢?
求助于万能的Google大神,万能的Google大神给出了一个提示——MaxDirectMemorySize,这个限制了nio的Native Heap的大小,但搜索出来的关于这个关键字的信息似乎与我们的场景背道相驰,基本上都是关于MaxDirectMemorySize默认设置太小导致的OutOfMemory。虽然实际并不相关,但搜索到的相关信息为我们重现问题创建了良好的环境,我们使用如下代码,可以大概地在window环境下“大概”地重现一下问题(注意,Window环境下默认值只有几百M,所以如下的循环不能太大)。
package ray.test;
import java.nio.ByteBuffer;
public class Test
{
public static void main(String[] args) throws Exception
{
System.out.println("start");
for (int i = 0; i < 200; i++)
{
final ByteBuffer bb = ByteBuffer.allocateDirect(1024 * 1024);//1M
bb.clear();
System.out.println("allocat:" + i);
}
System.out.println("before sleep");
Thread.sleep(60 * 1000);
}
}
运行一下,我们会看到JVM内存占了200多M
GC一下,内存下降下来了
为什么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 <constMethodKlass>
5: 16911 2038712 <methodKlass>
6: 1506 1610216 <constantPoolKlass>
7: 27150 1412992 <symbolKlass>
8: 20561 1315904 java.lang.ref.Finalizer
9: 1506 1079128 <instanceKlassKlass>
10: 1391 1038176 <constantPoolCacheKlass>
11: 20728 663296 java.util.concurrent.LinkedBlockingQueue$Node
12: 20430 653760 com.sun.jna.Memory
13: 10677 427080 java.lang.String
14: 737 358328 <methodDataKlass>
里面很值得我们注意的是com.sun.jna.Memory这个类,我们反汇编一下看看这个类Sun是如何实现的,下面代码代码中,大概的,猜测malloc和free是直接在Native堆上分配对象的,我们会发现,该类在构造函数中申请Native Heap内存,而在GC的时候释放掉
public class Memory extends Pointer
{
public Memory(long size)
{
this.size = size;
if(size <= 0L)
throw new IllegalArgumentException("Allocation size must be >= 0");
peer = malloc(size);
if(peer == 0L)
throw new OutOfMemoryError("Cannot allocate " + size + " bytes");
else
return;
}
protected void finalize()
{
if(peer != 0L)
{
free(peer);
peer = 0L;
}
}
static native long malloc(long l);
static native void free(long l);
}
再dump一下堆空间,我们看一下这些Memory对象中的size是多少,我们基本上可以猜测其会占掉多少内存
里面实际存活的对象只有219个,我们可以知道大量的Memory对象实际上处于等待GC回收的状态。从上面Memory的实现上,我们也可以知道,只要对象不被GC,则其向Native Heap分配的内存就不会释放。我们随机看看里面的size值是多少。见下图,大概都是256K。
大概我们知道这些Memory对象,每个会 向Native Heap申请256K的空间,而只要这些等待GC的对象不被GC,则这些空间则不会被释放(见Memory的finalize方法)。从上面目前处于等待被GC的Memroy对象数量来看(当然,并非每个都会是256K这么大),这个内存数是非常大的。
非常遗憾的是,Memory对象本身并没有提供接口可以让我们显示地去释放掉这块内存,我们只能坐等着JVM的GC动作,而更让人郁闷的是,由于实际Java Heap占用不大,导致Native Heap一致不断地增长而得不到回收。
当然,自己看一下JNA的实现,实际上我们还是有办法的,虽然实现方式并不是那么直截了当,但好歹还是可以实现,至于具体怎么处理,有兴趣地可以研究一下JNA的实现^_^
分享到:
相关推荐
JNI层内存泄漏检测工具是针对Android应用开发中的一个重要问题——JNI内存泄漏的解决方案。JNI,全称为Java Native Interface,允许Java代码与其他编程语言(如C++)交互,从而利用其性能优势。然而,由于Java和C/...
JNI在很多场景下都非常有用,例如提高性能、调用操作系统底层功能,以及本例中的读取内存信息。在Android应用开发中,如果需要获取设备的内存状态,可以通过JNI来实现。 首先,我们需要了解JNI的基本概念。JNI提供...
在这个“jni例子——使用int数组”的示例中,我们将深入探讨如何在Java和C/C++之间传递和操作int数组。 1. **JNI基础知识**: - JNI接口提供了Java与本地代码(如C/C++)通信的桥梁,使得开发者可以在Java应用中...
JNI(Java Native Interface)是Java平台的标准组成部分,它允许Java代码和其他语言写的代码进行交互。在Android开发中,JNI常用于提升性能、调用系统库或者实现特定硬件功能。本篇将深入探讨如何在多个类中动态注册...
在本例中,我们探讨的是如何使用JNI来获取系统的CPU和内存使用率,这对于系统监控、性能分析或者资源管理类的应用来说是至关重要的。 首先,我们需要了解JNI的基本工作原理。JNI提供了一套接口,让Java代码可以调用...
本文将详细讲解如何通过JNI在Java中获取CPU和内存的使用率。 首先,我们需要理解CPU和内存使用率的基本概念。CPU使用率是指CPU在一段时间内执行非空闲任务的比例,通常以百分比表示。内存使用率则反映了系统分配给...
在这个"JNI开发实例——锅炉压力监控器的源码"中,我们将深入探讨如何使用JNI来开发一个实时监控锅炉压力的系统。这个系统可能是为了确保工业生产过程中的安全性和效率,通过硬件接口获取实时数据,并在Java应用程序...
### 使用JNA替代JNI调用DLL,并解决内存溢出问题 #### 问题背景 在项目的开发过程中,常常遇到需要处理二进制流数据并对其进行解析处理的情况。这种情况下,如果上层应用平台采用的是Java开发,而底层算法或数据...
附件是Android下检测ndk和jni内存泄漏的demo,可以用于native中malloc和free的检测。使用方法(参见博客):https://blog.csdn.net/zhuyong006/article/details/88537499
这篇博客“JNI编程(二) —— 让C++和Java相互调用(2)”显然深入探讨了如何利用JNI实现Java与C++之间的互调用。在Java应用程序中,有时为了性能优化或者利用已有的C/C++库,我们需要借助JNI来实现这种跨语言的通信。 ...
这篇博客文章“Java中JNI的使用(一)——native”很可能是对Java程序员如何首次接触和使用JNI的一个入门教程。 首先,我们来理解“native”关键字。在Java中,`native`是用来标记一个方法的,表示这个方法的实现是在...
在实际开发中,还需要注意一些细节,比如错误处理、内存管理(Java对象在本地代码中使用需要通过JNI函数创建和释放)、线程安全问题等。同时,由于涉及到跨语言和跨平台,理解JNI的工作原理和正确使用JNI头文件至关...
《Android NDK与JNI开发详解:从Hello World...但需要注意,虽然原生代码能提高性能,但也增加了调试难度和内存管理的风险,因此应谨慎使用。在实际项目中,要权衡性能和复杂性,合理选择Java和C/C++的混合编程策略。
本文将深入探讨JNI的两个核心头文件——`jni.h`和`jni_md.h`。 首先,`jni.h`是JNI的主要接口头文件,包含了所有JNI的函数声明和数据类型定义。当你在C或C++程序中使用JNI时,需要包含这个头文件。`jni.h`提供了...
5. **内存管理**:JNI提供了在Java和本地代码之间管理内存的机制,如NewGlobalRef、DeleteLocalRef等,以避免内存泄漏。 6. **字符串和转换**:JNI提供了将Java字符串转换为本地字符串,以及反之的转换函数,方便...
Android手机内存管理与性能优化&JNI、NDK高级编程(JNI、Dalvik、内存监测) 视频资源
【是男人就下100层——JNI代码】 在Android游戏开发中,JNI(Java Native Interface)是一个重要的技术,它允许Java代码和其他语言写的代码进行交互。本项目"是男人就下100层"是一款基于Cocos2d-x框架的Android小...
————————————— ——————— ^ ^ 包名 类名 5. 编写相应的.c文件(hello-jni.c) #include #include<jni.h> JNIEXPORT jstring JNICALL Java_com_xxx_hello_HelloJni_stringFromJNI(JNIEnv *...