作为一名Java程序员,我们对String简直是再熟悉不过了。String类的使用的频率可谓相当高。它是Java语言中的核心类,在java.lang包下,主要用于字符串的比较、查找、拼接等等操作。那么它的底层究竟是怎么实现的呢,随着JDK的不断更新,String 又有哪些优化呢?跟随这篇文章一起走进它的“内心”,解开它神秘的面纱。
String的基本特性
想要深入了解一个类,就得从它的源码讲起:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
//
...
}
--取自JDK1.8
从上面的源码至少可以看出以下几点:
- String 类被final关键字修饰,表示String类不能被继承。
- String 实现了 Comparable 接口,表示 String 可以比较大小
- String 类的是通过 char[] 数组进行存储的,并且 char[] 数组是 final 修饰,表示它的值一旦创建就不能被修改
String 的最大特点就是它的
不可变性: - 当对字符串进行重新赋值时,需要重新指定内存区域赋值,不会修改原有的 value 值。
- 当对现有的字符串进行拼接时,同样也需要重新指定内存赋值,不会修改原有的 value 值。
- 使用 String 类的 replace() 方法修改指定字符串时,也需要重新指定内存空间赋值,不会修改原有的 value 值。
一句话说就是:字符串一旦在内存中存在,就不会被改变!
String 的存储结构变更
打从一开始 String 内部都是使用 char 型数组来存储表示一个字符串,其实这也很好理解,一个字符串当然是由多个字符组成的。但是等到了 JDK1.9 之后,这种情况发生了改变:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
@Stable
private final byte[] value;
private final byte coder;
private int hash;
...
}
--取自JDK1.9
从 JDK1.9 起,字符串开始采用 byte[] 字节数组存储值,并且增加了一个成员变量 coder。与此同时,与 String 相关的 StringBuilder、StringBuffer 等也进行了相应的调整。来看一下官方的解释:
Motivation
The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.
动机
string类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁-1字符。这样的字符只需要一个字节的存储空间,因此这样的字符串对象的内部字符数组中的一半空间将被闲置。
在程序中,绝大多数字符串只包含英文字母数字等字符,使用 Latin-1 编码,一个字符占用一个 byte。那么采用 byte 在一定程度上能够减少字符串对内存的占用。
如果字符串中使用了中文等超出Latin-1表示范围的字符,使用Latin-1就没办法表示了。这时JDK会使用UTF-16编码,那么占用的空间和旧版(使用char[])是一样的。新增的成员变量 coder 就是用来标记当前字符串是使用 Latin-1 编码还是 UTF16 编码。
String 的内存分配
在 JDK1.7 之前 String Pool(字符串常量池)存储在永久代,1.7 开始字符串常量池放到堆中存储。JDK1.8 移除了永久代,使用元空间来实现方法区,也没有再对字符串常量池的存储位置做任何的调整。将字符串常量池移到堆中存储也是为了能够进行GC,及时将不再使用的字符串清理。这其中涉及到的过程还挺复杂的,这里就不做过多描述。
下面来聊聊字符串常量池
字符串常量池
池化思想其实在Java中是非常常见的,常量池就类似一个 Java 系统提供的缓存,当内存中有相同内容就不会再去创建,而是使用已有的内容,这样也能更好的节省内存空间。Java 虚拟机中维护了运行时常量池以及字符串常量池。
字符串常量池就是一个固定大小的HashTable,HashTable 是采用数组+链表的形式存储对象,在存储对象之前会进行 hash 算法取 hash 值存入表中,这也保证了字符串常量池中不会存储相同内容的字符串。使用 -XX:StringTableSize 可以设置常量池的大小,这个值再JDK1.7 之后默认为60013。哪些情况下会将字符串存储到常量池中呢?
- 使用双引号直接声明的String对象会直接存储到常量池中
String s = "Hello";
- new String 对象的同时也会在常量池中维护一个该对象
String s = new String("Hello");
这就表示 new 一个 String 对象的同时创建了两个对象(前提是在此之前没有声明过该字符串),一个在堆中,一个在字符串常量池中。
- 调用 String 的 intern() 方法
/**
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
*/
public native String intern();
--取自JDK9
用人话讲就是调用 intern() 方法时,如果池已包含等于确定的String对象的字符串,则返回池中的字符串的引用。否则,将此String对象添加到池中,并返回对此对象的引用。所有字符串字面量和字符串值常量表达式都是实体。
字符串的拼接操作
我们都喜欢使用 "+" 去拼接两个字符串,但使用 "+" 去拼接字符串无形中又造了很多的 String 对象,这无疑会占用一部分内存。因此所有人都会推荐我们使用 StringBuilder 或 StringBuffer 拼接字符串。但是明明一个简单的 "+" 就可以解决的事情却需要去造另外一个对象,显然很多人都不愿意这么做。事实上 Java 官方开发人员一直在对 "+" 优化,努力减少底层对内存的消耗。那么一个简单的 "+" 又发生了那些事呢?
public class Demo {
public static void main(String[] args) {
String s1 = "Hello"
String s2 = "world";
String s3 = s1 + s2;
System.out.println(s3);
}
}
对这样一段简单的代码进行反编译得到它的字节码文件
我们发现在Java底层是创建了一个StringBuilder对象调用它的append()方法组成一个新的字符串,并调用StringBuilder 的 toString 方法创建这个字符串,我们知道 StringBuilder 的 toString 方法其实就是 new 了一个 String 对象,但此时的对象不会放入常量池。想不到一个简单的+需要这么多的步骤!这就是为什么我们要少用 + 的原因。
其实在 JDK9 之后对这个操作进行了优化,底层不再使用 StringBuilder 来拼接两个字符串,而是使用了
StringConcatFactory的makeConcatWithConstants()方法动态的拼接字符串。
Code:
stack=2, locals=4, args_size=1
0: ldc #2 // String Hello
2: astore_1
3: ldc #3 // String world
5: astore_2
6: aload_1
7: aload_2
8: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
13: astore_3
14: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
17: aload_3
18: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
这样在一定程度上提高了+的效率。但即使这样。当遇到大量的字符串拼接+操作时,使用StringBuilder/StringBuffer 的 append() 方法,后者的效率是远远优于前者的。当然 Java 也在不断的更新、优化、增强,也许在不久的将来+的操作可以与 append 媲美。
今天的分享就到这里啦,感谢阅读!