String 方法用于文本分析及大量字符串处理时会对内存性能造成一些影响。可能导致内存占用太大甚至OOM。
一、先介绍一下String对象的内存占用
一般而言,Java 对象在虚拟机的结构如下:
•对象头(object header):8 个字节(保存对象的 class 信息、ID、在虚拟机中的状态)
•Java 原始类型数据:如 int, float, char 等类型的数据
•引用(reference):4 个字节
•填充符(padding)
String定义:
JDK6:
private final char value[];
private final int offset;
private final int count;
private int hash;
JDK6的空字符串所占的空间为40字节
JDK7:
private final char value[];
private int hash;
private transient int hash32;
JDK7的空字符串所占的空间也是40字节
JDK6字符串内存占用的计算方式:
首先计算一个空的 char 数组所占空间,在 Java 里数组也是对象,因而数组也有对象头,故一个数组所占的空间为对象头所占的空间加上数组长度,即 8 + 4 = 12 字节 , 经过填充后为 16 字节。
那么一个空 String 所占空间为:
对象头(8 字节)+ char 数组(16 字节)+ 3 个 int(3 × 4 = 12 字节)+1 个 char 数组的引用 (4 字节 ) = 40 字节。
因此一个实际的 String 所占空间的计算公式如下:
8*( ( 8+12+2*n+4+12)+7 ) / 8 = 8*(int) ( ( ( (n) *2 )+43) /8 )
其中,n 为字符串长度。
二、举个例子:
1、substring
package demo;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
public class TestBigString
{
private String strsub;
private String strempty = new String();
public static void main(String[] args) throws Exception
{
TestBigString obj = new TestBigString();
obj.strsub = obj.readString().substring(0,1);
Thread.sleep(30*60*1000);
}
private String readString() throws Exception
{
BufferedReader bis = null;
try
{
bis = new BufferedReader(new InputStreamReader(new FileInputStream(new File("d:\\teststring.txt"))));
StringBuilder sb = new StringBuilder();
String line = null;
while((line = bis.readLine()) != null)
{
sb.append(line);
}
System.out.println(sb.length());
return sb.toString();
}
finally
{
if (bis != null)
{
bis.close();
}
}
}
}
其中文件"d:\\teststring.txt"里面有33475740个字符,文件大小有35M。
用JDK6来运行上面的代码,可以看到strsub只是substring(0,1)只取一个,count确实只有1,但其占用的内存却高达接近67M。
然而用JDK7运行同样的上面的代码,strsub对象却只有40字节
什么原因呢?
来看下JDK的源码:
JDK6:
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);
}
// 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;
}
JDK7:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
可以看到原来是因为JDK6的String.substring()所返回的 String 仍然会保存原始 String的引用,所以原始String无法被释放掉,因而导致了出乎意料的大量的内存消耗。
JDK6这样设计的目的其实也是为了节约内存,因为这些 String 都复用了原始 String,只是通过 int 类型的 offerset, count 等值来标识substring后的新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());
2、同样,再看看split方法
public class TestBigString
{
private String strsub;
private String strempty = new String();
private String[] strSplit;
public static void main(String[] args) throws Exception
{
TestBigString obj = new TestBigString();
obj.strsub = obj.readString().substring(0,1);
obj.strSplit = obj.readString().split("Address:",5);
Thread.sleep(30*60*1000);
}
JDK6中分割的字符串数组中,每个String元素占用的内存都是原始字符串的内存大小(67M):
而JDK7中分割的字符串数组中,每个String元素都是实际的内存大小:
原因:
JDK6源代码:
public String[] split(String regex, int limit) {
return Pattern.compile(regex).split(this, limit);
}
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);
public CharSequence subSequence(int beginIndex, int endIndex) {
return this.substring(beginIndex, endIndex);
}
三、其他方面:
1、String a1 = “Hello”; //常量字符串,JVM默认都已经intern到常量池了。
创建字符串时 JVM 会查看内部的缓存池是否已有相同的字符串存在:如果有,则不再使用构造函数构造一个新的字符串,
直接返回已有的字符串实例;若不存在,则分配新的内存给新创建的字符串。
String a2 = new String(“Hello”); //每次都创建全新的字符串
2、在拼接静态字符串时,尽量用 +,因为通常编译器会对此做优化。
public String constractStr()
{
return "str1" + "str2" + "str3";
}
对应的字节码:
Code:
0: ldc #24; //String str1str2str3 --将字符串常量压入栈顶
2: areturn
3、在拼接动态字符串时,尽量用 StringBuffer 或 StringBuilder的 append,这样可以减少构造过多的临时 String 对象(javac编译器会对String连接做自动优化):
public String constractStr(String str1, String str2, String str3)
{
return str1 + str2 + str3;
}
对应字节码(JDK1.5之后转换为调用StringBuilder.append方法):
Code:
0: new #24; //class java/lang/StringBuilder
3: dup
4: aload_1
5: invokestatic #26; //Method java/lang/String.valueOf:(Ljava/lang/Objec
t;)Ljava/lang/String;
8: invokespecial #32; //Method java/lang/StringBuilder."<init>":(Ljava/la
ng/String;)V
11: aload_2
12: invokevirtual #35; //Method java/lang/StringBuilder.append:(Ljava/lang
/String;)Ljava/lang/StringBuilder;
15: aload_3
16: invokevirtual #35; //Method java/lang/StringBuilder.append:(Ljava/lang
/String;)Ljava/lang/StringBuilder; ――调用StringBuilder的append方法
19: invokevirtual #39; //Method java/lang/StringBuilder.toString:()Ljava/l
ang/String;
22: areturn ――返回引用
分享到:
相关推荐
标题和描述均强调了在Java中处理`String`对象时可能遇到的陷阱,尤其是在理解和使用`String`的处理机制上。文章由天津工业大学软件工程专业的翁龙辉撰写,旨在深入剖析`String`在Java中的独特行为及其潜在的陷阱。...
Java语言在实际应用中充满了各种陷阱,这些陷阱可能在编程过程中导致意料之外的问题,对程序的稳定性和性能造成影响。对于求职者来说,熟悉这些陷阱并在面试中能够准确解答,是展示自身技能水平的重要方式。本文将...
综上所述,Java基础方面的陷阱涵盖了语法理解、数据类型使用、运算符和流程控制的误用、异常处理的不当、内存管理的误区,以及面向对象设计原则的应用等多个方面。通过深入学习和实践,可以有效地避免这些陷阱,提升...
- **GC(Garbage Collection)**:Java中的垃圾回收机制自动管理内存,当对象不再被引用时,它们会被垃圾回收器回收。 - 可以通过调用`System.gc()`或`Runtime.getRuntime().gc()`来请求垃圾回收,但这并不保证...
### Java核心基础+Java中的数据在内存中的存储 #### 一、内存中的堆与栈 在探讨Java中数据如何在内存中存储之前,我们需要先理解...了解不同数据类型如何存储可以帮助开发者更好地管理内存资源,避免常见的编程陷阱。
2. **内存管理**:Java使用垃圾收集器自动回收不再使用的对象,但过度依赖垃圾收集可能导致内存泄漏。理解引用类型(强、软、弱、虚)和`finalize()`方法的使用是避免问题的关键。 3. **异常处理**:Java强制要求...
### Java程序员面试陷阱大全 在Java编程领域,面试不仅是对技术知识的考验,更是对细节把握和陷阱识别能力的一次挑战。以下是从标题、描述、部分文件内容中提炼出的关键知识点,旨在帮助Java程序员深入理解并有效...
从给定的文件标题“Java面试中的陷阱”和描述“Java面试中一些常用问题的小总结”,我们可以提炼出一系列重要的Java知识点,这些知识点是面试官在评估候选人时常常关注的领域。下面将对这些知识点进行详细解析,以...
在Java面试中,面试官可能会提出一系列陷阱问题来测试候选人的理解深度和广度。以下是对这些陷阱问题的详细解释: 1. **final, finally, finalize**:final用于声明不可变的类、变量或方法;finally是异常处理中的...
### Java基础复习笔记03:我们不会注意的陷阱 #### 1. 虚拟机对字符串的处理 在Java中,字符串是一个非常重要的概念,它由`java.lang.String`类表示。对于字符串的处理,Java虚拟机(JVM)有一个特殊的优化机制,即*...
Java语言在设计上有着丰富的特性和机制,但同时也存在一些陷阱和细节问题,这使得面试时经常出现一些挑战性的题目。下面将详细解释标题和描述中提到的一些关键知识点。 1. **final, finally, finalize的区别**: -...
### String 比较 在Java中,`String` 是一个非常重要的类,它用于表示文本字符串。与其他语言不同的是,Java中的 `...通过了解这些基本概念和最佳实践,开发者可以避免常见的陷阱,并写出更健壮、更有效的 Java 代码。
8. **GC (Garbage Collector)**:Java 的垃圾回收机制,自动回收不再使用的对象,以避免内存泄漏。 9. **String s = new String("xyz")**:创建了两个 String 对象,一个在常量池,一个在堆。 10. **Math.round**...
Java编程语言中包含了许多陷阱和细节,这些都是面试官在寻找潜在候选人时经常提出的知识点。以下是一些关于Java面试题的详细解释: 1. **final, finally, finalize的区别**: - `final` 用于声明不可变变量、表示...
了解并掌握这些陷阱可以帮助开发者写出更高效、更健壮的Java代码。在编写涉及大量装箱和拆箱操作的代码时,需要特别注意潜在的性能和并发问题,选择合适的数据结构和算法,以避免不必要的对象创建和内存浪费。同时,...
第八,GC(Garbage Collector)是Java的内存管理机制,负责自动回收不再使用的对象所占用的内存。有了GC,程序员无需手动管理内存,降低了内存泄漏的风险。 第九,`String s = new String("xyz");`创建了两个String...