热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

String类的split方法引起的内存泄漏

一直赞叹Sun对待技术的严谨和优雅(可怜的Sun)。Sun JDK中Java库的源代码,连注释都清清楚楚、规规范范,javadoc注解的使用也一
一直赞叹Sun对待技术的严谨和优雅(可怜的Sun)。Sun JDK中Java库的源代码,连注释都清清楚楚、规规范范,javadoc注解的使用也一丝不苟,读起来很熟舒服。因此,在日常工作和学习中,经常读读 Java库的源代码,不亦乐乎?如果遇到诡异问题,源代码的帮助就更大了。

 

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

 

    说起Java的内存泄露,其实定义不是那么明确。首先,如果JVM没有bug,那么理论上是不会出现“无法回收的堆空间”,也就是说C/C++中的那种内存泄露在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   

jmap -dump:format=b,file=heap.bin  

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

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

    就是JVM的进程号。

    (在linux下)先执行ps aux | grep java,找到JVM的pid;然后再执行jmap -dump:format=b,file=heap.bin ,得到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 matchList = new    
  6. ArrayList();   
  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 &#61; input.subSequence(index,    
  12. m.start()).toString();   
  13.                 matchList.add(match);   
  14.                 index &#61; m.end();   
  15.             } else if (matchList.size() &#61;&#61; limit - 1) { // last one   
  16.                 String match &#61; input.subSequence(index,   
  17.                                                     
  18. input.length()).toString();   
  19.                 matchList.add(match);   
  20.                 index &#61; m.end();   
  21.             }   
  22.         }   
  23.         // If no match was found, return this   
  24.         if (index &#61;&#61; 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 &#61; matchList.size();   
  32.         if (limit &#61;&#61; 0)   
  33.             while (resultSize > 0 &&    
  34. matchList.get(resultSize-1).equals(""))   
  35.                 resultSize--;   
  36.         String[] result &#61; new String[resultSize];   
  37.         return matchList.subList(0, resultSize).toArray(result);   
  38.     }  



    注意看第9行&#xff1a;Stirng match &#61; input.subSequence(intdex, m.start()).toString();

这里的match就是split出来的String小对象&#xff0c;它其实是String大对象subSequence的结果。继续看 String.subSequence的代码&#xff1a;


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



    String.subSequence有调用了String.subString&#xff0c;继续看&#xff1a;


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 &#61;&#61; 0) && (endIndex &#61;&#61; count)) ? this :   
  13.         new String(offset &#43; beginIndex, endIndex - beginIndex, value);   
  14.     }  



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


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



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

原因解释

 

    其实上一节已经分析出了原因&#xff0c;这一节再整理一下&#xff1a;

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

解决方案

 

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


Java代码
  1.     /**  
  2.      * Initializes a newly created {&#64;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 {&#64;code original} is needed, use of this   
  8. constructor is  
  9.      * unnecessary since Strings are immutable.  
  10.      *  
  11.      * &#64;param  original  
  12.      *         A {&#64;code String}  
  13.      */  
  14.     public String(String original) {   
  15.     int size &#61; original.count;   
  16.     char[] originalValue &#61; 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 &#61; original.offset;   
  23.             v &#61; Arrays.copyOfRange(originalValue, off, off&#43;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 &#61; originalValue;   
  28.     }   
  29.     this.offset &#61; 0;   
  30.     this.count &#61; size;   
  31.     this.value &#61; v;   
  32.     }  



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

是否Bug

 

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

 

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

一些补充

有个地方我没有说清楚。

 

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

 

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

转至http://www.iteye.com/topic/626801大家可看原文


推荐阅读
  • 正则表达式及其范例
    为什么80%的码农都做不了架构师?一、前言部分控制台输入的字符串,编译成java字符串之后才送进内存,比如控制台打\, ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • Java程序设计第4周学习总结及注释应用的开发笔记
    本文由编程笔记#小编为大家整理,主要介绍了201521123087《Java程序设计》第4周学习总结相关的知识,包括注释的应用和使用类的注释与方法的注释进行注释的方法,并在Eclipse中查看。摘要内容大约为150字,提供了一定的参考价值。 ... [详细]
  • Hibernate延迟加载深入分析-集合属性的延迟加载策略
    本文深入分析了Hibernate延迟加载的机制,特别是集合属性的延迟加载策略。通过延迟加载,可以降低系统的内存开销,提高Hibernate的运行性能。对于集合属性,推荐使用延迟加载策略,即在系统需要使用集合属性时才从数据库装载关联的数据,避免一次加载所有集合属性导致性能下降。 ... [详细]
  • java io换行符_Java IO:为什么从stdin读取时,换行符的数字表示出现在控制台上?...
    只是为了更好地理解我在讲座中听到的内容(关于Java输入和输出流),我自己做了这个小程序:publicstaticvoidmain(String[]args)thro ... [详细]
  • 本文介绍了解决Netty拆包粘包问题的一种方法——使用特殊结束符。在通讯过程中,客户端和服务器协商定义一个特殊的分隔符号,只要没有发送分隔符号,就代表一条数据没有结束。文章还提供了服务端的示例代码。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • 本文介绍了在Java中检查字符串是否仅包含数字的方法,包括使用正则表达式的示例代码,并提供了测试案例进行验证。同时还解释了Java中的字符转义序列的使用。 ... [详细]
  • Ihaveaworkfolderdirectory.我有一个工作文件夹目录。holderDir.glob(*)>holder[ProjectOne, ... [详细]
  • 使用freemaker生成Java代码的步骤及示例代码
    本文介绍了使用freemaker这个jar包生成Java代码的步骤,通过提前编辑好的模板,可以避免写重复代码。首先需要在springboot的pom.xml文件中加入freemaker的依赖包。然后编写模板,定义要生成的Java类的属性和方法。最后编写生成代码的类,通过加载模板文件和数据模型,生成Java代码文件。本文提供了示例代码,并展示了文件目录结构。 ... [详细]
  • R语言拼接字符串_paste的用法说明
    这篇文章主要介绍了R语言拼接字符串_paste的用法说明,具有很好的参考价值,希望对大家有所帮助。一 ... [详细]
  • Annotation的大材小用
    为什么80%的码农都做不了架构师?最近在开发一些通用的excel数据导入的功能,由于涉及到导入的模块很多,所以开发了一个比较通用的e ... [详细]
  • 巧用arguments在Javascript的函数中有个名为arguments的类数组对象。它看起来是那么的诡异而且名不经传,但众多的Javascript库都使用着它强大的功能。所 ... [详细]
  • 获取时间的函数js代码,js获取时区代码
    本文目录一览:1、js获取服务器时间(动态)2 ... [详细]
author-avatar
特别要_966
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有