String类是在做Java开发时最常用的一个类,因此在JDK6,7/8,9中分别对String类进行了优化。本文将结合源码介绍JDK6,7/8,9中,String类分别做了哪些优化,以及String类在创建对象时分别会在哪创建出对象。
一、String类在JDK6,7/8,9中的实现
1. JDK6中的String类
源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
{
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
...
}
构造一个String对象所对应的内存结构
想象一种场景,假设我执行了
String B = A.substring(2, 4);
String A = null;
根据substring
以及String构造函数
的源码
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(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
可知此时的内存结构会变成这样
对象A的内存空间被回收了,对象B指向这个字符数组,但对象B只使用了其中的c d
两个字符,其余的空间没用但又无法回收,也就造成了内存泄露。因此在JDK7中对String类做了优化
2. JDK7/8中的String类
JDK7中对String类做了优化,一直延续到JDK8,先看源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...
}
可以看到offset和count
没有了,也就没有了内存泄露的问题。当然JDK6和JDK7中的String实现方式,在理论上来说,是各有利弊的。加上offset和count(JDK6)
可以在substring
时共用同一个数组,节省内存,但是会造成内存泄露;而不加offset和count(JDK7/8)
在substring
时无法共享数组,多占了空间,但同时也避免了内存泄露。
3. JDK9中的String类
Java默认使用的是Unicode
编码,所以在String中使用的是char数组
,其中每个字符占两个字节,如果开发人员使用的是纯英文,使用ASCII编码即可,那么每个字符用一个字节即可,这种情况下使用char数组
会浪费50%
的内存空间,因此在JDK9中又进行了优化,先看源码
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final byte[] value;
/**
* The identifier of the encoding used to encode the bytes in
* {@code value}. The supported values in this implementation are
*
* LATIN1
* UTF16
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*/
private final byte coder;
可以看到value
的类型变成了byte[]
,如果字符串是纯拉丁文的话,coder
值为0,此时字节数组中一个字节代表字符串中的一个字符,如果不是纯拉丁文的话,coder
值为1,此时字节数组中两个字节代表字符串中第一个字符。
二、字符串分别会在哪些位置创建对象
String A = "abc"、String A = new String("abc")、String A = new String("abc").intern()是比较常见的创建对象的方式,那么在Java代码编译运行时,这些语句分别会对应什么操作呢?
这里先设置两个前提,以方便后续的讨论:
- 字符串实现以JDK7/8为准
- 不同版本的字符串常量池可能会处于不同的Java内存区,为了讨论方便,以下会使用堆、栈和字符串常量池等运行时内存区中的概念,但不会讨论运行时常量池处理方法区还是堆区
1. String A = "abc"
通过以下jclasslib
查看字节码文件,发现"abc"
在编译时会被编译到字节码文件的常量池中
在代码运行时,class文件会被加载进内存,那么"abc"
这个String对象会在字符串常量池中创建,如图所示
在执行String A = "abc"
时,会将字符串常量池中的对象地址返回给变量A
,于是内存结构如图所示
2. String A = new String("abc")
在这种情况下,编译和类加载步骤和上一种情况一样,但是执行步骤会有不同。在执行该语句时,会在堆中创建出对象。先看构造函数源码
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
所以,执行new String("abc")
时会在堆中再创建一个对象,但是共用常量池中的字符数组,内存结构如下:
3. String A = new String("abc").intern()
对于这种情况,编译、类加载以及执行new String("abc")
的步骤和第二种情况一样,为了便于理解,先将改语句分为两个步骤String B = new String("abc"); String A = B.intern();
对于语句一执行后,内存结构如下:
对于语句二,B.intern()
方法会去字符串常量池中找到和字符串B equals
的String对象,并将找到的String对象的地址返回,所以语句二执行之后,内存结构如下:
现在,回到语句String A = new String("abc").intern()
,该语句执行后的内存结构如下所示:
地址为0x3456
的对象在接下来的内存回收中会被回收
三、总结
以上是参考网上各类博客,结合源码以及个人理解总结而成的,如果有误,欢迎指正。