Java中字符串对象建立有两种形式,一种为字面量形式,如String str = "droid";
,另外一种就是使用new这种标准的构造对象的方法,如String str = new String("droid");
,这两种方式咱们在代码编写时都常用,尤为是字面量的方式。然而这两种实现其实存在着一些性能和内存占用的差异。这一切都是源于JVM为了减小字符串对象的重复建立,其维护了一个特殊的内存,这段内存被成为字符串常量池或者字符串字面量池。html
工做原理
当代码中出现字面量形式建立字符串对象时,JVM首先会对这个字面量进行检查,若是字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,不然新的字符串对象被建立,而后将这个引用放入字符串常量池,并返回该引用。java
举例说明
字面量建立形式
1
|
String str1 = "droid"; |
JVM检测这个字面量,这里咱们认为没有内容为droid
的对象存在。JVM经过字符串常量池查找不到内容为droid
的字符串对象存在,那么会建立这个字符串对象,而后将刚建立的对象的引用放入到字符串常量池中,而且将引用返回给变量str1。android
若是接下来有这样一段代码安全
1
|
String str2 = "droid"; |
一样JVM仍是要检测这个字面量,JVM经过查找字符串常量池,发现内容为”droid”字符串对象存在,因而将已经存在的字符串对象的引用返回给变量str2。注意这里不会从新建立新的字符串对象。bash
验证是否为str1和str2是否指向同一对象,咱们能够经过这段代码app
1
|
System.out.println(str1 == str2); |
结果为true
。ide
使用new建立
1
|
String str3 = new String("droid"); |
当咱们使用了new来构造字符串对象的时候,无论字符串常量池中有没有相同内容的对象的引用,新的字符串对象都会建立。所以咱们使用下面代码测试一下,性能
1
2 |
String str3 = new String("droid"); System.out.println(str1 == str3); |
结果如咱们所想,为false
,代表这两个变量指向的为不一样的对象。测试
intern
对于上面使用new建立的字符串对象,若是想将这个对象的引用加入到字符串常量池,可使用intern方法。优化
调用intern后,首先检查字符串常量池中是否有该对象的引用,若是存在,则将这个引用返回给变量,不然将引用加入并返回给变量。
1
2 |
String str4 = str3.intern(); System.out.println(str4 == str1); |
输出的结果为true
。
疑难问题
前提条件?
字符串常量池实现的前提条件就是Java中String对象是不可变的,这样能够安全保证多个变量共享同一个对象。若是Java中的String对象可变的话,一个引用操做改变了对象的值,那么其余的变量也会受到影响,显然这样是不合理的。
引用 or 对象
字符串常量池中存放的时引用仍是对象,这个问题是最多见的。字符串常量池存放的是对象引用,不是对象。在Java中,对象都建立在堆内存中。
更新验证,收到的不少评论也在讨论这个问题,我简单的进行了验证。 验证环境
1
2 3 4 5 6 7 8 9 10 11 12 13 |
22:18:54-androidyue~/Videos$ cat /etc/os-release NAME=Fedora VERSION="17 (Beefy Miracle)" ID=fedora VERSION_ID=17 PRETTY_NAME="Fedora 17 (Beefy Miracle)" ANSI_COLOR="0;34" CPE_NAME="cpe:/o:fedoraproject:fedora:17" 22:19:04-androidyue~/Videos$ java -version java version "1.7.0_25" OpenJDK Runtime Environment (fedora-2.3.12.1.fc17-x86_64) OpenJDK 64-Bit Server VM (build 23.7-b01, mixed mode) |
验证思路:如下的Java程序读取一个大小为82M的视频文件,以字符串形式进行intern操做。
1
2 |
22:01:17-androidyue~/Videos$ ll -lh | grep why_to_learn.mp4 -rw-rw-r--. 1 androidyue androidyue 82M Oct 20 2013 why_to_learn.mp4 |
验证代码
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class TestMain { private static String fileContent; public static void main(String[] args) { fileContent = readFileToString(args[0]); if (null != fileContent) { fileContent = fileContent.intern(); System.out.println("Not Null"); } } private static String readFileToString(String file) { BufferedReader reader = null; try { reader = new BufferedReader(new FileReader(file)); StringBuffer buff = new StringBuffer(); String line; while ((line = reader.readLine()) != null) { buff.append(line); } return buff.toString(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (null != reader) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } } |
因为字符串常量池存在于堆内存中的永久代,适用于Java8以前。咱们经过设置永久代一个很小的值来进行验证。若是字符串对象存在字符串常量池中,那么必然抛出java.lang.OutOfMemoryError permgen space
错误。
1
|
java -XX:PermSize=6m TestMain ~/Videos/why_to_learn.mp4 |
运行证实程序没有抛出OOM,其实这个不能很好的证实存储的是对象仍是引用。
可是这个至少证实了字符串的实际内容对象char[]不存放在字符串常量池中。既然这样的话,其实字符串常量池存储字符串对象仍是字符串对象的引用反而不是那么重要。但我的仍是倾向于存储的为引用。
优缺点
字符串常量池的好处就是减小相同内容字符串的建立,节省内存空间。
若是硬要说弊端的话,就是牺牲了CPU计算时间来换空间。CPU计算时间主要用于在字符串常量池中查找是否有内容相同对象的引用。不过其内部实现为HashTable,因此计算成本较低。
GC回收?
由于字符串常量池中持有了共享的字符串对象的引用,这就是说是否是会致使这些对象没法回收?
首先问题中共享的对象通常状况下都比较小。据我查证了解,在早期的版本中确实存在这样的问题,可是随着弱引用的引入,目前这个问题应该没有了。
关于这个问题,能够具体了解这片文章interned Strings : Java Glossary
intern使用?
关于使用intern的前提就是你清楚本身确实须要使用。好比,咱们这里有一份上百万的记录,其中记录的某个值屡次为美国加利福尼亚州,咱们不想建立上百万条这样的字符串对象,咱们可使用intern只在内存中保留一份便可。关于intern更深刻的了解请参考深刻解析String#intern。
总有例外?
你知道下面的代码,会建立几个字符串对象,在字符串常量池中保存几个引用么?
1
|
String test = "a" + "b" + "c"; |
答案是只建立了一个对象,在常量池中也只保存一个引用。咱们使用javap反编译看一下便可得知。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
17:02 $ javap -c TestInternedPoolGC Compiled from "TestInternedPoolGC.java" public class TestInternedPoolGC extends java.lang.Object{ public TestInternedPoolGC(); Code: 0: aload_0 1: invokespecial #1; //Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]) throws java.lang.Exception; Code: 0: ldc #2; //String abc 2: astore_1 3: return |
看到了么,实际上在编译期间,已经将这三个字面量合成了一个。这样作其实是一种优化,避免了建立多余的字符串对象,也没有发生字符串拼接问题。关于字符串拼接,能够查看Java细节:字符串的拼接。