引言字符串常量池(StringTable)是JVM中一个重要的结构,它有助于避免重复创建相同内容的String对象 。那么StringTable是怎么实现的?“把字符串加入到字符串常量池中”这个过程发生了?intern()方法又做了什么?上面的问题在JDK6和JDK7中又有什么不一样的答案?
网络上已经有海量的文章讨论过上面这些问题,但是不同的文章会给出截然相反的结论 。
比如:
- StringTable中保存的是String对象,还是String对象的引用?
new String("a"),是在堆里创建一个新的值为“a"的String对象,还是创建一个指向StringTable中代表”a“的value数组的对象?new String("a")和 字面量"a"产生的字符串对象,用的是不是同一个value数组?
“Talk is cheap. Show me the code.”
源码中StringTable的结构StringTable的底层结构字符串常量池可以简单理解为就是一个hashmap的结构,记录的是字符串序列和String对象引用的映射关系 。
在
hotspot\share\memory\universe.cpp中对StringTable进行了初始化:StringTable::create_table();可以看看create_table()函数的源码,位于hotspot\share\classfile\stringTable.cppvoid StringTable::create_table() {size_t start_size_log_2 = ceil_log2(StringTableSize);_current_size = ((size_t)1) << start_size_log_2;log_trace(stringtable)("Start size: " SIZE_FORMAT " (" SIZE_FORMAT ")",_current_size, start_size_log_2);_local_table = new StringTableHash(start_size_log_2, END_SIZE, REHASH_LEN);_oop_storage = OopStorageSet::create_weak("StringTable Weak");_oop_storage->register_num_dead_callback(&gc_notification);}里面最关键的是_local_table = new StringTableHash(start_size_log_2, END_SIZE, REHASH_LEN);这一行代码对
_local_table进行了初始化,这里的_local_table是一个static类型的变量,指向的是StringTableHash类的对象 。StringTableHash是什么?
StringTableHash是个别名,它实际上是
hotspot\share\utilities\concurrentHashTable.hpp中定义的ConcurrentHashTable 。如下:typedef ConcurrentHashTable<StringTableConfig, mtSymbol> StringTableHash;static StringTableHash* _local_table = NULL;ConcurrentHashTable的源码就不贴出来了,里面有注释说明它是A mostly concurrent-hash-table,简单来说就是支持并发操作的hash表,类似于jdk中的ConcurrentHashMap 。读到这里,可以得到以下信息:
- StringTable只在
universe.cpp中被初始化,之后都是共享的 。 - StringTable的底层是
_local_table指向的ConcurrentHashTable,一个并发散列表 。 - StringTable的数据保存在一个静态变量中,全局共享 。
下面是
stringTable.hpp中定义的核心public函数列表:public:static size_t table_size();static TableStatistics get_table_statistics();static void create_table();static void do_concurrent_work(JavaThread* jt);static bool has_work();// Probingstatic oop lookup(Symbol* symbol);static oop lookup(const jchar* chars, int length);// Interningstatic oop intern(Symbol* symbol, TRAPS);static oop intern(oop string, TRAPS);static oop intern(const char *utf8_string, TRAPS);// Rehash the string table if it gets out of balancestatic void rehash_table();static bool needs_rehashing() { return _needs_rehashing; }static inline void update_needs_rehash(bool rehash) {if (rehash) {_needs_rehashing = true;}}从函数命名也可以看出StringTable主要支持的操作:- 创建,查看表信息和状态等操作如
table_size()、create_table()、has_work()、get_table_statistics() - 查找字符串如
lookup(),尝试池化字符串如intern() - hash相关操作如
rehash_table()、needs_rehashing()
lookup()和intern()方法,intern()后面会再解释 。这里先看看lookup()lookup就是查找的意思,用于通过字符串查找对应的String对象 。最终会执行到
do_lookup()方法:oop StringTable::do_lookup(const jchar* name, int len, uintx hash) {Thread* thread = Thread::current();StringTableLookupJchar lookup(thread, hash, name, len);StringTableGet stg(thread);bool rehash_warning;_local_table->get(thread, lookup, stg, &rehash_warning);update_needs_rehash(rehash_warning);return stg.get_res_oop();}这里可以看到这样一行代码: _local_table->get(thread, lookup, stg, &rehash_warning);说明String对象最终是从
_local_table中拿到的,返回值类型是oop也就是普通对象引用 。类数据共享(Class-Data Sharing)从StringTable的另外一个Map说起前面说到StringTable的底层是
_local_table指向的concurrentHashTable 。但我看的StringTable源码中(JDK16),还有另外一个Map:static CompactHashtable<const jchar*, oop,read_string_from_compact_hashtable,java_lang_String::equals> _shared_table;这里定义了一个CompactHashtable类型的变量_shared_table 。并且有一些专门为其提供的方法:// Sharing private:static oop lookup_shared(const jchar* name, int len, unsigned int hash) NOT_CDS_JAVA_HEAP_RETURN_(NULL); public:static oop create_archived_string(oop s, Thread* THREAD) NOT_CDS_JAVA_HEAP_RETURN_(NULL);static void shared_oops_do(OopClosure* f) NOT_CDS_JAVA_HEAP_RETURN;static void write_to_archive(const DumpedInternedStrings* dumped_interned_strings) NOT_CDS_JAVA_HEAP_RETURN;static void serialize_shared_table_header(SerializeClosure* soc) NOT_CDS_JAVA_HEAP_RETURN;// Jcmdstatic void dump(outputStream* st, bool verbose=false);// Debuggingstatic size_t verify_and_compare_entries();static void verify();因此去看了一下源码_compact_buckets = MetaspaceShared::new_ro_array<u4>(_num_buckets + 1);_compact_entries = MetaspaceShared::new_ro_array<u4>(entries_space);它是通过MetaspaceShared::new_ro_array来申请空间 。ro表示了它是块只读的内存空间 。MetaspaceShared的源码注释中提到,它提供三种类型的空间分配:
// The CDS archive is divided into the following regions://mc- misc code (the method entry trampolines, c++ vtables)//rw- read-write metadata//ro- read-only metadata and read-only tables并且这三块空间在内存中是连续的 。看起来很奇怪,已经有了
_local_table,为什么还需要用一个只读的空间来保存字符串?而且Metaspace在JDK1.8中已经移动到本地内存中了,而字符串常量池此时是在堆中?
这就要提到下面的类数据共享了 。
类数据共享的发展历史下面的历史引自博客:Java12新特性 -- 默认生成类数据共享(CDS)归档文件
- JDK5引入了Class-Data Sharing可以用于多个JVM共享class,提升启动速度,最早只支持system classes及serial GC 。
- JDK9对其进行扩展以支持application classes及其他GC算法 。
- java10的新特性JEP 310: Application Class-Data Sharing扩展了JDK5引入的Class-Data Sharing,支持application的Class-Data Sharing并开源出来(以前是commercial feature)
- CDS 只能作用于 BootClassLoader 加载的类,不能作用于 AppClassLoader 或者自定义的 ClassLoader加载的类 。在 Java 10 中,则将 CDS 扩展为 AppCDS,顾名思义,AppCDS 不止能够作用于BootClassLoader了,AppClassLoader 和自定义的 ClassLoader 也都能够起作用,大大加大了 CDS 的适用范围 。也就说开发自定义的类也可以装载给多个JVM共享了 。
- JDK11将
-Xshare:off改为默认-Xshare:auto,以更加方便使用CDS特性 。
JEP 310: Application Class-Data SharingSummary To improve startup and footprint, extend the existing Class-Data Sharing ("CDS") feature to allow application classes tobe placed in the shared archive.Goals- Reduce footprint by sharing common class metadata across different Java processes.- Improve startup time.- Extend CDS to allow archived classes from the JDK's run-time image file ($JAVA_HOME/lib/modules) and the application classpath to be loaded into the built-in platform and system class loaders.- Extend CDS to allow archived classes to be loaded into custom class loaders.网上似乎没有多少资料谈到这个类数据共享机制,不过从这个草案也可以略知一二:- Class-Data Sharing 允许将Java类放置在共享的存档空间中
- 通过在不同的Java进程之间共享公共类元数据来减少内存占用
_shared_table的用处:用于在不同的Java进程之间共享字符串池 。StringTable和intern()方法的变化StringTable在JDK1.7的变化把String对象加入StringTable的逻辑是:
- 从 StringTable 中找给定的字符串对象,找到的话就直接返回其引用
- 找不到就把当前字符串对象添加到 StringTable 中,然后返回引用
String s1 = "abc";String s2 = new String("abc");在JDK6及以前,StringTable在PermGen中,字符串常量池中保存的也是PermGen中的对象引用,如下图所示:
文章插图
执行过程如下:
- 执行第一行代码时,发现"abc"不存在StringTable中,会在PermGen新建一个String对象,并返回其引用
- 执行第二行代码时,发现"abc"已经存在于StringTable中,会在Heap中新建一个String对象,并且这个对象会共享之前s1的value数组

