引言

字符串常量池(StringTable)是JVM中一个重要的结构,它有助于避免重复创建相同内容的String对象。那么StringTable是怎么实现的?“把字符串加入到字符串常量池中”这个过程发生了?intern()方法又做了什么?上面的问题在JDK6和JDK7中又有什么不一样的答案?

网络上已经有海量的文章讨论过上面这些问题,但是不同的文章会给出截然相反的结论。

比如:

  • StringTable中保存的是String对象,还是String对象的引用?
  • new String("a"),是在堆里创建一个新的值为“a"的String对象,还是创建一个指向StringTable中代表”a“的value数组的对象
  • new String("a") 和 字面量"a"产生的字符串对象,用的是不是同一个value数组?

想找到这些问题的准确答案,靠搜索引擎上面的资料实在太难了,还是直接看HotSpot VM的源代码更方便一点。这也印证了Linus Torvalds的那句名言:

“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.cpp

void 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支持的操作

StringTable里面的函数全部是static类型的,这意味着它是一个提供静态方法的类,是全局共享的。

下面是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(); // Probing
static oop lookup(Symbol* symbol);
static oop lookup(const jchar* chars, int length); // Interning
static 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 balance
static 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()方法

对外部来说最关键的就是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; // Jcmd
static void dump(outputStream* st, bool verbose=false);
// Debugging
static 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特性。

Java 10的Application Class-Data Sharing

Java 10中引入了Application Class-Data Sharing。在JEP 310中做了简单说明:

JEP 310: Application Class-Data Sharing
Summary
To improve startup and footprint, extend the existing Class-Data Sharing ("CDS") feature to allow application classes to be 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 class path 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 中,然后返回引用

接下来以下面的代码执行过程为例说明StringTable在JDK6和JDK7中的区别:

String s1 = "abc";
String s2 = new String("abc");

在JDK6及以前,StringTable在PermGen中,字符串常量池中保存的也是PermGen中的对象引用,如下图所示:

执行过程如下:

  • 执行第一行代码时,发现"abc"不存在StringTable中,会在PermGen新建一个String对象,并返回其引用
  • 执行第二行代码时,发现"abc"已经存在于StringTable中,会在Heap中新建一个String对象,并且这个对象会共享之前s1的value数组

在JDK7中,StringTable被移动到Heap中。在执行第一行代码时,创建"abc"字符串也是在Heap中进行。看起来区别并不大,仅仅是从PermGen移动到了Heap中,但这一改动会影响intern()方法的执行逻辑,后面会具体解释。

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);

