转载:原文地址:http://jarfield.iteye.com/admin/blogs/583946
一直赞叹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工具,可以做这件事情。它的执行方法是:
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);
- // 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的代码:
- 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的构造函数确认这一点:
- // 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),这个构造函数是安全的,具体可以看代码:
- /**
- * 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。
“抛出异常的爱”同学,在回帖(第7页)中建议用"java.io.StreamTokenizer"来解决本文的问题。确实是终极解决方案,比我上面提到的“new String()”,要好很多很多。
发表评论
-
(转载)又见内存泄露
2012-02-27 20:54 0转载:http://www.iteye.com/topic/1 ... -
(转载)详解spring事务属性
2012-02-27 20:53 0转载:http://www.iteye.com/t ... -
(转载)简明扼要,再谈ThreadLocal和synchronized
2012-02-27 20:52 0转载:http://www.iteye.com/topic/1 ... -
(转载)Spring声明式事务管理源码解读之事务开始
2012-02-27 20:51 0转载:http://www.iteye.com/topic/8 ... -
(转载)Web Services开发体会和项目教训
2012-02-27 20:50 0转载:http://www.iteye.com/topic/7 ... -
(转载)使用JDBC和Hibernate来写入Blob型数据到Oracle中
2012-02-27 20:49 0转载:http://www.iteye.com/topic/2 ... -
(转载)Java学习从入门到精通
2012-02-27 20:45 0转载:http://www.iteye.com/t ... -
(转载)解惑 spring 嵌套事务
2012-02-27 20:43 0转载:http://www.iteye.com/topic/3 ... -
(转载)Spring源代码解析(一):IOC容器
2012-02-27 20:41 0转载:http://www.iteye.com/topic/8 ... -
(转载)Word/Excel/PDF文件转换成HTML整理
2012-02-26 23:24 0转载:http://www.iteye.com/topic/5 ... -
(转载)为什么需要Singleton
2012-02-26 23:22 0转载:http://www.iteye.com/topic/4 ... -
(转载)java并发编程-Executor框架
2012-02-26 23:21 0转载:http://www.iteye.com/topic/3 ... -
(转载)Spring注解入门
2012-02-26 23:21 0转载:http://www.iteye.com/topic/2 ... -
(转载)一个简单例子:贫血模型or领域模型
2012-02-26 23:19 714转载:http://www.iteye.com/topic/2 ... -
(转载)JVM如何理解Java泛型类
2012-02-26 23:18 797转载:http://www.iteye.com/topic/5 ...
相关推荐
本文将深入探讨String类的特性、构造、方法以及在内存中的表现,帮助你全面理解这个核心类。 首先,String类在Java中是不可变的。这意味着一旦创建了一个String对象,它的值就不能被改变。这是由其内部实现决定的,...
- 如果库未提供清理方法,记得在不再使用分割结果时释放内存,以防止内存泄漏。 6. **优化和替代方案**:对于内存敏感的项目,可以考虑使用字符数组(C 风格字符串)和指针,或者利用 `split()` 函数的返回值,...
综上,Java中的String类提供了丰富的方法来处理字符串,但正确使用这些方法需要对Java内存模型、不可变对象和多线程安全等方面有深刻理解。这十个问题和解决方法,可以帮助开发者高效且正确地在Java中进行字符串操作...
此外,由于Util类通常是静态方法,需要注意避免创建过多的静态变量以防止内存泄漏。 在压缩包文件`util`中,可能包含了作者实现的一些定制化的DateUtil和StringUtil类。这些类可能包含了上述提及的一些方法,或者是...
描述中提到的“用C++写”的字符串分割函数,暗示了我们不是在讨论标准库(如`std::string`的`std::getline`或`std::stringstream`)中的方法,而是在创建一个定制的解决方案,可能是为了适应特定的性能需求或与MFC的...
- **安全性:** `string` 类内部自动管理内存,避免了传统 C 字符串可能引发的内存泄漏或溢出等问题。 - **便利性:** 提供了许多成员函数来处理字符串,如拼接、查询等。 - **兼容性:** 能够很好地与其他标准库...
可以使用String类的split()方法,并传入一个正则表达式来分割字符串。例如,str.split("\\s+")会按照一个或多个空白字符分割字符串。其中,“\s”代表任意空白字符,包括空格、制表符(\t)、回车符(\r)和换行符(\n)...
理解字符串的内存管理对于避免内存泄漏和提高程序性能至关重要。 5. **字符串常量与变量**:字符串可以是常量(不可改变)或变量(可改变)。在C/C++中,字符串常量是字符数组的别名,而Java和Python则有特殊的字符...
这个类提供了两个静态方法:`WriteValue`用于写入键值对,`ReadValue`用于读取键对应的值。它们接受文件路径、节名、键名作为参数,使得在代码中调用非常直观。 4. 使用示例: 要写入一个值,可以这样调用: ```...
合理的内存管理不仅能提高程序的运行效率,还能避免内存泄漏等问题。本题目的核心是设计并实现一个简易的内存池管理系统,该系统能够有效地处理内存的分配与释放操作。 #### 题目要求概述 - **内存池总大小**:100...
### 面向对象知识点详解 #### 一、引用数据的操作深入 在Java中,数据类型分为两大类:基本数据类型和引用数据类型。其中,引用数据类型包括类...同时,熟悉String类的特性和方法也是日常开发中必不可少的一部分。
在处理字符串时,还需要注意内存管理,特别是在使用动态分配的内存(如`new`运算符)时,确保在适当的地方释放资源以避免内存泄漏。同时,考虑到异常安全性和性能优化,例如在处理大量数据时,可能需要考虑使用迭代...
35. 使用String.split()方法可以将逗号分割的字符串转换成数组。 36. 数组的length属性是一个final属性,String的length()方法用于返回字符串的长度。 37. Strings="a"+"b"+"c"+"d";这行代码在编译时会被优化,...
### 千方百计笔试题大全 - Core Java 部分 ...使用 `String.split()` 方法可以按照指定的分隔符分割字符串。 #### 59. Java 编程, 打印昨天的当前时刻。 可以使用 `Calendar` 类或 `LocalDate` 类...
`String`类提供了一个名为`charAt(int index)`的方法,可以用来获取字符串中指定索引位置的字符。索引从0开始,例如: ```java String s = "Hello"; char c = s.charAt(1); // c 的值为 'e' ``` #### 四、如何使用...
- 使用`String.split()`方法根据指定的分隔符对字符串进行分割。 #### 四十六、打印昨天的时间 - 使用`java.util.Date`和`java.util.Calendar`或`java.time.LocalDate`来计算并打印昨天的时间。 #### 四十七、Java...
将逗号分隔的字符串转换为数组,可以使用`split()`方法,例如`String[] arr = str.split(",")`。这将按照逗号分隔字符串,并返回一个字符串数组。 #### 36\. 数组与`String`的`length`属性 数组确实没有`length()`...