在 JAVA 语言中有8中基本类型和一种比较特殊的类型String
。这些类型为了使他们在运行过程当中速度更快,更节省内存,都提供了一种常量池的概念。常量池就相似一个JAVA系统级别提供的缓存。java
8种基本类型的常量池都是系统协调的,String
类型的常量池比较特殊。它的主要使用方法有两种:c++
直接使用双引号声明出来的String
对象会直接存储在常量池中。程序员
若是不是用双引号声明的String
对象,可使用String
提供的intern
方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中express
接下来咱们主要来谈一下String#intern
方法。缓存
首先深刻看一下它的实现原理。oracle
/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class <code>String</code>. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this <code>String</code> object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this <code>String</code> object is added to the * pool and a reference to this <code>String</code> object is returned. * <p> * It follows that for any two strings <code>s</code> and <code>t</code>, * <code>s.intern() == t.intern()</code> is <code>true</code> * if and only if <code>s.equals(t)</code> is <code>true</code>. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern();
String#intern
方法中看到,这个方法是一个 native 的方法,但注释写的很是明了。“若是常量池中存在当前字符串, 就会直接返回当前字符串. 若是常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。app
在 jdk7后,oracle 接管了 JAVA 的源码后就不对外开放了,根据 jdk 的主要开发人员声明 openJdk7 和 jdk7 使用的是同一分主代码,只是分支代码会有些许的变更。因此能够直接跟踪 openJdk7 的源码来探究 intern 的实现。jvm
####native实现代码:
\openjdk7\jdk\src\share\native\java\lang\String.coop
Java_java_lang_String_intern(JNIEnv *env, jobject this) { return JVM_InternString(env, this); }
\openjdk7\hotspot\src\share\vm\prims\jvm.h性能
/* * java.lang.String */ JNIEXPORT jstring JNICALL JVM_InternString(JNIEnv *env, jstring str);
\openjdk7\hotspot\src\share\vm\prims\jvm.cpp
// String support /////////////////////////////////////////////////////////////////////////// JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str)) JVMWrapper("JVM_InternString"); JvmtiVMObjectAllocEventCollector oam; if (str == NULL) return NULL; oop string = JNIHandles::resolve_non_null(str); oop result = StringTable::intern(string, CHECK_NULL); return (jstring) JNIHandles::make_local(env, result); JVM_END
\openjdk7\hotspot\src\share\vm\classfile\symbolTable.cpp
oop StringTable::intern(Handle string_or_null, jchar* name, int len, TRAPS) { unsigned int hashValue = java_lang_String::hash_string(name, len); int index = the_table()->hash_to_index(hashValue); oop string = the_table()->lookup(index, name, len, hashValue); // Found if (string != NULL) return string; // Otherwise, add to symbol to table return the_table()->basic_add(index, string_or_null, name, len, hashValue, CHECK_NULL); }
\openjdk7\hotspot\src\share\vm\classfile\symbolTable.cpp
oop StringTable::lookup(int index, jchar* name, int len, unsigned int hash) { for (HashtableEntry<oop>* l = bucket(index); l != NULL; l = l->next()) { if (l->hash() == hash) { if (java_lang_String::equals(l->literal(), name, len)) { return l->literal(); } } } return NULL; }
它的大致实现结构就是:
JAVA 使用 jni 调用c++实现的StringTable
的intern
方法, StringTable
的intern
方法跟Java中的HashMap
的实现是差很少的, 只是不能自动扩容。默认大小是1009。
要注意的是,String的String Pool是一个固定大小的Hashtable
,默认值大小长度是1009,若是放进String Pool的String很是多,就会形成Hash冲突严重,从而致使链表会很长,而链表长了后直接会形成的影响就是当调用String.intern
时性能会大幅降低(由于要一个一个找)。
在 jdk6中StringTable
是固定的,就是1009的长度,因此若是常量池中的字符串过多就会致使效率降低很快。在jdk7中,StringTable
的长度能够经过一个参数指定:
-XX:StringTableSize=99991
相信不少 JAVA 程序员都作作相似 String s = new String("abc")
这个语句建立了几个对象的题目。 这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是建立了2个对象,第一个对象是"abc"字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。
来看一段代码:
public static void main(String[] args) { String s = new String("1"); s.intern(); String s2 = "1"; System.out.println(s == s2); String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); }
打印结果是
jdk6 下false false
jdk7 下false true
具体为何稍后再解释,而后将s3.intern();
语句下调一行,放到String s4 = "11";
后面。将s.intern();
放到String s2 = "1";
后面。是什么结果呢
public static void main(String[] args) { String s = new String("1"); String s2 = "1"; s.intern(); System.out.println(s == s2); String s3 = new String("1") + new String("1"); String s4 = "11"; s3.intern(); System.out.println(s3 == s4); }
打印结果为:
jdk6 下false false
jdk7 下false false
####1,jdk6中的解释
注:图中绿色线条表明 string 对象的内容指向。 黑色线条表明地址指向。
如上图所示。首先说一下 jdk6中的状况,在 jdk6中上述的全部打印都是 false 的,由于 jdk6中的常量池是放在 Perm 区中的,Perm 区和正常的 JAVA Heap 区域是彻底分开的。上面说过若是是使用引号声明的字符串都是会直接在字符串常量池中生成,而 new 出来的 String 对象是放在 JAVA Heap 区域。因此拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较确定是不相同的,即便调用String.intern
方法也是没有任何关系的。
####2,jdk7中的解释
再说说 jdk7 中的状况。这里要明确一点的是,在 Jdk6 以及之前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片断等内容,默认大小只有4m,一旦常量池中大量使用 intern 是会直接产生java.lang.OutOfMemoryError: PermGen space
错误的。 因此在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为何要移动,Perm 区域过小是一个主要缘由,固然据消息称 jdk8 已经直接取消了 Perm 区域,而新创建了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合如今 JAVA 的发展了。
正式由于字符串常量池移动到 JAVA Heap 区域后,再来解释为何会有上述的打印结果。
在第一段代码中,先看 s3和s4字符串。String s3 = new String("1") + new String("1");
,这句代码中如今生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String("1")
咱们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。
接下来s3.intern();
这一句代码,是将 s3中的“11”字符串放入 String 常量池中,由于此时常量池中不存在“11”字符串,所以常规作法是跟 jdk6 图中表示的那样,在常量池中生成一个 "11" 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块作了调整。常量池中不须要再存储一份对象了,能够直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
最后String s4 = "11";
这句代码中"11"是显示声明的,所以会直接去常量池中建立,建立的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此 s4 引用就指向和 s3 同样了。所以最后的比较 s3 == s4
是 true。
再看 s 和 s2 对象。 String s = new String("1");
第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。s.intern();
这一句是 s 对象去常量池中寻找后发现 “1” 已经在常量池里了。
接下来String s2 = "1";
这句代码是生成一个 s2的引用指向常量池中的“1”对象。 结果就是 s 和 s2 的引用地址明显不一样。图中画的很清晰。
来看第二段代码,从上边第二幅图中观察。第一段代码和第二段代码的改变就是 s3.intern();
的顺序是放在String s4 = "11";
后了。这样,首先执行String s4 = "11";
声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。而后再执行s3.intern();
时,常量池中“11”对象已经存在了,所以 s3 和 s4 的引用是不一样的。
第二段代码中的 s 和 s2 代码中,s.intern();
,这一句日后放也不会有什么影响了,由于对象池中在执行第一句代码String s = new String("1");
的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。
####小结
从上述的例子代码能够看出 jdk7 版本对 intern 操做和常量池都作了必定的修改。主要包括2点:
将String常量池 从 Perm 区移动到了 Java Heap区
String#intern
方法时,若是存在堆中的对象,会直接保存对象的引用,而不会从新建立对象。