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

Java 性能优化之 String 篇

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

13 评论:

杨 博文, 软件工程师, IBM

应 乐年, 软件工程师, IBM

杨 雯雯, 技术文档工程师, IBM

2012 年 5 月 14 日

  • 内容

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"

在拼接动态字符串时,尽量用 StringBuffer 或 StringBuilder的 append,这样可以减少构造过多的临时 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 的性能优化的场合。

参考资料

学习

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

Java 性能优化之 String 篇的更多相关文章

  1. Java性能优化之String字符串优化

    字符串是软件开发中最重要的对象之一.通常,字符串对象在内存中是占据了最大的空间块,因此如何高效地处理字符串,必将是提高整体性能的关键所在. 1.字符串对象及其特点 Java中八大基本数据类型没有Str ...

  2. 【转载】 Spark性能优化指南——基础篇

    转自:http://tech.meituan.com/spark-tuning-basic.html?from=timeline 前言 开发调优 调优概述 原则一:避免创建重复的RDD 原则二:尽可能 ...

  3. 【转】Spark性能优化指南——基础篇

    http://mp.weixin.qq.com/s?__biz=MjM5NDMwNjMzNA==&mid=2651805828&idx=1&sn=2f413828d1fdc6a ...

  4. Spark性能优化指南——基础篇(转载)

    前言 在大数据计算领域,Spark已经成为了越来越流行.越来越受欢迎的计算平台之一.Spark的功能涵盖了大数据领域的离线批处理.SQL类处理.流式/实时计算.机器学习.图计算等各种不同类型的计算操作 ...

  5. Spark性能优化指南——基础篇

    本文转自:http://tech.meituan.com/spark-tuning-basic.html 感谢原作者 前言 在大数据计算领域,Spark已经成为了越来越流行.越来越受欢迎的计算平台之一 ...

  6. Spark性能优化指南——高级篇

    本文转载自:https://tech.meituan.com/spark-tuning-pro.html 美团技术点评团队) Spark性能优化指南——高级篇 李雪蕤 ·2016-05-12 14:4 ...

  7. Spark性能优化指南——基础篇转

    前言 在大数据计算领域,Spark已经成为了越来越流行.越来越受欢迎的计算平台之一.Spark的功能涵盖了大数据领域的离线批处理.SQL类处理.流式/实时计算.机器学习.图计算等各种不同类型的计算操作 ...

  8. Java 性能优化手册 — 提高 Java 代码性能的各种技巧

    转载: Java 性能优化手册 - 提高 Java 代码性能的各种技巧 Java 6,7,8 中的 String.intern - 字符串池 这篇文章将要讨论 Java 6 中是如何实现 String ...

  9. Spark性能优化指南--基础篇

    前言 开发调优 调优概述 原则一:避免创建重复的RDD 原则二:尽可能复用同一个RDD 原则三:对多次使用的RDD进行持久化 原则四:尽量避免使用shuffle类算子 原则五:使用map-side预聚 ...

随机推荐

  1. iOS--高级技术

    1.iOS---搜索功能 2.iOS--通讯录.蓝牙.内购.GameCenter.iCloud.Passbook等系统服务开发汇总 3.iOS-技巧性总结 4.iOS-调试技巧 5.iOS-即时通讯- ...

  2. TODO:Github的使用技巧之同步代码

    TODO:Github的使用技巧之同步代码 GitHub 是一个面向开源及私有软件项目的托管平台,因为只支持 Git 作为唯一的版本库格式进行托管,故名 GitHub. GitHub 于 2008 年 ...

  3. 【CSS进阶】伪元素的妙用2 - 多列均匀布局及title属性效果

    最近无论是工作还是自我学习提升都很忙,面对长篇大论的博文总是心有余而力不足,但又不断的接触学习到零碎的但是很有意义的知识点,很想分享给大家,所以本篇可能会很短. 本篇接我另一篇讲述 CSS 伪元素的文 ...

  4. 浅谈系列之 javascript原型与对象

    在我学习与使用javascript三个月中,我一直对javascript的继承关系以及prototype理解不清,导致很多时候为什么这么用说不出个所以然来.截止到本周为止,通过之前的学习以及自己的再学 ...

  5. C# 给PDF文件添加水印

      水印种类及功能介绍 PDF水印分为两种:文本水印和图片水印.文本水印一般被用在商业领域,提醒读者该文档是受版权保护的,其他人不能抄袭或者免费使用.除了这个特征,水印还可以用来标记这个文档 的一些基 ...

  6. 如约而至:微信自用的移动端IM网络层跨平台组件库Mars已正式开源

    1.前言 关于微信内部正在使用的网络层封装库Mars开源的消息,1个多月前就已满天飞(参见<微信Mars:微信内部正在使用的网络层封装库,即将开源>),不过微信团队没有失约,微信Mars ...

  7. 微信小程序(微信应用号)开发ide安装解决方法

    这两天整个技术圈都炸锅了,微信小程序(微信应用号)发布内测,首批200家收到邀请,但是没受邀请的同学,也不用担心,下面介绍一下解决方法. 首先需要下载ide,昨天只需要下载0.9版本的编辑器并替换文件 ...

  8. JSONP的诞生、原理及应用实例

    问题: 页面中有一个按钮,点击之后会更新网页中的一个盒子的内容. Ajax可以很容易的满足这种无须刷新整个页面就可以实现数据变换的需求. 但是,Ajax有一个缺点,就是他不允许跨域请求资源. 如果我的 ...

  9. 利用Python进行数据分析(13) pandas基础: 数据重塑/轴向旋转

    重塑定义     重塑指的是将数据重新排列,也叫轴向旋转. DataFrame提供了两个方法: stack: 将数据的列“旋转”为行. unstack:将数据的行“旋转”为列. 例如: 处理堆叠格式 ...

  10. Google官方关于Android架构中MVP模式的示例续-DataBinding

    基于前面的TODO示例,使用Data Binding库来显示数据并绑定UI元素的响应动作. 这个示例并未严格遵循 Model-View-ViewModel 或 Model-View-Presenter ...