`
wuhenliushui
  • 浏览: 17473 次
社区版块
存档分类
最新评论

Java 性能优化之 String 篇

 
阅读更多

简介:String 方法用于文本分析及大量字符串处理时会对内存性能造成不可低估的影响。我们在一个大文本数据分析的项目中(我们统计一个约 300MB 的 csv 文件中所有单词出现的次数)发现,用于存放结果的 Collection 占用了几百兆的内存,远远超出唯一单词总数 20000 个。 本文将通过分析 String 在 JVM 中的存储结构,以及常见 String 操作对内存的影响阐述问题产生的原因及解决。.

本文的标签:api,应用开发,性能,算法




String 在 JVM 的存储结构

一般而言,Java 对象在虚拟机的结构如下:

  • 对象头(object header):8 个字节
  • Java 原始类型数据:如 int, float, char 等类型的数据,各类型数据占内存如表 1. Java 各数据类型所占内存.
  • 引用(reference):4 个字节
  • 填充符(padding)

表 1. Java 各数据类型所占内存
数据类型 占用内存(字节数)
boolean 1
byte
char 2
short
int 4
float
long 8
double

然而,一个 Java 对象实际还会占用些额外的空间,如:对象的 class 信息、ID、在虚拟机中的状态。在 Oracle JDK 的 Hotspot 虚拟机中,一个普通的对象需要额外 8 个字节。

如果对于 String(JDK 6)的成员变量声明如下:

  private final char value[]; 
  private final int offset; 
  private final int count; 
  private int hash; 		
		

那么因该如何计算该 String 所占的空间?

首先计算一个空的 char 数组所占空间,在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度,即 8 + 4 = 12 字节 , 经过填充后为 16 字节。

那么一个空 String 所占空间为:

对象头(8 字节)+ char 数组(16 字节)+ 3 个 int(3 × 4 = 12 字节)+1 个 char 数组的引用 (4 字节 ) = 40 字节。

因此一个实际的 String 所占空间的计算公式如下:

8*( ( 8+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )

其中,n 为字符串长度。

案例分析

在我们的大规模文本分析的案例中,程序需要统计一个 300MB 的 csv 文件所有单词的出现次数,分析发现共有 20,000 左右的唯一单词,假设每个单词平均包含 15 个字母,这样根据上述公式,一个单词平均占用 75 bytes. 那么这样 75 * 20,000 = 1500000,即约为 1.5M 左右。但实际发现有上百兆的空间被占用。 实际使用的内存之所以与预估的产生如此大的差异是因为程序大量使用String.split()String.substring()来获取单词。在 JDK 1.6 中String.substring(int, int)的源码为:

 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); 
 } 			 
			

调用的 String 构造函数源码为:

 String(int offset, int count, char value[]) { 
 this.value = value; 
 this.offset = offset; 
 this.count = count; 
 } 

仔细观察粗体这行代码我们发现String.substring()所返回的 String 仍然会保存原始 String, 这就是 20,000 个平均长度的单词竟然占用了上百兆的内存的原因。 一个 csv 文件中每一行都是一份很长的数据,包含了上千的单词,最后被String.split()String.substring()截取出的每一个单词仍旧包含了其原先所在的上下文中,因而导致了出乎意料的大量的内存消耗。

当然,JDK String 的源码设计当然有着其合理之处,对于通过String.split()String.substring()截取出大量 String 的操作,这种设计在很多时候可以很大程度的节省内存,因为这些 String 都复用了原始 String,只是通过 int 类型的 start, end 等值来标识每一个 String。 而对于我们的案例,从一个巨大的 String 截取少数 String 为以后所用,这样的设计则造成大量冗余数据。 因此有关通过String.split()String.substring()截取 String 的操作的结论如下:

  • 对于从大文本中截取少量字符串的应用,String.substring()将会导致内存的过度浪费。
  • 对于从一般文本中截取一定数量的字符串,截取的字符串长度总和与原始文本长度相差不大,现有的String.substring()设计恰好可以共享原始文本从而达到节省内存的目的。

既然导致大量内存占用的根源是String.substring()返回结果中包含大量原始 String,那么一个显而易见的减少内存浪费的的途径就是去除这些原始 String。办法有很多种,在此我们采取比较直观的一种,即再次调用newString构造一个的仅包含截取出的字符串的 String,我们可调用String.toCharArray()方法:

 String newString = new String(smallString.toCharArray()); 

举一个极端例子,假设要从一个字符串中获取所有连续的非空子串,字符串长度为 n,如果用 JDK 本身提供的String.substring() 方法,则总共的连续非空子串个数为:

n+(n-1)+(n-2)+ … +1 = n*(n+1)/2 =O(n2)

由于每个子串所占的空间为常数,故空间复杂度也为 O(n2)。

如果用本文建议的方法,即构造一个内容相同的新的字符串,则所需空间正比于子串的长度,则所需空间复杂度为:

1*n+2*(n-1)+3*(n-2)+ … +n*1 = (n3+3*n2+2*n)/6 = O(n3)

所以,从以上定量的分析看来,当需要截取的字符串长度总和大于等于原始文本长度,本文所建议的方法带来的空间复杂度反而高了,而现有的String.substring()设计恰好可以共享原始文本从而达到节省内存的目的。反之,当所需要截取的字符串长度总和远小于原始文本长度时,用本文所推荐的方法将在很大程度上节省内存,在大文本数据处理中其优势显而易见。

其他 String 使用的优化建议

以上我们描述了在我们的大量文本分析案例中调用 String 的subString方法导致内存消耗的问题,下面再列举一些其他将导致内存浪费的 String 的 API 的使用:

String 拼接的方法选择

在拼接静态字符串时,尽量用 +,因为通常编译器会对此做优化,如:

 String test = "this " + "is " + "a " + "test " + "string"

编译器会把它视为:

 String test = "this is a test string"

在拼接动态字符串时,尽量用StringBufferStringBuilderappend,这样可以减少构造过多的临时 String 对象。

String 构造的方法选择

常见的创建一个 String 可以用赋值操作符"=" 或用 new 和相应的构造函数。初学者一定会想这两种有何区别,举例如下:

 String a1 = “Hello”; 
 String a2 = new String(“Hello”); 

第一种方法创建字符串时 JVM 会查看内部的缓存池是否已有相同的字符串存在:如果有,则不再使用构造函数构造一个新的字符串,直接返回已有的字符串实例;若不存在,则分配新的内存给新创建的字符串。

第二种方法直接调用构造函数来创建字符串,如果所创建的字符串在字符串缓存池中不存在则调用构造函数创建全新的字符串,如果所创建的字符串在字符串缓存池中已有则再拷贝一份到 Java 堆中。

尽管这是一个简单明显的例子,然而在实际项目中编程者却不那么容易洞察因为这两种方式的选择而带来的性能问题。

使用构造函数 string() 带来的内存性能隐患和缓解

仍然以之前的从 csv 文件中截取 String 为例,先前我们通过用 new String() 去除返回的 String 中附带的原始 String 的方法优化了subString导致的内存消耗问题。然而,当我们下意识地使用newString去构造一个全新的字符串而不是用赋值符来创建(重用)一个字符串时,就导致了另一个潜在的性能问题,即:重复创建大量相同的字符串。说到这里,您也许会想到使用缓存池的技术来解决这一问题,大概有如下两种方法:

方法一,使用 String 的intern()方法返回 JVM 对字符串缓存池里相应已存在的字符串引用,从而解决内存性能问题,但这个方法并不推荐!原因在于:首先,intern()所使用的池会是 JVM 中一个全局的池,很多情况下我们的程序并不需要如此大作用域的缓存;其次,intern() 所使用的是 JVM heap 中 PermGen 相应的区域,在 JVM 中 PermGen 是用来存放装载类和创建类实例时用到的元数据。程序运行时所使用的内存绝大部分存放在 JVM heap 的其他区域,过多得使用intern()将导致 PermGen 过度增长而最后返回OutOfMemoryError,因为垃圾收集器不会对被缓存的 String 做垃圾回收。所以我们建议使用第二种方式。

方法二,用户自己构建缓存,这种方式的优点是更加灵活。创建 HashMap,将需缓存的 String 作为 key 和 value 存放入 HashMap。假设我们准备创建的字符串为 key,将 Map cacheMap 作为缓冲池,那么返回 key 的代码如下:

 private String getCacheWord(String key) { 
     String tmp = cacheMap.get(key); 
     if(tmp != null) { 
            return tmp; 
     } else { 
             cacheMap.put(key, key); 
             return key; 
     } 
 } 


结束语

本文通过一个实际项目中遇到的因使用 String 而导致的性能问题讲述了 String 在 JVM 中的存储结构,String 的 API 使用可能造成的性能问题以及解决方法。相信这些建议能对处理大文本分析的朋友有所帮助,同时希望文中提到的某些优化方法能被举一反三的应用在其他有关 String 的性能优化的场合。



原文:http://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/


分享到:
评论

相关推荐

    阿里巴巴Java性能调优实战(2021-2022华山版)+Java架构核心宝典+性能优化手册100技巧.rar

    性能优化手册是一套java性能学习研究小技巧,包含内容:Java性能优化、JVM性能优化、服务器性能优化、数据库性能优化、前端性能优化等。 内容包括但不限于: String 性能优化的 3 个小技巧 HashMap 7 种遍历方式...

    《Java程序性能优化》(葛一鸣)PDF版本下载.txt

    根据提供的文件信息,我们可以推断出这是一本关于Java程序性能优化的书籍,作者是葛一鸣,并提供了该书PDF版本的下载链接。虽然没有具体的书籍内容,但基于标题、描述以及通常这类书籍会涉及的主题,我们可以总结出...

    Java性能优化手册100技巧 中文PDF最新版

    性能优化手册是一套java性能学习研究小技巧,包含内容:Java性能优化、JVM性能优化、服务器性能优化、数据库性能优化、前端性能优化等。 内容包括但不限于: String 性能优化的 3 个小技巧 HashMap 7 种遍历方式...

    Java 性能优化实战 21 讲

    在Java性能优化实战的21讲中,涵盖了Java开发中至关重要的性能调优技术,旨在提升应用程序的效率、稳定性和可扩展性。以下是对这些关键知识点的详细解析: 1. **JVM内存模型**:理解Java虚拟机(JVM)的内存结构是...

    java性能优化java性能优化

    Java性能优化是提升Java应用程序效率的关键技术,涵盖了内存管理、代码优化、I/O处理等多个方面。以下是一些关键的性能优化策略: 1. **对象创建与克隆**:使用`new`关键字创建对象时,会调用构造函数链,这可能...

    Java性能优化的策略研究.pdf

    垃圾回收机制是Java性能优化的关键之一。需要合理地配置垃圾回收机制的参数,避免垃圾回收机制对系统性能的影响。 3. 代码优化。代码优化是Java性能优化的重要方面。需要合理地编写代码,避免使用低效的算法,使用...

    java性能优化集锦

    本资料集锦主要涵盖了Java性能优化的多个方面,包括但不限于代码优化、内存管理、系统设计与优化等关键领域。 一、Java程序性能优化(23条) 1. **避免过度使用反射**:反射在某些场景下很有用,但其开销较大,应...

    JAVA性能优化.pptx

    ### JAVA性能优化关键知识点 #### 一、字符串处理与优化 **1.1 字符串拼接** - **原理解析**: 在Java中,`String` 类型是不可变的,这意味着每次对 `String` 对象进行修改都会创建一个新的 `String` 对象。当涉及...

    Java程序性能优化.rar

    这份资料"Java程序性能优化.rar"包含了高清文档和书籍源码,为我们提供了深入学习和实践Java性能优化的机会。 1. **JVM调优** - **垃圾回收(Garbage Collection)**:理解不同的GC算法,如Serial、Parallel、CMS...

    java程序性能优化-pdf+源码

    通过学习这本书,开发者不仅可以掌握Java性能优化的核心技术,还能培养出良好的编程习惯,使程序运行得更快、更稳定,为企业的系统性能提供有力保障。同时,对于个人技能提升和职业发展,这本书也具有很高的价值。

    java性能优化方法

    Java性能优化是提升系统效率的关键环节,特别是在处理大量数据或者高并发场景时,优化显得尤为重要。本主题将围绕“Java性能优化方法”展开,重点讨论Java集合的排序、反射机制在Spring中的应用以及如何减少对象的...

    Java性能优化技巧集锦.doc

    Java性能优化技巧集锦是一篇详细的技术文章,旨在帮助Java开发者提高应用程序的性能。下面是该文章中提到的重要知识点: 一、通用篇 1.1 不用 new 关键词创建类的实例 使用clone()方法创建新的对象实例,而不是...

    java performance java性能优化

    ### Java性能优化技巧详解 #### 一、引言 在当今快速发展的软件行业中,提高程序的执行效率成为了软件工程师的一项重要任务。对于Java开发者来说,理解如何优化Java程序的性能至关重要。本文将深入探讨Java性能...

    《java性能优化》源码

    《Java性能优化》一书是Java开发者的重要参考资料,它深入探讨了如何提升Java应用程序的运行效率,涵盖了从代码设计到系统调优的多个层面。源码是书籍理论知识的实践体现,通过分析和学习这些源码,我们可以更直观地...

    Java性能优化技巧集锦

    【Java性能优化技巧集锦】 Java性能优化是一个关键的话题,对于提升应用程序的效率和响应速度至关重要。以下是一些通用和特定领域的性能优化技巧。 一、通用篇 1.1 不用 new 关键词创建类的实例 使用`new`创建...

    java开发性能优化

    Java开发中的性能优化是提升应用程序效率的关键步骤。在编程中,我们需要关注代码的运行效率,以确保程序在处理大量数据或高并发场景下仍能保持高效。以下是一些Java编程中为了性能需要注意的地方: 1. **单例模式...

    Java中String性能优化

    以下是一些关键的Java String性能优化策略: 1. 避免使用String构造函数: 直接使用字符串字面量通常是更高效的选择,而不是通过构造函数创建String对象。例如,`String str = "example"`比`String str = new ...

    java性能优化.pdf

    Java性能优化是一个重要的主题,它涉及程序的效率和资源利用率。优化的目标是在有限的资源下,比如内存、CPU时间和网络带宽,使程序能够高效地完成任务。优化可以从两个方面入手:减小代码体积和提高代码运行效率。...

Global site tag (gtag.js) - Google Analytics