java内存泄露处理的方法
2010-03-29 14:50
http://www.iteye.com/topic/626801
直赞叹Sun对待技术的严谨和优雅(可怜的Sun)。Sun JDK中Java库的源代码,连注释都清清楚楚、规规范范,javadoc注解的使用也一丝不苟,读起来很熟舒服。因此,在日常工作和学习中,经常读读 Java库的源代码,不亦乐乎?如果遇到诡异问题,源代码的帮助就更大了。
闲话少说,回归正题。这几天,一直在为Java的“内存泄露”问题纠结。Java应用程序占用的内存在不断的、有规律的上涨,最终超过了监控阈值。福尔摩 斯不得不出手了!
说起Java的内存泄露,其实定义不是那么明确。首先,如果JVM没有bug,那么理论上是不会出现“无法回收的堆空间”,也就是说C/C++中的那种内 存泄露在Java中不存在的。其次,如果由于Java程序一直持有某个对象的引用,但是从程序逻辑上看,这个对象再也不会被用到了,那么我们可以认为这个 对象被泄露了。如果这样的对象数量很多,那么很明显,大量的内存空间就被泄露(“浪费”更准确一些)了。
不过,本文要说的内存泄露,并不属于上述原因,因此打上了引号。其具体原因,确实出乎意料。欲知详情,请看下面讲解。
分析内存泄露的一般步骤
如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析
把Java应用程序使用的heap dump下来
使用Java heap分析工具,找出内存占用超出预期(一般是因为数量太多)的嫌疑对象
必要时,需要分析嫌疑对象和其他对象的引用关系。
查看程序的源代码,找出嫌疑对象数量过多的原因。
dump heap
如果Java应用程序出现了内存泄露,千万别着急着把应用杀掉,而是要保存现场。如果是互联网应用,可以把流量切到其他服务器。保存现场的目的就是为了把 运行中JVM的heap dump下来。
JDK自带的jmap工具,可以做这件事情。它的执行方法是:
Java代码
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方法的实现:
Java代 码
public
String[] split(String regex, int limit) {
return Pattern.compile(regex).split(this, limit);
}
public String[] split(String regex, int limit) { return Pattern.compile(regex).split(this, limit); }
可以看出,Stirng.split方法调用了Pattern.split方法。继续看Pattern.split方法的代码:
Java代 码
public
String[] split(CharSequence input, int limit) {
int index = 0;
boolean matchLimited = limit > 0;
ArrayList<String> matchList = new
ArrayList<String>();
Matcher m = matcher(input);
// Add segments before each match found
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) { // last one
String match = input.subSequence(index,
input.length()).toString();
matchList.add(match);
index = m.end();
}
}
// If no match was found, return this
if (index == 0)
return new String[] {input.toString()};
// Add remaining segment
if (!matchLimited || matchList.size() < limit)
matchList.add(input.subSequence(index,
input.length()).toString());
// Construct result
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);
}
public String[] split(CharSequence input, int limit) { int index = 0; boolean matchLimited = limit > 0; ArrayList<String> matchList = new ArrayList<String>(); Matcher m = matcher(input); // Add segments before each match found 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) { // last one String match = input.subSequence(index, input.length()).toString(); matchList.add(match); index = m.end(); } } // If no match was found, return this if (index == 0) return new String[] {input.toString()}; // Add remaining segment if (!matchLimited || matchList.size() < limit) matchList.add(input.subSequence(index, input.length()).toString()); // Construct result 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的代码:
Java代 码
public
CharSequence subSequence(int beginIndex, int endIndex) {
return this.substring(beginIndex, endIndex);
}
public CharSequence subSequence(int beginIndex, int endIndex) { return this.substring(beginIndex, endIndex); }
String.subSequence有调用了String.subString,继续看:
Java代 码
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);
}
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的构造函数确认这一点:
Java代 码
// Package
private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
// Package 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),这个构造函数是安全的,具体可以看代码:
Java代 码
/**
* Initializes a newly created {@code String} object so that it
represents
* the same sequence of characters as the argument; in other words,
the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this
constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
if (originalValue.length > size) {
// The array representing the String is bigger than the new
// String itself. Perhaps this constructor is being called
// in order to trim the baggage, so make a copy of the array.
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off+size);
} else {
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}
/** * Initializes a newly created {@code String} object so that it represents * the same sequence of characters as the argument; in other words, the * newly created string is a copy of the argument string. Unless an * explicit copy of {@code original} is needed, use of this constructor is * unnecessary since Strings are immutable. * * @param original * A {@code String} */ public String(String original) { int size = original.count; char[] originalValue = original.value; char[] v; if (originalValue.length > size) { // The array representing the String is bigger than the new // String itself. Perhaps this constructor is being called // in order to trim the baggage, so make a copy of the array. int off = original.offset; v = Arrays.copyOfRange(originalValue, off, off+size); } else { // The array representing the String is the same // size as the String, so no point in making a copy. 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。
分享到:
相关推荐
因此,了解Java内存泄漏的成因、检测方法以及解决方案对于保证应用的高效稳定运行至关重要。 #### 2. Java内存回收机制 Java的内存管理主要集中在堆(Heap)区域,其中对象的创建通常是通过`new`关键字或反射方式...
这篇博客“JAVA内存泄漏问题处理方法经验总结”分享了作者在处理此类问题时的一些实用技巧和经验,结合源码分析和工具使用,对于Java开发者来说是非常有价值的。 首先,了解内存泄漏的基本概念是至关重要的。内存...
### Java内存泄漏解决方案详解 #### 一、Java内存泄漏概述 在Java开发过程中,经常会遇到内存泄漏的问题,尤其是在长时间运行的应用程序中更为常见。本文将详细介绍如何解决Java内存泄漏问题,帮助开发者更好地...
Java内存泄漏分析是一个关键的系统优化任务,尤其是在大型企业级应用中,长期运行的系统可能会因为内存泄漏导致性能下降甚至服务中断。"JAVA内存泄漏分析工具"正是一款用于解决此类问题的专业工具,它能帮助开发者...
Java内存机制是Java虚拟机(JVM)的关键组成部分,它管理着程序运行时的数据存储。在Java中,内存主要分为以下几个区域: ...正确理解和运用Java内存机制以及异常处理机制对于开发健壮、高效的Java应用程序至关重要。
4. "java内存泄露专题研究和应用_石麟.docx"可能提供了更深入的研究和实际案例,包括如何识别特定类型的内存泄漏,以及针对不同场景下的解决方案。而"ha450.jar"可能是一个示例应用或者工具,用于演示内存泄漏问题...
Java内存泄露检测是Java...总之,Java内存泄露检测是一个涉及编程规范、内存管理原理、内存泄露类型以及检测方法的综合问题。通过深入理解和实践,开发者可以有效地防止和解决内存泄露问题,提高程序的健壮性和性能。
本文将深入探讨Java内存泄露的原因、表现形式以及预防措施。 #### 二、Java内存回收机制 Java的内存管理机制主要依赖于垃圾回收器(Garbage Collection, GC),这是一种自动化的内存管理方式。当对象不再被引用时,...
### Java内存泄露、溢出检查方法及工具详解 #### 一、引言 在实际的项目开发和运维过程中,经常会遇到Java应用程序出现内存溢出(`java.lang.OutOfMemoryError`)的情况。这类问题不仅影响应用程序的稳定性和性能...
如果DLL中分配了内存但未正确释放,或者Java和DLL之间对对象的引用处理不当,都可能导致内存泄露,使得Java进程的内存占用持续增长,影响系统性能。 描述中的“NULL”可能是博主在描述问题时的简化表示,通常在编程...
Java内存泄漏问题处理方法经验总结 Java内存泄漏问题处理方法经验总结是Java开发中非常重要的一个方面,今天我们将分享关于Java内存泄漏问题处理方法经验总结的知识点。 首先,Java内存泄漏的问题描述是系统在...
### Java内存泄露总结 #### 一、Java内存回收机制 Java中的内存管理主要依赖于垃圾回收(Garbage Collection, GC)机制。与C/C++等需要手动管理内存的语言不同,Java虚拟机(JVM)自动处理对象的创建与销毁过程。...
以下是关于Java内存泄露的一些关键知识点和分析方法: 1. **内存泄露定义**: 内存泄露是指程序在申请内存后,无法释放已申请的内存空间,一次小的内存泄露可能不明显,但随着时间推移,内存消耗持续增长,最终...
### Java避免内存泄露的关键知识点 #### 一、内存泄露的概念及原因 内存泄露是指程序在申请内存后未能释放,导致这部分内存无法再次被利用。在Java中,由于具备垃圾回收机制(GC),理论上开发者不必担心内存泄露...
二、Java内存模型 在Java中,内存分为堆(Heap)和栈(Stack)两部分。堆是存储对象实例的地方,而栈则存储方法调用时的局部变量。Java虚拟机(JVM)有自动垃圾回收机制(Garbage Collection, GC),用于回收不再...
Java编程语言在处理大型应用程序时,内存管理是一个至关重要的环节。内存泄漏是导致程序性能下降,甚至引发Out of Memory (OOM)错误的主要原因之一。本文将详细介绍如何使用JMAP工具来定位Java代码中的内存泄漏问题...
Java内存泄露是一个重要的技术主题,尤其对于开发大型、长期运行的应用程序来说,它可能导致系统性能下降,甚至引发系统崩溃。本文将深入探讨内存泄露的概念、分类、后果以及检测方法。 内存泄露简介: 内存泄露指...
9. 保持更新:随着 Java 平台的不断改进,新的垃圾收集算法和内存管理特性可以更好地处理内存泄漏问题。定期升级到最新版本的 JDK 可能有助于解决此类问题。 总之,理解 Java 内存管理和垃圾收集的工作原理对于识别...
解决Java内存泄漏的方法主要包括: 1. 及时清除不再使用的对象引用,如从集合中移除对象,将变量设为null。 2. 避免使用静态变量引用大对象或长时间存在的对象。 3. 注意匿名内部类的使用,确保在适当的时候断开引用...
处理Java内存泄漏的策略包括: 1. 代码审查:确保代码中没有对已无用对象的长期引用。 2. 使用智能工具:有许多工具可以帮助检测和分析内存泄漏,例如VisualVM、MAT(Memory Analyzer Tool)等。这些工具可以显示内存...