String 类是Java中很是基础和重要的类,提供了构造和管理字符串的基本逻辑,它是典型的 Immutable 类,被声明成为 final class,全部的属性也是 fianl 的。因为它的不可变性,相似拼接、裁剪字符串等动做,都会产生新的 String 对象,所以字符串相关性能的效率对应用性能有明显影响。java
String str = "a"
的形式,使用这种形式建立字符串时, JVM 会在字符串常量池中先检查是否存在该对象,若是存在,返回该对象的引用地址,若是不存在,则在字符串常量池中建立该字符串对象而且返回引用。使用这种方式建立的好处是:避免了相同值的字符串重复建立,节约了内存。正则表达式
String str = new String("a")
的形式,使用这种方式建立字符串对象过程就比较复杂,分红两个阶段,首先在编译时,字符串a
会被加入到常量结构中,类加载时候就会在常量池中建立该字符串。而后就是在调用new()时,JVM 将会调用String
的构造函数,同时引用常量池中的a
字符串, 在堆内存中建立一个String
对象而且返回堆中的引用地址。数组
了解了String
对象两种建立方式,咱们来分析一下下面这段代码,加深咱们对这两种方式的理解,下面这段代码片中,str
是否等于str1
呢?缓存
String str = "a"; String str1 = new String("a"); system.out.println(str==str1) 复制代码
咱们逐一来分析这几行代码,首先从String str = "a"
开始,这里使用了字符串常量的方式建立字符串对象,在建立a
字符串对象时,JVM会去常量池中查找是否存在该字符串,这里的答案确定是没有的,因此JVM将会在常量池中建立该字符串对象而且返回对象的地址引用,因此str
指向的是a
字符串对象在常量池中的地址引用。安全
而后是String str1 = new String("a")
这行代码,这里使用的是构造函数的方式建立字符串对象,根据咱们上面对构造函数方式建立字符串对象的理解,str1
获得的应该是堆中a
字符串的引用地址。因为str
指向的是a
字符串对象在常量池中的地址引用而str1
指向的是堆中a
字符串的引用地址,因此str
确定不等于str1
。app
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { // String类使用final做为一个不可变类,实现了序列化接口、比较接口、字符序列接口 // CharSequence 字符序列接口 经常使用实现类有String StringBuilder StringBuffer public int length() { return value.length;} public char charAt(int index) { //... return value[index]; } public String toString(); // 拆分字符序列,包左不包右 public CharSequence subSequence(int beginIndex, int endIndex) { return this.substring(beginIndex, endIndex); } // replace 和 replaceAll 二者都是能够所有替换 replaceAll 支持正则表达式 public String replace(CharSequence target, CharSequence replacement) { return Pattern.compile(target.toString(), Pattern.LITERAL).matcher( this).replaceAll(Matcher.quoteReplacement(replacement.toString())); } public String replaceAll(String regex, String replacement) { return Pattern.compile(regex).matcher(this).replaceAll(replacement); } // equals是Object的方法,contenEquals能够判断字符序列内容是否相等 public boolean equals(Object anObject) { //... } public boolean contentEquals(CharSequence cs) { //... } // 是否包含指定 字符序列 public boolean contains(CharSequence s) { return indexOf(s.toString()) > -1; } /* * 在调用”ab”.intern()方法的老是会返回常量池中”ab”的引用 * 该方法会首先检查字符串池中是否有”ab”这个字符串,若是存在则返回该字符串的引用, * 不然就将这个字符串添加到常量池中,然会返回该字符串的引用。 */ public native String intern(); // 返回指定索引的字符 public char charAt(int index) { if ((index < 0) || (index >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return value[index]; } //判断字符串中是否包含 指定字符,如包含返回索引,不然返回-1 public int indexOf(String str) { //... } // 将输入值转为字符串 public static String valueOf(int i) { return Integer.toString(i); } // 按regex分割成String数组,limit默认为0,是分红几块 public String[] split(String regex, [int limit]) // 分割字符串包左不包右 public String substring(int beginIndex, [int endIndex]) {} // 向当前字符串对象末尾追加str public String concat(String str) {} // 变成字符数组 public char[] toCharArray() {} // 字符串反转 StringBuilder public AbstractStringBuilder reverse() {}
字符串的分割是字符串操做的经常使用操做之一,对于字符串的分割,大部分人使用的都是 Split() 方法,Split() 方法大多数状况下使用的是正则表达式,这种分割方式自己没有什么问题,可是因为正则表达式的性能是很是不稳定的,使用不恰当会引发回溯问题,极可能致使 CPU 居高不下。在如下两种状况下 Split() 方法不会使用正则表达式:函数
因此咱们在字符串分割时,应该慎重使用 Split() 方法,首先考虑使用 String.indexOf() 方法进行字符串分割,若是 String.indexOf() 没法知足分割要求,再使用 Split() 方法,使用 Split() 方法分割字符串时,须要注意回溯问题。性能
经过对常见应用的堆转储(Dump Heap),分析对象组成,会发现25%的对象是字符串,其中有半数是重复的,若是能避免建立重复的字符串,那么能够有效下降内存消耗和对象建立开销,这即是 字符串常量池 存在的意义。优化
编译期间,编译器就会将代码中用到的全部字符串汇总到一块儿,保存在字节码文件中某个位置,这一部分就是字符串常量池了,以上是静态地生成常量池的过程。在程序运行期间,字符串也能够被动态的添加至池中。ui
String 在 Java 6 之后提供了 intern()方法,目的是提示JVM把对应的字符串缓存起来,以备重复使用。在咱们对字符串对象调用 intern()方法的时候,若是常量池中已经有缓存的字符串,则会返回缓存的实例引用,不然先将其缓存起来,再返回实例引用。这样就能够避免建立重复的对象,帮助咱们节约很多的空间。
Intern 是一种显示的排重机制,但它也有必定的反作用,一个是由于常量池的实现机制,相似于HashTable的实现方式,存储的数据量越大,遍历的时间复杂度就会增长,背离了intern的初衷;另外一个是写起来不方便,并且在开发时,很难统计字符串的重复状况。所以,咱们要结合场景使用。
Oracle JDK 8u20以后,推出了一个新特性,在G1 GC 下的字符串排重,它是经过将相同数据的字符串指向同一份数据来作到的,是JVM底层的改变,不须要Java类库作什么修改。
通常状况下JVM 会将 相似 "abc"这样的文本字符串,或者字符串常量之类的缓存起来。
在Java 6及之前的版本中,HotSpot 虚拟机经过用永久代来实现了JVM中方法区的概念(其余虚拟机实现可没有永久代的概念),这个空间是有限的,基本不会被FullGC之外的垃圾收集照顾到,所以,容易发生OOM。
Java 7中将字符串常量从永久代移出,放置在堆中,这样就极大的避免了永久带占满的问题。
JDK 8中永久代被 MetaSpace (元数据区)代替了,这个区域并不在JVM中,而是属于本地内存,所以能够随着物理机的内存增加而增加。元空间存储类的元信息,静态变量和常量池等并入堆中。
字符串的拼接是对字符串操做使用最频繁的操做之一,所以编译器会对其进行优化。
// 字面量(文本字符串)直接拼接 public void test0() { String str = "111" + "222"; } // 反编译后: public void test0(); Code: 0: ldc #2 // String 111222 将int,float或String型常量值从常量池中推送至栈顶 2: astore_1 3: return
前面提到JVM会将相似 “abc” 这样的文本字符串缓存起来,代码中 “111”+“222”,编译器能够直接判断出结果是 “111222”,因此会将 “111222” 这个字符字面量存放到常量池中,还须要注意的是,常量池中不会缓存 “111” 和 “222”。
public void test1() { final String str1 = "111"; final String str2 = "222"; String str = str1 + str2; }
字符串常量也会被JVM在编译期缓存起来,上面的代码在反编译后会和 test0()的反编译结果相同。
// 字符串变量的拼接 public void test1() { String str1 = "111"; String str2 = "222"; String str = str1 + str2; } public void test1(); Code: 0: ldc #3 // String 111 2: astore_1 3: ldc #4 // String 222 5: astore_2 6: new #5 // class java/lang/StringBuilder 9: dup 10: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 13: aload_1 14: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: aload_2 18: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: astore_3 25: return
在JDK 8中,使用 “ + ” 进行的字符串变量拼接操做会自动被 Java编译器转换为 StringBuilder 操做,下面的代码进行编译后会生成和上面同样的字节码指令,但写法比较繁琐。
public void test2() { String str1 = "111"; String str2 = "222"; String str=new StringBuilder().append(str1).append(str2).toString(); }
因为这个特性的存在,咱们要尽可能不要在循环中使用字符串拼接,由于这种作法会致使建立大量的StringBuilder对象,从而容易引发OOM。
String str = "str"; for(int i=0; i<1000; i++) { str = str + i; } //编译器优化后 String str = "str"; for(int i=0; i<1000; i++) { str = (new StringBuilder(String.valueOf(str))).append(i).toString(); }
因此咱们在作字符串拼接时,若是不涉及到线程安全的状况下,咱们显示的使用 StringBuilder 进行拼接,提高系统性能,若是涉及到线程安全的话,可使用 StringBuffer 来进行字符串拼接。
StringBuilder str = new StringBuilder("str"); for (int i = 0; i < 1000; i++) { str = str.append(i); }
在JDK 9中为了更加统一字符串的操做优化,提供了 StringConcatFactory。
Java6 以及之前版本
String 对象是对 char 数组进行了封装实现的对象,主要有四个成员变量: char 数组、偏移量 offset、字符数量 count、哈希值 hash。
String 对象是经过 offset 和 count 两个属性来定位 char[] 数组,获取字符串。这么作能够高效、快速地共享数组对象,同时节省内存空间,但这种方式颇有可能会致使内存泄漏。
Java7 版本开始到 Java8 版本
从 Java7 版本开始,Java 对 String 类作了一些改变。String 类中再也不有 offset 和 count 两个变量了。这样的好处是String 占用的内存稍微少了些,同时 String.substring 方法也再也不共享 char[],从而解决了使用该方法可能致使的内存泄漏问题。
Java9 版本开始
将 char[] 数组改成了 byte[] 数组,并增长一个标识编码的属性 coder。coder 属性默认有 0 和 1 两个值, 0 表明Latin-1(单字节编码),1 表明 UTF-16 编码。在计算字符串长度或者调用 indexOf() 方法时,会用到这个属性。
Java中char 是两个字节大小,但咱们平时使用的26个字母和数字均可以用一个byte来表示,只有在存中文字符的时候才会用到两个字节,所以为了节约空间,将字符串的实现由以前的两个字节,改成一个字节,咱们能够明显感觉到紧凑字符串带来的优点,即更小的内存占用,更快的传输和操做速度。
StringBuffer 是为了解决上面提到的拼接产生太多中间对象的问题而提供的一个类,它是Java1.5中新增的,咱们能够用appened 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置,StringBuffer 本质是一个线程安全的可修改字符序列,初始容量为16,它在保证线程安全的同时也带来了额外的性能开销,除非有线程安全的须要,否则仍是推荐使用StringBuilder,这二者都继承自 AbstractStringBuilder,在重写父类方法时,只是简单的调用了父类对应方法,区别在于StringBuffer 会在方法上加上 synchronized 来保证线程的安全。
StirngBuileder动态扩容
构建时初始的字符数组长度为16,每次在拼接字符串的时候,都会修改底层的字符数组,不过在正式修改以前,先会肯定当前字符数组的容量是否足够,不够的话会进行扩容,扩容以后还要进行数组拷贝。这一过程会产生多重开销,所以若是咱们能肯定拼接会发生很是屡次且容量大体可预计,那么就能够指定合适的初始大小,避免屡次扩容带来的性能开销。
Reference
《Java核心技术36讲》