按照常规的思路,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 possible
if (!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 possible
if (!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 中。

结语

在HotSpot VM的源码中主要得到了下面的信息:

  • 字符串常量池可以简单理解为就是一个hashmap的结构,记录的是字符串序列和String对象引用的映射关系
  • 为了在不同的Java进程之间共享字符串池,StringTable还有另外一个名为_shared_table的Map
  • JDK6中,会在永久代创建String对象再放入StringTable,而在JDK7中则直接将堆中的String对象放入StringTable中

OpenJDK中包含HotSpot VM的源码,是完全开源的。感兴趣的可以自行下载阅读:OpenJDK源代码

如果嫌Github下载太慢也可以去Gitee找国内的镜像。

参考资料

从HotSpot VM源码看字符串常量池(StringTable)和intern()方法的更多相关文章

  1. 字符串常量池和String.intern()方法在jdk1.6、1.7、1.8中的变化

    字符串常量池也是运行时常量池 jdk1.6中,它是在方法区中,属于“永久代” jdk1.7中,它被移除方法区,放在java堆中 jdk1.8中,取消了“永久代”,将常量池放在元空间,与堆独立了 pub ...

  2. java基础进阶一:String源码和String常量池

    作者:NiceCui 本文谢绝转载,如需转载需征得作者本人同意,谢谢. 本文链接:http://www.cnblogs.com/NiceCui/p/8046564.html 邮箱:moyi@moyib ...

  3. JVM字符串常量池StringTable

    String的基本特性 String:字符串,使用一对""引起来表示. String sl = "hello"://字面量的定义方式: String s2 = ...

  4. Knowledge Point 20180309 字符串常量池与String,intern()

    引言 什么都先不说,先看下面这个引入的例子: public static void test4(){ String str1 = new String("SEU") + new S ...

  5. 常量池之String.intern()方法

    JDK7将String常量池从Perm区移动到了Java Heap区.在JDK1.6中,intern方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中的实例.但是在JDK1.7以后,Str ...

  6. jvm源码解读--04 常量池 常量项的解析CONSTANT_Class_info

    接上篇的继续 ConstantPool* constant_pool = ConstantPool::allocate(_loader_data, length, CHECK_(nullHandle) ...

  7. jvm源码解读--03 常量池的解析ConstantPool

    先看bt栈 (gdb) bt #0 ConstantPool::allocate (loader_data=0x7fe21802e868, length=87, __the_thread__=0x7f ...

  8. jvm源码解读--05 常量池 常量项的解析JVM_CONSTANT_Utf8

    当index=18的时候JVM_CONSTANT_Utf8 case JVM_CONSTANT_Utf8 : { cfs->guarantee_more(2, CHECK); // utf8_l ...

  9. Java中的字符串常量池

    ava中字符串对象创建有两种形式,一种为字面量形式,如String str = "droid";,另一种就是使用new这种标准的构造对象的方法,如String str = new ...

随机推荐

  1. shit 环信 IM SDK & IM SDK & web

    shit 环信 IM SDK & IM SDK & web 环信 IM SDK, 采坑大全 自己写 UI appkey 是否正确 password 是字符串,不是 数字 HTTPS 是 ...

  2. node.js 如何处理一个很大的文件

    node.js 如何处理一个很大的文件 思路 arraybuffer 数据分段 时间分片 多线程 web workers sevice workers node.js 如何处理一个很大的文件 http ...

  3. SameSite cookies explained

    SameSite cookies explained

  4. windows10 安装NASM

    NASM官网 下载NASM NASM在线HTML文档 下载golink golink文档 NASM教程 in windows x64调用约定 x86调用约定 编码样式约定 在编写nasm时数字默认为1 ...

  5. Flutter: ValueListenableBuilder 内容与ValueListenable保持"同步"的窗口小部件

    API 使用这个修改状态可以不用setState(). class _MyHomeState extends State<MyHome> { final ValueNotifier< ...

  6. 一周精彩内容分享(第 3 期):开工大吉的 B 面

    这里记录过去一周,我看到的值得分享的东西. 一方面是整理记录一下自己一周的学习,另一方面也是期待自己有更多的输出,有更多的价值. 周刊开源(Github:wmyskxz/weekly),欢迎提交 is ...

  7. SQL Server中DELETE和TRUNCATE的区别

    ​DELETE和TRUNCATE语句之间的区别是求职面试中最常见的问题之一.这两条语句都可以从表中删除数据.然而,也有不同之处. 本文将重点讨论这些差异,并通过实例加以说明. TRUNCATE DEL ...

  8. 谁手握账本?趣讲 ZK 的内存模型

    本文作者:HelloGitHub-老荀 Hi,这里是 HelloGitHub 推出的 HelloZooKeeper 系列,免费开源.有趣.入门级的 ZooKeeper 教程,面向有编程基础的新手. 本 ...

  9. Windows定时重新启动(适用于server 2012 r2)

    直接看链接吧:https://jingyan.baidu.com/article/2d5afd69dd8e9d85a2e28eb7.html 开始菜单,找到"计划任务程序"; 2 ...

  10. Loki日志系统

    一.概述 背景 Loki的第一个稳定版本于2019年11月19日发布,是 Grafana Labs 团队最新的开源项目,是一个水平可扩展,高可用性,多租户的日志聚合系统. Grafana 对 Loki ...