String的Intern⽅法详解
引⾔
在 JAVA 语⾔中有8中基本类型和⼀种⽐较特殊的类型String。这些类型为了使他们在运⾏过程中速度更快,更节省内存,都提供了⼀种常量池的概念。常量池就类似⼀个JAVA系统级别提供的缓存。8种基本类型的常量池都是系统协调的,String类型的常量池⽐较特殊。它的主要使⽤⽅法有两种:
直接使⽤双引号声明出来的String对象会直接存储在常量池中。
如果不是⽤双引号声明的String对象,可以使⽤String提供的intern⽅法。intern ⽅法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放⼊常量池中
⼀. intern 的实现原理
1.JAVA 代码
/**
* 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();
View Code
String#intern⽅法中看到,这个⽅法是⼀个 native 的⽅法,但注释写的⾮常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放⼊常量池中后, 再返回”。
2,native 代码
在 jdk7后,oracle 接管了 JAVA 的源码后就不对外开放了,根据 jdk 的主要开发⼈员声明 openJdk7 和 jdk7 使⽤的是同⼀分主代码,只是分⽀代码会有些许的变动。所以可以直接跟踪 openJdk7 的源码来探究 intern 的实现。
native实现代码:
\openjdk7\jdk\src\share\native\java\lang\String.c
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
⼆.jdk6 和 jdk7 下 intern 的区别
相信很多 JAVA 程序员都做做类似 String s = new String("abc")这个语句创建了⼏个对象的题⽬。这种题⽬主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第⼀个对象是”abc”字符串存储在常量池中,第⼆个对象在JAVA Heap中的String 对象。
1 2 3 4 5 6 7 8 9 10 11public 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";后⾯。是什么结果呢
1 2 3 4 5 6public static void main(String[] args) { String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
7891011
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中的解释
在 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 ⽅法时,如果存在堆中的对象,会直接保存对象的引⽤,⽽不会重新创建对象。
三.使⽤ intern
1.intern 正确使⽤例⼦
接下来我们来看⼀下⼀个⽐较常见的使⽤String#intern ⽅法的例⼦。
1
23456789101112131415161718static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];
public static void main(String[] args) throws Exception {
Integer[] DB_DATA = new Integer[10];
Random random = new Random(10 * 10000);
for (int i = 0; i < DB_DATA.length; i++) {
DB_DATA[i] = Int();
}
long t = System.currentTimeMillis(); for (int i = 0; i < MAX; i++) { //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])); arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern(); } System.out.println((System.currentTimeMillis() - t) + "ms");
<();
}
运⾏的参数是:-Xmx2g -Xms2g -Xmn1500M 上述代码是⼀个演⽰代码,其中有两条语句不⼀样,⼀条是使⽤ intern ,⼀条是未使⽤intern 。结果如下图
2160ms
826ms
通过上述结果,我们发现不使⽤ intern 的代码⽣成了1000w 个字符串,占⽤了⼤约640m 空间。使⽤了 intern 的代码⽣成了1345个字符串,占⽤总空间 133k 左右。其实通过观察程序中只是⽤到了10个字符串,所以准确计算后应该是正好相差100w 倍。虽然例⼦有些极端,但确实能准确反应出 intern 使⽤后产⽣的巨⼤空间节省。
细⼼的同学会发现使⽤了 intern ⽅法后时间上有了⼀些增长。这是因为程序中每次都是⽤了 new String 后,然后⼜进⾏ intern 操作的耗时时间,这⼀点如果在内存空间充⾜的情况下确实是⽆法避免的,但我们平时使⽤时,内存空间肯定不是⽆限⼤的,不使⽤ intern 占⽤空间导致 jvm 垃圾回收的时间是要远远⼤于这点时间的。毕竟这⾥使⽤了1000w次intern 才多出来1秒钟多的时间。
2,intern 不当使⽤
看过了 intern 的使⽤和 intern 的原理等,我们来看⼀个不当使⽤ intern 操作导致的问题。
在使⽤ fastjson 进⾏接⼝读取的时候,我们发现在读取了近70w条数据后,我们的⽇志打印变的⾮常缓慢,每打印⼀次⽇志⽤时30ms左右,如果在⼀个请求中打印2到3条⽇志以上会发现请求有⼀倍以上的耗时。在重新启动 jvm 后问题消失。继续读取接⼝后,问题⼜重现。接下来我们看⼀下出现问题的过程。
1,根据 log4j 打印⽇志查问题原因
在使⽤log4j#info打印⽇志的时候时间⾮常长。所以使⽤ housemd 软件跟踪 info ⽅法的耗时堆栈。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论