分析内存泄露的一般步骤

转载 2017年01月03日 13:08:02

想要了解更多,加QQ群72132378


 如果发现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()”,要好很多很多。


使用 Eclipse Memory Analyzer 进行内存泄漏分析的一次过程

在平时开发、测试过程中、甚至是生产环境中,有时会遇到OutOfMemoryError,Java堆溢出了,这表明程序有严重的问题。我们需要找造成OutOfMemoryError原因。一般有两种情况: ...
  • luo_yifan
  • luo_yifan
  • 2014年04月09日 11:07
  • 2696

内存泄露问题分析方法

本文介绍了内存泄露的定位方法,可以帮助Android系统的开发者定位一些常见的内存泄露问题。本文暂时只从工具的角度讲解了如何定位,后面会补充如何从代码的角度去分析和定位,并且加入内存管理的优化方法。...
  • wlwl0071986
  • wlwl0071986
  • 2015年11月07日 12:24
  • 4318

利用内存分析工具(Memory Analyzer Tool,MAT)分析java项目内存泄露

一、开发环境: 操作系统:ubuntu 14.04 IDE:Eclipse Java EE IDE for Web Developers. Version: Luna Service Releas...
  • wanghuiqi2008
  • wanghuiqi2008
  • 2016年02月23日 20:46
  • 15101

内存泄露分析

Native 内存泄漏 第一部分:通过malloc分配内存泄漏 对于native内存泄漏,比较常见的一类是C堆内存泄漏,即调用malloc申请的内存没有及时释放造成的内存泄漏。 调试方法 ...
  • u012248972
  • u012248972
  • 2017年06月13日 15:33
  • 1021

使用IBM heapAnalyzer分析内存泄露的原因

大家在开发J2EE应用时可能经常碰到OOM的问题。出现这种问题的原因就是内存消耗殆尽,创建新的对象时没有可分配的内存了。为了找到真正导致OOM的原因,这里将使用IBM heapAnalyzer工具进行...
  • xu1314
  • xu1314
  • 2012年07月03日 14:05
  • 10073

分析和解决JAVA 内存泄露的实战例子

这几天,一直在为Java的“内存泄露”问题纠结。Java应用程序占用的内存在不断的、有规律的上涨,最终超过了监控阈值。福尔摩 斯不得不出手了! 分析内存泄露的一般步骤      ...
  • bigtree_3721
  • bigtree_3721
  • 2016年01月20日 18:21
  • 30835

windows下内存泄露分析工具

windows检查程序是否存在内存泄漏方法1: 1.准备工具:windbg 2.准备环境:需要设置windbg符号路径 内存泄露分析demo [cpp] view plain cop...
  • listener51
  • listener51
  • 2016年10月23日 11:35
  • 1729

JAVA 内存泄露详解(原因、例子及解决)

Java中的内存泄露,广义并通俗的说,就是:不再会被使用的对象的内存不能被回收,就是内存泄露。 Java中的内存泄露与C++中的表现有所不同。 在C++中,所有被分配了内存的对象,不再...
  • anxpp
  • anxpp
  • 2016年05月05日 20:24
  • 37245

使用MAT分析内存泄露步骤

项目优化过程中,总会遇到内存的一些问题,即便前期设计很优秀,可有可能在编码过程中留下内存泄露等问题,而一般查找内存问题是比较困难的,而使用MAT工具,结合项目代码及个人经验,能够比较快速的定位问题并解...
  • chen825919148
  • chen825919148
  • 2014年04月17日 16:26
  • 3708

BoundsChecker检查内存泄露 使用步骤

网上有几篇文章是写BoundsChecker的,主要是讲用途在哪,
  • wu_123_456
  • wu_123_456
  • 2014年09月28日 08:52
  • 1687
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:分析内存泄露的一般步骤
举报原因:
原因补充:

(最多只允许输入30个字)