关于java:深入解析Stringintern

41次阅读

共计 11013 个字符,预计需要花费 28 分钟才能阅读完成。

在 JAVA 语言中有 8 中根本类型和一种比拟非凡的类型 String。这些类型为了使他们在运行过程中速度更快,更节俭内存,都提供了一种常量池的概念。常量池就相似一个 JAVA 零碎级别提供的缓存。

8 种根本类型的常量池都是零碎协调的,String 类型的常量池比拟非凡。它的次要应用办法有两种:

间接应用双引号申明进去的 String 对象会间接存储在常量池中。
如果不是用双引号申明的 String 对象,能够应用 String 提供的 intern 办法。intern 办法会从字符串常量池中查问以后字符串是否存在,若不存在就会将以后字符串放入常量池中
接下来咱们次要来谈一下 String#intern 办法。

首先深刻看一下它的实现原理。

1,JAVA 代码
/**

  • Returns a canonical representation for the string object.
  • <p>
  • A pool of strings, initially empty, is maintained privately by the
  • class String.
  • <p>
  • When the intern method is invoked, if the pool already contains a
  • string equal to this String object as determined by
  • the {@link #equals(Object)} method, then the string from the pool is
  • returned. Otherwise, this String object is added to the
  • pool and a reference to this String object is returned.
  • <p>
  • It follows that for any two strings s and t,
  • s.intern() == t.intern() is true
  • if and only if s.equals(t) is true.
  • <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 的办法,但正文写的十分明了。“如果常量池中存在以后字符串, 就会间接返回以后字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。

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
置信很多 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 办法时,如果存在堆中的对象,会间接保留对象的援用,而不会从新创建对象。
1,intern 正确应用例子
接下来咱们来看一下一个比拟常见的应用 String#intern 办法的例子。

代码如下:

static 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] = random.nextInt();}
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");
System.gc();

}
运行的参数是:-Xmx2g -Xms2g -Xmn1500M 上述代码是一个演示代码,其中有两条语句不一样,一条是应用 intern,一条是未应用 intern。后果如下图

2160ms
应用 intern

826ms
未应用 intern

通过上述后果,咱们发现不应用 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 办法的耗时堆栈。

trace SLF4JLogger.
trace AbstractLoggerWrapper:
trace AsyncLogger
org/apache/logging/log4j/core/async/AsyncLogger.actualAsyncLog(RingBufferLogEvent) sun.misc.Launcher$AppClassLoader@109aca82 1 1ms org.apache.logging.log4j.core.async.AsyncLogger@19de86bb
org/apache/logging/log4j/core/async/AsyncLogger.location(String) sun.misc.Launcher$AppClassLoader@109aca82 1 30ms org.apache.logging.log4j.core.async.AsyncLogger@19de86bb
org/apache/logging/log4j/core/async/AsyncLogger.log(Marker, String, Level, Message, Throwable) sun.misc.Launcher$AppClassLoader@109aca82 1 61ms org.apache.logging.log4j.core.async.AsyncLogger@19de86bb
代码出在 AsyncLogger.location 这个办法上. 里边次要是调用了 return Log4jLogEvent.calcLocation(fqcnOfLogger); 和 Log4jLogEvent.calcLocation()

Log4jLogEvent.calcLocation()的代码如下:

public static StackTraceElement calcLocation(final String fqcnOfLogger) {

if (fqcnOfLogger == null) {return null;}  
final StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();  
boolean next = false;  
for (final StackTraceElement element : stackTrace) {final String className = element.getClassName();  
    if (next) {if (fqcnOfLogger.equals(className)) {continue;}  
        return element;  
    }  
    if (fqcnOfLogger.equals(className)) {next = true;} else if (NOT_AVAIL.equals(className)) {break;}  
}  
return null;  

}
通过跟踪发现是 Thread.currentThread().getStackTrace(); 的问题。

2, 跟踪 Thread.currentThread().getStackTrace()的 native 代码,验证 String#intern

Thread.currentThread().getStackTrace();native 的办法:

public StackTraceElement[] getStackTrace() {

if (this != Thread.currentThread()) {  
    // check for getStackTrace permission  
    SecurityManager security = System.getSecurityManager();  
    if (security != null) {  
        security.checkPermission(SecurityConstants.GET_STACK_TRACE_PERMISSION);  
    }  
    // optimization so we do not call into the vm for threads that  
    // have not yet started or have terminated  
    if (!isAlive()) {return EMPTY_STACK_TRACE;}        StackTraceElement[][] stackTraceArray = dumpThreads(new Thread[] {this});  
    StackTraceElement[] stackTrace = stackTraceArray[0];  
    // a thread that was alive during the previous isAlive call may have  
    // since terminated, therefore not having a stacktrace.  
    if (stackTrace == null) {stackTrace = EMPTY_STACK_TRACE;}  
    return stackTrace;  
} else {  
    // Don't need JVM help for current thread  
    return (new Exception()).getStackTrace();}  

}

private native static StackTraceElement[][] dumpThreads(Thread[] threads);
下载 openJdk7 的源码查问 jdk 的 native 实现代码,列表如下【这里因为篇幅问题,不具体列举波及到的代码,有趣味的能够依据文件名称和行号查找相干代码】:

\openjdk7\jdk\src\share\native\java\lang\Thread.c
\openjdk7\hotspot\src\share\vm\prims\jvm.h line:294: \openjdk7\hotspot\src\share\vm\prims\jvm.cpp line:4382-4414:
\openjdk7\hotspot\src\share\vm\services\threadService.cpp line:235-267: \openjdk7\hotspot\src\share\vm\services\threadService.cpp line:566-577:
\openjdk7\hotspot\src\share\vm\classfile\javaClasses.cpp line:1635-[1651,1654,1658]:

实现跟踪了底层的 jvm 源码后发现,是下边的三条代码引发了整个程序的变慢问题。

oop classname = StringTable::intern((char*) str, CHECK_0);
oop methodname = StringTable::intern(method->name(), CHECK_0);
oop filename = StringTable::intern(source, CHECK_0);
这三段代码是获取类名、办法名、和文件名。因为类名、办法名、文件名都是存储在字符串常量池中的,所以每次获取它们都是通过 String#intern 办法。但没有思考到的是默认的 StringPool 的长度是 1009 且不可变的。因而一旦常量池中的字符串达到的肯定的规模后,性能会急剧下降。

3,fastjson 不当应用 String#intern

导致这个 intern 变慢的起因是因为 fastjson 对 String#intern 办法的使用不当造成的。跟踪 fastjson 中的实现代码发现,

com.alibaba.fastjson.parser.JSONScanner#scanFieldSymbol()

if (ch == ‘\”‘) {

bp = index;
this.ch = ch = buf[bp];
strVal = symbolTable.addSymbol(buf, start, index - start - 1, hash);
break;

}

com.alibaba.fastjson.parser.SymbolTable#addSymbol():

/**

  • Constructs a new entry from the specified symbol information and next entry reference.
    */