文章插图
intern()方法在JDK1.7的变化String Table在JDK1.6中位于Perm Gen,但是在JDK1.7中被转移到了Java Heap中,这次转移伴随着String.intern()方法的性质发生了一些微小的改变 。
- 在1.6中,intern的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该对象的引用 。如果没有找到,则将该字符串常量加入到字符串常量区,也就是在永久代中创建该字符串对象,再把引用保存到字符串常量池中 。
- 在1.7中,intern的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该对象的引用,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中 。
String s1 = new String(new char[]{'a','b','c'});s1.intern();String s2 = "abc";System.out.println(s1 == s2);【StringTable 从HotSpot VM源码看字符串常量池和intern方法】按照常规的思路,s1.intern()会将s1放进字符串常量池,然后String s2 = "abc"时,会通过StringTable返回s1的引用给s2,所以结果是true 。这在JDK7里面确实是没错的,如下图所示:

文章插图
但是在JDK6里面,因为字符串对象
s1是直接通过传入char数组new出来的,这个String对象是在Heap上的 。而StringTable是在PermGen里面的,无法直接将
s1放入StringTable,jvm会在PermGen创建一个新的String对象,再把这个新的String对象放入StringTable中 。所以后面
String s2 = "abc"时,会通过StringTable返回新的String对象给s2,因此此时结果为false,如下图所示:
文章插图
可以通过JDK6和JDK7中intern()的C++源码来验证:
JDK 6 版本的 openjdk 代码:
// try to reuse the string if possibleif (!string_or_null.is_null() && (!JavaObjectsInPerm || string_or_null()->is_perm())) {string = string_or_null;} else {string = java_lang_String::create_tenured_from_unicode(name, len, CHECK_NULL);}JDK 7 版本的 openjdk 代码:// try to reuse the string if possibleif (!string_or_null.is_null()) {string = string_or_null;} else {string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);}区别在JDK6在把字符串放入StringTable时多了一行判断: (!JavaObjectsInPerm || string_or_null()->is_perm())- 这个用于判断字符串是否在永久代中,如果是,最终会将这个 string_or_null 放入 StringTable 中
- 否则,最终会通过
java_lang_String::create_tenured_from_unicode在永久代中再次创建一个 String 对象,然后放入 StringTable 中 。
- 字符串常量池可以简单理解为就是一个hashmap的结构,记录的是字符串序列和String对象引用的映射关系
- 为了在不同的Java进程之间共享字符串池,StringTable还有另外一个名为
_shared_table的Map - JDK6中,会在永久代创建String对象再放入StringTable,而在JDK7中则直接将堆中的String对象放入StringTable中
如果嫌Github下载太慢也可以去Gitee找国内的镜像 。
参考资料
- 从字符串到常量池,一文看懂String类
- 深入解析String#intern
- JEP 310: Application Class-Data Sharing
- JEP 341: Default CDS Archives
- Java12新特性 -- 默认生成类数据共享(CDS)归档文件
- OpenJDK源代码
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
