java 内存泄露调试和解决

来源:互联网 发布:js触发a标签href 编辑:程序博客网 时间:2024/04/29 01:46
说起Java的内存泄露,其实定义不是那么明确。首先,如果JVM没有bug,那么理论上是不会出现“无法回收的堆空间”,也就是说C/C++中的那种内 存泄露在Java中不存在的。其次,如果由于Java程序一直持有某个对象的引用,但是从程序逻辑上看,这个对象再也不会被用到了,那么我们可以认为这个 对象被泄露了。如果这样的对象数量很多,那么很明显,大量的内存空间就被泄露(“浪费”更准确一些)了。

目录[-]

  • 分析内存泄露的一般步骤
  • dump heap
  • analyze heap
  • 原因解释
  • 解决方案
  • 是否Bug
  • 一些补充

  • 这几天,一直在为Java的“内存泄露”问题纠结。Java应用程序占用的内存在不断的、有规律的上涨,最终超过了监控阈值。福尔摩 斯不得不出手了!

    分析内存泄露的一般步骤

     

        如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析

    1. 把Java应用程序使用的heap dump下来
    2. 使用Java heap分析工具,找出内存占用超出预期(一般是因为数量太多)的嫌疑对象
    3. 必要时,需要分析嫌疑对象和其他对象的引用关系。
    4. 查看程序的源代码,找出嫌疑对象数量过多的原因。

    dump heap

     

        如果Java应用程序出现了内存泄露,千万别着急着把应用杀掉,而是要保存现场。如果是互联网应用,可以把流量切到其他服务器。保存现场的目的就是为了把 运行中JVM的heap dump下来。

     

        JDK自带的jmap工具,可以做这件事情。它的执行方法是:

    Java代码   收藏代码
    1. jmap -dump:format=b,file=heap.bin <pid>  
     

        format=b的含义是,dump出来的文件时二进制格式。

        file-heap.bin的含义是,dump出来的文件名是heap.bin。

        <pid>就是JVM的进程号。

        (在linux下)先执行ps aux | grep java,找到JVM的pid;然后再执行jmap -dump:format=b,file=heap.bin <pid>,得到heap dump文件。

    analyze heap

     

        将二进制的heap dump文件解析成human-readable的信息,自然是需要专业工具的帮助,这里推荐Memory Analyzer 。

     

        Memory Analyzer,简称MAT,是Eclipse基金会的开源项目,由SAP和IBM捐助。巨头公司出品的软件还是很中用的,MAT可以分析包含数亿级对 象的heap、快速计算每个对象占用的内存大小、对象之间的引用关系、自动检测内存泄露的嫌疑对象,功能强大,而且界面友好易用。

     

        MAT的界面基于Eclipse开发,以两种形式发布:Eclipse插件和Eclipe RCP。MAT的分析结果以图片和报表的形式提供,一目了然。总之个人还是非常喜欢这个工具的。下面先贴两张官方的screenshots:

    MAT的分析结果概述

    MAT分析对象的大小及数量

        言归正传,我用MAT打开了heap.bin,很容易看出,char[]的数量出其意料的多,占用90%以上的内存 。一般来说,char[]在JVM确实会占用很多内存,数量也非常多,因为String对象以char[]作为内部存储。但是这次的char[]太贪婪 了,仔细一观察,发现有数万计的char[],每个都占用数百K的内存 。这个现象说明,Java程序保存了数以万计的大String对象 。结合程序的逻辑,这个是不应该的,肯定在某个地方出了问题。

     

    顺藤摸瓜

     

        在可疑的char[]中,任意挑了一个,使用Path To GC Root功能,找到该char[]的引用路径,发现String对象是被一个HashMap中引用的 。这个也是意料中的事情,Java的内存泄露多半是因为对象被遗留在全局的HashMap中得不到释放。不过,该HashMap被用作一个缓存,设置了缓 存条目的阈值,导达到阈值后会自动淘汰。从这个逻辑分析,应该不会出现内存泄露的。虽然缓存中的String对象已经达到数万计,但仍然没有达到预先设置 的阈值(阈值设置地比较大,因为当时预估String对象都比较小)。

     

        但是,另一个问题引起了我的注意:为什么缓存的String对象如此巨大?内部char[]的长度达数百K。虽然缓存中的 String对象数量还没有达到阈值,但是String对象大小远远超出了我们的预期,最终导致内存被大量消耗,形成内存泄露的迹象(准确说应该是内存消 耗过多) 。

     

        就这个问题进一步顺藤摸瓜,看看String大对象是如何被放到HashMap中的。通过查看程序的源代码,我发现,确实有String大对象,不 过并没有把String大对象放到HashMap中,而是把String大对象进行split(调用String.split方法),然后将split出 来的String小对象放到HashMap中 了。

     

        这就奇怪了,放到HashMap中明明是split之后的String小对象,怎么会占用那么大空间呢?难道是String类的split方法有问题?

     

    查看代码

     

        带着上述疑问,我查阅了Sun JDK6中String类的代码,主要是是split方法的实现:

    Java代码 
    1. public   
    2. String[] split(String regex, int limit) {  
    3.     return Pattern.compile(regex).split(this, limit);  
    4. }  

    可以看出,Stirng.split方法调用了Pattern.split方法。继续看Pattern.split方法的代码:

    Java代码 
    1. public   
    2. String[] split(CharSequence input, int limit) {  
    3.         int index = 0;  
    4.         boolean matchLimited = limit > 0;  
    5.         ArrayList<String> matchList = new   
    6. ArrayList<String>();  
    7.         Matcher m = matcher(input);  
    8.         // Add segments before each match found  
    9.         while(m.find()) {  
    10.             if (!matchLimited || matchList.size() < limit - 1) {  
    11.                 String match = input.subSequence(index,   
    12. m.start()).toString();  
    13.                 matchList.add(match);  
    14.                 index = m.end();  
    15.             } else if (matchList.size() == limit - 1) { // last one  
    16.                 String match = input.subSequence(index,  
    17.                                                    
    18. input.length()).toString();  
    19.                 matchList.add(match);  
    20.                 index = m.end();  
    21.             }  
    22.         }  
    23.         // If no match was found, return this  
    24.         if (index == 0)  
    25.             return new String[] {input.toString()};  
    26.         // Add remaining segment  
    27.         if (!matchLimited || matchList.size() < limit)  
    28.             matchList.add(input.subSequence(index,   
    29. input.length()).toString());  
    30.         // Construct result  
    31.         int resultSize = matchList.size();  
    32.         if (limit == 0)  
    33.             while (resultSize > 0 &&   
    34. matchList.get(resultSize-1).equals(""))  
    35.                 resultSize--;  
    36.         String[] result = new String[resultSize];  
    37.         return matchList.subList(0, resultSize).toArray(result);  
    38.     }  

        注意看第9行:Stirng match = input.subSequence(intdex, m.start()).toString();

    这里的match就是split出来的String小对象,它其实是String大对象subSequence的结果。继续看 String.subSequence的代码:

    Java代码 
    1. public   
    2. CharSequence subSequence(int beginIndex, int endIndex) {  
    3.         return this.substring(beginIndex, endIndex);  
    4. }  

        String.subSequence有调用了String.subString,继续看:

    Java代码 
    1. public String   
    2. substring(int beginIndex, int endIndex) {  
    3.     if (beginIndex < 0) {  
    4.         throw new StringIndexOutOfBoundsException(beginIndex);  
    5.     }  
    6.     if (endIndex > count) {  
    7.         throw new StringIndexOutOfBoundsException(endIndex);  
    8.     }  
    9.     if (beginIndex > endIndex) {  
    10.         throw new StringIndexOutOfBoundsException(endIndex - beginIndex);  
    11.     }  
    12.     return ((beginIndex == 0) && (endIndex == count)) ? this :  
    13.         new String(offset + beginIndex, endIndex - beginIndex, value);  
    14.     }  

        看第11、12行,我们终于看出眉目,如果subString的内容就是完整的原字符串,那么返回原String对象;否则,就会创建一个新的 String对象,但是这个String对象貌似使用了原String对象的char[]。我们通过String的构造函数确认这一点:

    Java代码 
    1. // Package   
    2. private constructor which shares value array for speed.  
    3.     String(int offset, int count, char value[]) {  
    4.     this.value = value;  
    5.     this.offset = offset;  
    6.     this.count = count;  
    7.     }  

        为了避免内存拷贝、加快速度,Sun JDK直接复用了原String对象的char[],偏移量和长度来标识不同的字符串内容。也就是说,subString出的来String小对象 仍然会指向原String大对象的char[],split也是同样的情况 。这就解释了,为什么HashMap中String对象的char[]都那么大。

    原因解释

     

        其实上一节已经分析出了原因,这一节再整理一下:

    1. 程序从每个请求中得到一个String大对象,该对象内部char[]的长度达数百K。
    2. 程序对String大对象做split,将split得到的String小对象放到HashMap中,用作缓存。
    3. Sun JDK6对String.split方法做了优化,split出来的Stirng对象直接使用原String对象的char[]
    4. HashMap中的每个String对象其实都指向了一个巨大的char[]
    5. HashMap的上限是万级的,因此被缓存的Sting对象的总大小=万*百K=G级。
    6. G级的内存被缓存占用了,大量的内存被浪费,造成内存泄露的迹象。

    解决方案

     

        原因找到了,解决方案也就有了。split是要用的,但是我们不要把split出来的String对象直接放到HashMap中,而是调用一下 String的拷贝构造函数String(String original),这个构造函数是安全的,具体可以看代码:

    Java代码 
    1.     /** 
    2.      * Initializes a newly created {@code String} object so that it  
    3. represents 
    4.      * the same sequence of characters as the argument; in other words,  
    5. the 
    6.      * newly created string is a copy of the argument string. Unless an 
    7.      * explicit copy of {@code original} is needed, use of this  
    8. constructor is 
    9.      * unnecessary since Strings are immutable. 
    10.      * 
    11.      * @param  original 
    12.      *         A {@code String} 
    13.      */  
    14.     public String(String original) {  
    15.     int size = original.count;  
    16.     char[] originalValue = original.value;  
    17.     char[] v;  
    18.     if (originalValue.length > size) {  
    19.         // The array representing the String is bigger than the new  
    20.         // String itself.  Perhaps this constructor is being called  
    21.         // in order to trim the baggage, so make a copy of the array.  
    22.             int off = original.offset;  
    23.             v = Arrays.copyOfRange(originalValue, off, off+size);  
    24.     } else {  
    25.         // The array representing the String is the same  
    26.         // size as the String, so no point in making a copy.  
    27.         v = originalValue;  
    28.     }  
    29.     this.offset = 0;  
    30.     this.count = size;  
    31.     this.value = v;  
    32.     }  

        只是,new String(string)的代码很怪异,囧。或许,subString和split应该提供一个选项,让程序员控制是否复用String对象的 char[]。

    是否Bug

     

        虽然,subString和split的实现造成了现在的问题,但是这能否算String类的bug呢?个人觉得不好说。因为这样的优化是比较合理 的,subString和spit的结果肯定是原字符串的连续子序列。只能说,String不仅仅是一个核心类,它对于JVM来说是与原始类型同等重要的 类型。

     

        JDK实现对String做各种可能的优化都是可以理解的。但是优化带来了忧患,我们程序员足够了解他们,才能用好他们。

    一些补充

    有个地方我没有说清楚。

     

    我的程序是一个Web程序,每次接受请求,就会创建一个大的String对象,然后对该String对象进行split,最后split之后的String对象放到全局缓存中。如果接收了5W个请求,那么就会有5W个大String对象。这5W个大String对象都被存储在全局缓存中,因此会造成内存泄漏。我原以为缓存的是5W个小String,结果都是大String。

     

    “抛出异常的爱”同学,在回帖(第7页)中建议用"java.io.StreamTokenizer"来解决本文的问题。确实是终极解决方案,比我上面提到的“new String()”,要好很多很多。


    JDK工具:

    jstack -- 如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到 当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。目前只有在Solaris和Linux的JDK版本里面才有。

    jconsole – jconsole是基于Java Management Extensions (JMX)的实时图形化监测工具,这个工具利用了内建到JVM里面的JMX指令来提供实时的性能和资源的监控,包括了Java 程序的内存使用,Heap size, 线程的状态,类的分配状态和空间使用等等。

    jinfo – jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息,目前只有在Solaris和Linux的JDK版本里面才有。

    jmap – jmap 可以从core文件或进程中获得内存的具体匹配情况,包括Heap size, Perm size等等,目前只有在Solaris和Linux的JDK版本里面才有。

    jdb – jdb 用来对core文件和正在运行的Java进程进行实时地调试,里面包含了丰富的命令帮助您进行调试,它的功能和Sun studio里面所带的dbx非常相似,但 jdb是专门用来针对Java应用程序的。

    jstat – jstat利用了JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对Heap size和垃圾回收状况的监控等等。

    jps – jps是用来查看JVM里面所有进程的具体状态, 包括进程ID,进程启动的路径等等。 

    jstatd 
    启动jvm监控服务。它是一个基于rmi的应用,向远程机器提供本机jvm应用程序的信息。默认端口1099。


    0 0
    原创粉丝点击
    热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 平安普惠账号不可以注销怎么办? 京东购物非自营货没到降价了怎么办 实体店商家不承认卖的是假货怎么办 衣服上的装饰圆扣掉下来了怎么办 没在京东买东西却收到退款怎么办 小米分期付款买的手机不要了怎么办 唯品会在线支付后商品有问题怎么办 红米手机把时间删了怎么办 红米桌面上的时间删了怎么办 华为手机玩游戏老是闪退怎么办 别人家无线网距离太远信号差怎么办 微信公众号交话费交错了怎么办 手机卡里还有话费销户的话怎么办 号码忘记交话费变成空号怎么办 多屏互动没办法隔空播放怎么办? 一个人长期受一件事的打击怎么办 物流信息显示快递被别人签收怎么办 现在打工的人被领导骂怎么办 加密狗丢了打不开软件了怎么办 手机微信可以打开网页打不开怎么办 手机中国网打开网速慢该怎么办 在韩国用中国软件网速特别慢怎么办 淘宝退货快递一直没显示到货怎么办 高二美术生集训时文化课怎么办 唯品会不支持7天无理由退货怎么办 sy来4am了孤存怎么办 淘宝网买的电器坏了怎么办 在闲鱼上买的电器是坏的怎么办 打开时全屏不知变成小屏怎么办 苹果6s原装数据线不充电怎么办 vivo手机安卓系统耗电快怎么办 电视机机顶盒插了电源开不了怎么办 苹果手机更新系统后老是闪退怎么办 16g苹果6s手机内存不足怎么办 苹果以前浏览器页面忽然没了怎么办 白衬衣被别的衣服染了怎么办 把宝贝标题改了没访客了怎么办 微信公众号看不到评论时间了怎么办 微店退款退货买家发空快递怎么办 微信二维码收款顾客少付款了怎么办 江西高考二本差5分上线怎么办