public Entry(char[] ch, int offset, int length, int hash, Entry next){

characters = new char[length];
System.arraycopy(ch, offset, characters, 0, length);
symbol = new String(characters).intern();
this.next = next;
this.hashCode = hash;
this.bytes = null;

}
fastjson 中对所有的 json 的 key 应用了 intern 办法,缓存到了字符串常量池中,这样每次读取的时候就会十分快,大大减少工夫和空间。而且 json 的 key 通常都是不变的。这个中央没有思考到大量的 json key 如果是变动的,那就会给字符串常量池带来很大的累赘。

这个问题 fastjson 在 1.1.24 版本中曾经将这个破绽修复了。程序退出了一个最大的缓存大小,超过这个大小后就不会再往字符串常量池中放了。

[1.1.24 版本的 com.alibaba.fastjson.parser.SymbolTable#addSymbol() Line:113]代码

public static final int MAX_SIZE = 1024;

if (size >= MAX_SIZE) {

return new String(buffer, offset, len);

}
这个问题是 70w 数据量时候的引发的,如果是几百万的数据量的话可能就不只是 30ms 的问题了。因而在应用零碎级提供的 String#intern 形式肯定要谨慎!

本文大体的形容了 String#intern 和字符串常量池的日常应用,jdk 版本的变动和 String#intern 办法的区别,以及不失当应用导致的危险等内容,让大家对系统级别的 String#intern 有一个比拟深刻的意识。让咱们在应用和接触它的时候能避免出现一些 bug,加强零碎的健壮性。

正文完
 0