分析内存泄露的一般步骤
http://172.29.62.165/alicms/a/Javabianchengyuyingyong/Javajichu/2010/0830/106.html
如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析
- 把Java应用程序使用的heap dump下来
- 使用Java heap分析工具,找出内存占用超出预期(一般是因为数量太多)的嫌疑对象
- 必要时,需要分析嫌疑对象和其他对象的引用关系。
- 查看程序的源代码,找出嫌疑对象数量过多的原因。
dump heap
如果Java应用程序出现了内存泄露,千万别着急着把应用杀掉,而是要保存现场。如果是互联网应用,可以把流量切到其他服务器。保存现场的目的就是为了把 运行中JVM的heap dump下来。
JDK自带的jmap工具,可以做这件事情。它的执行方法是:
-
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打开了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方法的实现:
-
public
-
String[] split(String regex, int limit) {
-
return Pattern.compile(regex).split(this, limit);
-
}
可以看出,Stirng.split方法调用了Pattern.split方法。继续看Pattern.split方法的代码:
-
public
-
String[] split(CharSequence input, int limit) {
-
int index = 0;
-
boolean matchLimited = limit > 0;
-
ArrayList<String> matchList = new
-
ArrayList<String>();
-
Matcher m = matcher(input);
-
-
while(m.find()) {
-
if (!matchLimited || matchList.size() < limit - 1) {
-
String match = input.subSequence(index,
-
m.start()).toString();
-
matchList.add(match);
-
index = m.end();
-
} else if (matchList.size() == limit - 1) {
-
String match = input.subSequence(index,
-
-
input.length()).toString();
-
matchList.add(match);
-
index = m.end();
-
}
-
}
-
-
if (index == 0)
-
return new String[] {input.toString()};
-
-
if (!matchLimited || matchList.size() < limit)
-
matchList.add(input.subSequence(index,
-
input.length()).toString());
-
-
int resultSize = matchList.size();
-
if (limit == 0)
-
while (resultSize > 0 &&
-
matchList.get(resultSize-1).equals(""))
-
resultSize--;
-
String[] result = new String[resultSize];
-
return matchList.subList(0, resultSize).toArray(result);
-
}
注意看第9行:Stirng match = input.subSequence(intdex, m.start()).toString();
这里的match就是split出来的String小对象,它其实是String大对象subSequence的结果。继续看 String.subSequence的代码:
-
public
-
CharSequence subSequence(int beginIndex, int endIndex) {
-
return this.substring(beginIndex, endIndex);
-
}
String.subSequence有调用了String.subString,继续看:
-
public String
-
substring(int beginIndex, int endIndex) {
-
if (beginIndex < 0) {
-
throw new StringIndexOutOfBoundsException(beginIndex);
-
}
-
if (endIndex > count) {
-
throw new StringIndexOutOfBoundsException(endIndex);
-
}
-
if (beginIndex > endIndex) {
-
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
-
}
-
return ((beginIndex == 0) && (endIndex == count)) ? this :
-
new String(offset + beginIndex, endIndex - beginIndex, value);
-
}
看第11、12行,我们终于看出眉目,如果subString的内容就是完整的原字符串,那么返回原String对象;否则,就会创建一个新的 String对象,但是这个String对象貌似使用了原String对象的char[]。我们通过String的构造函数确认这一点:
-
-
private constructor which shares value array for speed.
-
String(int offset, int count, char value[]) {
-
this.value = value;
-
this.offset = offset;
-
this.count = count;
-
}
为了避免内存拷贝、加快速度,Sun JDK直接复用了原String对象的char[],偏移量和长度来标识不同的字符串内容。也就是说,subString出的来String小对象 仍然会指向原String大对象的char[],split也是同样的情况 。这就解释了,为什么HashMap中String对象的char[]都那么大。
原因解释
其实上一节已经分析出了原因,这一节再整理一下:
- 程序从每个请求中得到一个String大对象,该对象内部char[]的长度达数百K。
- 程序对String大对象做split,将split得到的String小对象放到HashMap中,用作缓存。
- Sun JDK6对String.split方法做了优化,split出来的Stirng对象直接使用原String对象的char[]
- HashMap中的每个String对象其实都指向了一个巨大的char[]
- HashMap的上限是万级的,因此被缓存的Sting对象的总大小=万*百K=G级。
- G级的内存被缓存占用了,大量的内存被浪费,造成内存泄露的迹象。
解决方案
原因找到了,解决方案也就有了。split是要用的,但是我们不要把split出来的String对象直接放到HashMap中,而是调用一下 String的拷贝构造函数String(String original),这个构造函数是安全的,具体可以看代码:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
public String(String original) {
-
int size = original.count;
-
char[] originalValue = original.value;
-
char[] v;
-
if (originalValue.length > size) {
-
-
-
-
int off = original.offset;
-
v = Arrays.copyOfRange(originalValue, off, off+size);
-
} else {
-
-
-
v = originalValue;
-
}
-
this.offset = 0;
-
this.count = size;
-
this.value = v;
-
}
只是,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()”,要好很多很多。
分享到:
相关推荐
下面将详细介绍如何使用GDB来查找和分析内存泄露。 首先,内存泄露通常发生在动态分配的内存没有被正确释放的情况下。当程序员通过`malloc`、`calloc`、`new`等函数分配内存后,忘记或无法在适当的地方调用`free`、...
#### 使用jVisualVM分析内存泄漏 - **查看VisualGC插件**:通过VisualGC插件可以直观地看到内存使用情况的变化。如果发现老生代的内存没有减少或者始终占用大量空间,这可能意味着存在内存泄漏。 - **进行堆转储**...
分析内存泄露的步骤通常包括: 1. **捕获快照**:使用内存分析工具在程序运行的不同阶段捕获内存快照,对比分析,找出内存占用增加的原因。 2. **识别泄漏对象**:通过分析快照,找到长时间存活且占用内存大的对象...
理解并有效地分析内存泄漏是每个Android开发者必须掌握的关键技能。 内存泄漏通常发生在对象被创建后,但没有正确地释放。在Java(Android的主要编程语言)中,垃圾回收机制会自动清理不再使用的对象,但当一个对象...
**三、分析内存泄漏** 1. **对比快照**:连续捕获两个快照,通过对比找出对象数量明显增加的部分。 2. **LeakCanary插件**:Android Studio推荐的第三方库,自动检测内存泄漏并提供详细的报告。 3. **引用链分析**:...
使用这两款工具,一般步骤如下: 1. **安装与配置**:下载并安装LeakDiag和LDGrapher,根据需求配置环境。 2. **运行诊断**:通过命令行或者集成到开发环境中,启动待检测的应用程序。 3. **收集信息**:LeakDiag将...
Java内存泄漏是一个严重的问题,它会导致程序性能下降,甚至可能导致应用程序崩溃。为了有效地诊断和解决这类问题,开发者需要借助...在日常开发中,定期进行内存分析并结合代码审查,是预防和解决内存泄漏的关键步骤。
3. **分析结果**:在应用运行过程中,LeakTracer会持续收集信息,开发者可以在合适的时间点查看或导出内存泄漏报告。 4. **问题修复**:根据报告,开发者可以找出导致内存泄漏的JNI代码,并进行相应的优化,确保...
### 使用mtrace分析内存泄露 #### 一、为何选择mtrace作为内存泄露分析工具 在嵌入式系统中,内存管理至关重要。对于这类系统中的程序,通常会在启动时分配大量内存,并持续运行而不释放这些内存。因此,关注点...
标题中的“vs2010内存泄露检查工具”指的是Visual Leak Detector(VLD),这是一个为Visual C++编译器设计的插件,它可以在运行时检测并报告C++程序中的内存泄漏情况。VLD能够集成到VS2010的环境中,使得开发者可以...
### Linux 内存泄露查找详解 #### 一、引言 在进行Linux C语言编程时,内存管理一直是程序员关注的重点之一。特别是在动态内存分配场景下,如果不妥善处理,很容易出现内存泄露的问题。内存泄露不仅会消耗系统资源...
MAT(Memory Analyzer Tool)是由Eclipse基金会开发的一款强大的Java内存分析工具,它专门用于检测和分析Java应用的内存泄漏问题。 MAT提供了多种功能来帮助开发者诊断和解决内存泄漏问题。首先,MAT可以生成详细的...
- 使用专门的工具(如Eclipse Memory Analyzer Tool等)打开和分析内存快照文件。 - 查找重复的对象实例或过大的对象集合,这可能是内存泄漏的根源。 #### 六、总结 通过上述步骤和技术手段,您可以有效地诊断...
Valgrind是一款开源的动态分析工具,主要用于检测C和C++程序中的内存错误,包括内存泄漏、未初始化的内存读取、无效指针访问等。它通过构建一个虚拟机,使得程序在该虚拟机上运行,从而能够对内存操作进行精细的监控...
7. **代码审查和测试**:定期的代码审查和充分的测试是防止内存泄漏的重要步骤。通过仔细检查代码,特别是涉及动态内存的部分,可以发现潜在的内存泄漏问题。 总之,检测C++中的内存泄漏是一个多方面的工作,涉及到...
一旦配置好OptimizeIt,可以通过以下几个步骤来进行内存泄漏的检查: - **收集数据**:运行应用程序,并在运行过程中收集内存使用数据。 - **分析数据**:分析收集的数据,找出内存使用模式中的异常情况。 - **定位...
4. **分析内存泄漏**:使用“对象保留图”功能,可以查看哪些对象被保留,从而找出导致内存泄漏的对象。如果发现某些对象的引用链过长,可能就是问题所在。 5. **定位问题代码**:JProfiler还提供了源代码级别的...
本文将详细介绍如何使用UMDH来分析内存泄漏。 一、UMDH工具简介 UMDH全称为User-Mode Dump Heap,它是一个命令行工具,可以从用户模式的进程创建内存转储文件,并分析该文件以获取内存分配和使用的信息。UMDH能够...
7. MemoryProfiler:对于Python开发者,有一个名为MemoryProfiler的库,可以用来分析Python脚本运行时的内存使用情况,找出可能的内存泄露点。 使用这些工具时,通常需要按照以下步骤进行: 1. 构建项目:首先,...
4. **日志记录**:工具可以记录内存变化的历史数据,通过对比不同时间点的内存状态,帮助分析内存增长趋势,进一步定位问题。 对于使用tolua、xlua或slua等库将C++与Lua结合的项目,内存管理变得更加复杂,因为涉及...