Java 性能优化之String 篇

欢迎进入Java社区论坛,与200万技术人员互动交流 >>进入

String 在 JVM 的存储结构

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

?对象头(object header):8 个字节

?Java 原始类型数据:如 int, float, char 等类型的数据,各类型数据占内存如 表 1. Java 各数据类型所占内存.

?引用(reference):4 个字节

?填充符(padding)

表 1. Java 各数据类型所占内存

然而,一个 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()设计恰好可以共享原始文本从而达到节省内存的目的.

[1][2]

往往教导我们大家要好好学习天天向上,

Java 性能优化之String 篇

相关文章:

你感兴趣的文章:

标签云: