Java中String.intern的作用及适用场景

2023-03-15 13:50:18 浏览数 (1)

本文将从源码角度分析String.intern方法的作用及其适用场景。OpenJDK版本

➜ jdk hg id 76072a077ee1 jdk-11 28

首先,我们来看下该方法的Javadoc文档

代码语言:javascript复制
/**
 * Returns a canonical representation for the string object.
 * <p>
 * A pool of strings, initially empty, is maintained privately by the
 * class {@code String}.
 * <p>
 * When the intern method is invoked, if the pool already contains a
 * string equal to this {@code String} object as determined by
 * the {@link #equals(Object)} method, then the string from the pool is
 * returned. Otherwise, this {@code String} object is added to the
 * pool and a reference to this {@code String} object is returned.
 * <p>
 * It follows that for any two strings {@code s} and {@code t},
 * {@code s.intern() == t.intern()} is {@code true}
 * if and only if {@code s.equals(t)} is {@code 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&trade; 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.
 * @jls 3.10.5 String Literals
 */
public native String intern();

其实上面文档说的还是比较清楚的,当该方法被调用时,如果JVM内部维护的string pool中已经存在和这个string内容相同的string,就返回pool中的string,否则的话,就会先把这个string放入pool中,然后再返回这个string。

不过,为了加深对该方法的理解,我们还是从源码角度再看下。

由上可知,intern是个native方法,所以我们要先找到这个方法对应的C代码

C文件src/java.base/share/native/libjava/String.c

代码语言:javascript复制
JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
    return JVM_InternString(env, this);
}

该方法调用了JVM_InternString,看下这个方法

C 文件src/hotspot/share/prims/jvm.cpp

代码语言:javascript复制
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  ...
  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

继续看下StringTable::intern方法

C 文件src/hotspot/share/classfile/stringTable.cpp

代码语言:javascript复制
oop StringTable::intern(oop string, TRAPS) {
  if (string == NULL) return NULL;
  ...
  Handle h_string (THREAD, string);
  jchar* chars = java_lang_String::as_unicode_string(string, length,
                                                     CHECK_NULL);
  oop result = intern(h_string, chars, length, CHECK_NULL);
  return result;
}
...
oop StringTable::intern(Handle string_or_null_h, jchar* name, int len, TRAPS) {
  ...
  unsigned int hash = java_lang_String::hash_code(name, len);
  ...
  return StringTable::the_table()->do_intern(string_or_null_h, name, len,
                                             hash, CHECK_NULL);
}

继续看下StringTable::the_table()->do_intern方法

C 文件src/hotspot/share/classfile/stringTable.cpp

代码语言:javascript复制
oop StringTable::do_intern(Handle string_or_null_h, jchar* name,
                           int len, uintx hash, TRAPS) {
  ...
  StringTableLookupOop lookup(THREAD, hash, string_h);
  StringTableCreateEntry stc(THREAD, string_h);
  ...
  _local_table->get_insert_lazy(THREAD, lookup, stc, stc, &rehash_warning);
  ...
  return stc.get_return();
}

该方法中,_local_table就是JVM内部维护的string pool,其类型类似与Java中的ConcurrentHashMap。

StringTableLookupOop类是用于查询_local_table中是否存在对应的string。

StringTableCreateEntry类的作用是,当_local_table中不存在对应string时,用它来创建一个存放着新string的WeakHandle,之后这个WeakHandle会被添加到_local_table中。

StringTableCreateEntry类还有另外一个作用,就是用于接收最终的结果string,不管这个string是新创建的,还是原来就存在的。这也是为什么get_insert_lazy方法第四个参数还是stc的原因。

最后,StringTable::do_intern方法调用stc.get_return()返回结果,即,如果有对应的string,则返回对应的string,如果没有,则返回原string。

我们再来看下StringTableLookupOop和StringTableCreateEntry这两个类的实现

C 文件src/hotspot/share/classfile/stringTable.cpp

代码语言:javascript复制
class StringTableLookupOop : public StackObj {
 private:
  Thread* _thread;
  uintx _hash;
  Handle _find;
  ...
 public:
  StringTableLookupOop(Thread* thread, uintx hash, Handle handle)
    : _thread(thread), _hash(hash), _find(handle) { }
  ...
  bool equals(WeakHandle<vm_string_table_data>* value, bool* is_dead) {
    oop val_oop = value->peek();
    ...
    bool equals = java_lang_String::equals(_find(), val_oop);
    if (!equals) {
      return false;
    }
    ...
    return true;
  }
};

上面equals方法就是用来检测_local_table中的string是否就是我们想要的string。

代码语言:javascript复制
class StringTableCreateEntry : public StackObj {
 private:
   Thread* _thread;
   Handle  _return;
   Handle  _store;
 public:
  StringTableCreateEntry(Thread* thread, Handle store)
    : _thread(thread), _store(store) {}


  WeakHandle<vm_string_table_data> operator()() { // No dups found
    WeakHandle<vm_string_table_data> wh =
      WeakHandle<vm_string_table_data>::create(_store);
    return wh;
  }
  void operator()(bool inserted, WeakHandle<vm_string_table_data>* val) {
    oop result = val->resolve();
    assert(result != NULL, "Result should be reachable");
    _return = Handle(_thread, result);
  }
  oop get_return() const {
    return _return();
  }
};

上面的第一个operator()方法就是用来创建_local_table中的WeakHandle,来存放新的string。第二个operator()方法就是用来接收最终结果,其中inserted参数用来表示这个string是否是新添加的。

至此,源码分析就结束了。

结合上面的Javadoc文档和源码分析,我们来想下,String.intern方法的适用场景究竟是什么呢?

首先,从Javadoc文档中我们可以得知,literal string本身就已经被intern了,所以intern方法对动态创建的string才有意义。

其次,从源码中我们可以看到,intern方法的逻辑还是比较复杂的,所以对于它返回的结果,我们不应该是立即使用,然后就丢弃,这样得不偿失。

最后,由intern本身的机制我们可以得知,调用intern方法的string必须是大量重复的,否则也没有意义。

对于这些限制条件,我第一想到的适用场景是,从网络接收的string,且这些string是提前预设的ID,且这些string会派发到其他线程做后续处理。这个场景也基本满足了动态创建、延迟使用、大量重复等特性。不过之后我做了些性能测试,发现intern的性能比我预想的要差很多,所以这种场景也不太适用了。

最后总结下,intern方法主要用于那些,动态创建的,会较长时间存活的,最好是会多次使用的,且存在大量重复的string。并且,调用intern方法的代码段对性能没有非常严格的要求。

0 人点赞