引言

字符串常量池(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进行了初始化:

  1. StringTable::create_table();

可以看看create_table()函数的源码,位于hotspot\share\classfile\stringTable.cpp

  1. void StringTable::create_table() {
  2. size_t start_size_log_2 = ceil_log2(StringTableSize);
  3. _current_size = ((size_t)1) << start_size_log_2;
  4. log_trace(stringtable)("Start size: " SIZE_FORMAT " (" SIZE_FORMAT ")",
  5. _current_size, start_size_log_2);
  6. _local_table = new StringTableHash(start_size_log_2, END_SIZE, REHASH_LEN);
  7. _oop_storage = OopStorageSet::create_weak("StringTable Weak");
  8. _oop_storage->register_num_dead_callback(&gc_notification);
  9. }

里面最关键的是_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。如下:

  1. typedef ConcurrentHashTable<StringTableConfig, mtSymbol> StringTableHash;
  2. 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函数列表:

  1. public:
  2. static size_t table_size();
  3. static TableStatistics get_table_statistics();
  4. static void create_table();
  5. static void do_concurrent_work(JavaThread* jt);
  6. static bool has_work();
  7. // Probing
  8. static oop lookup(Symbol* symbol);
  9. static oop lookup(const jchar* chars, int length);
  10. // Interning
  11. static oop intern(Symbol* symbol, TRAPS);
  12. static oop intern(oop string, TRAPS);
  13. static oop intern(const char *utf8_string, TRAPS);
  14. // Rehash the string table if it gets out of balance
  15. static void rehash_table();
  16. static bool needs_rehashing() { return _needs_rehashing; }
  17. static inline void update_needs_rehash(bool rehash) {
  18. if (rehash) {
  19. _needs_rehashing = true;
  20. }
  21. }

从函数命名也可以看出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()方法:

  1. oop StringTable::do_lookup(const jchar* name, int len, uintx hash) {
  2. Thread* thread = Thread::current();
  3. StringTableLookupJchar lookup(thread, hash, name, len);
  4. StringTableGet stg(thread);
  5. bool rehash_warning;
  6. _local_table->get(thread, lookup, stg, &rehash_warning);
  7. update_needs_rehash(rehash_warning);
  8. return stg.get_res_oop();
  9. }

这里可以看到这样一行代码: _local_table->get(thread, lookup, stg, &rehash_warning);

说明String对象最终是从_local_table中拿到的,返回值类型是oop也就是普通对象引用。

类数据共享(Class-Data Sharing)

从StringTable的另外一个Map说起

前面说到StringTable的底层是_local_table指向的concurrentHashTable。但我看的StringTable源码中(JDK16),还有另外一个Map:

  1. static CompactHashtable<
  2. const jchar*, oop,
  3. read_string_from_compact_hashtable,
  4. java_lang_String::equals
  5. > _shared_table;

这里定义了一个CompactHashtable类型的变量_shared_table。并且有一些专门为其提供的方法:

  1. // Sharing
  2. private:
  3. static oop lookup_shared(const jchar* name, int len, unsigned int hash) NOT_CDS_JAVA_HEAP_RETURN_(NULL);
  4. public:
  5. static oop create_archived_string(oop s, Thread* THREAD) NOT_CDS_JAVA_HEAP_RETURN_(NULL);
  6. static void shared_oops_do(OopClosure* f) NOT_CDS_JAVA_HEAP_RETURN;
  7. static void write_to_archive(const DumpedInternedStrings* dumped_interned_strings) NOT_CDS_JAVA_HEAP_RETURN;
  8. static void serialize_shared_table_header(SerializeClosure* soc) NOT_CDS_JAVA_HEAP_RETURN;
  9. // Jcmd
  10. static void dump(outputStream* st, bool verbose=false);
  11. // Debugging
  12. static size_t verify_and_compare_entries();
  13. static void verify();

因此去看了一下源码

  1. _compact_buckets = MetaspaceShared::new_ro_array<u4>(_num_buckets + 1);
  2. _compact_entries = MetaspaceShared::new_ro_array<u4>(entries_space);

它是通过MetaspaceShared::new_ro_array来申请空间。ro表示了它是块只读的内存空间。

MetaspaceShared的源码注释中提到,它提供三种类型的空间分配:

  1. // The CDS archive is divided into the following regions:
  2. // mc - misc code (the method entry trampolines, c++ vtables)
  3. // rw - read-write metadata
  4. // 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中做了简单说明:

  1. JEP 310: Application Class-Data Sharing
  2. Summary
  3. To improve startup and footprint, extend the existing Class-Data Sharing ("CDS") feature to allow application classes to be placed in the shared archive.
  4. Goals
  5. - Reduce footprint by sharing common class metadata across different Java processes.
  6. - Improve startup time.
  7. - 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.
  8. - 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中的区别:

  1. String s1 = "abc";
  2. 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的处理是先判断字符串常量是否在字符串常量池中,如果存在直接返回该对象的引用,如果没有找到,说明该字符串常量在堆中,则处理是把堆区该对象的引用加入到字符串常量池中,以后别人拿到的是该字符串常量的引用,实际存在堆中。

例如下面的代码:

  1. String s1 = new String(new char[]{'a','b','c'});
  2. s1.intern();
  3. String s2 = "abc";
  4. 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 代码:

  1. // try to reuse the string if possible
  2. if (!string_or_null.is_null() && (!JavaObjectsInPerm || string_or_null()->is_perm())) {
  3. string = string_or_null;
  4. } else {
  5. string = java_lang_String::create_tenured_from_unicode(name, len, CHECK_NULL);
  6. }

JDK 7 版本的 openjdk 代码:

  1. // try to reuse the string if possible
  2. if (!string_or_null.is_null()) {
  3. string = string_or_null;
  4. } else {
  5. string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
  6. }

区别在JDK6在把字符串放入StringTable时多了一行判断:

  1. (!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. github & markdown & image layout

    github & markdown & image layout css & right https://github.com/sindresorhus/log-symbols ...

  2. js in depth: closure function & curly function

    js in depth: closure function & curly function 闭包, 科里化 new js 构造函数 实例化, 不需要 new var num = new Ar ...

  3. 高倍币VAST,如何破局NGK算力市场?

    2020年,全球经济危机的爆发,无疑是给全球经济蒙上了一层阴影.而世界主要经济体也开启了无节制的放水,通过一轮又一轮的宽松货币政策,以刺激经济的发展.然而宽松的货币政策也加速了以美元为首的货币贬值,同 ...

  4. SharedPreferences 数据传输中遇到的一些问题总结

    原构想:MainActivity 设置两个按钮,btn1--跳转Main2Activity通过复选框组选择并提交,btn2--跳转Main3Activity通过RecycleView显示选择结果. 主 ...

  5. 12.scikit-learn中的Scaler

    import numpy as np from sklearn import datasets iris = datasets.load_iris() X = iris.data y = iris.t ...

  6. 基于tcp的应用层消息边界如何定义

    聊聊基于tcp的应用层消息边界如何定义 背景 2018年笔者有幸接触一个项目要用到长连接实现云端到设备端消息推送,所以借机了解过相关的内容,最终是通过rabbitmq+mqtt实现了相关功能,同时在心 ...

  7. js中函数调用时,对参数个数和类型没有要求

    因为js是一种弱类型的编程语言,对数据类型的要求没有其他编程语言的要求严格,所以在定义函数的时候不需要像java一样对其传入参数的类型进行定,也对传入参数的个数没有要求. js函数的参数与大多数其他语 ...

  8. Git 提交获取项目与提交项目 记录

    首先去git官网下载版本安装:https://git-scm.com/downloads 在自己生产免密令牌,安装后用git程序导出. 1.自己在桌面或者某盘创建一个文件夹,在文件夹右键找到 GIt ...

  9. 博客数据库要连接Elasticsearch,使用MySQL还是MongoDB更合理

    若进行博客等文本类数据的读写以及专业搜索引擎的连接的解决方案对比,可以肯定的下结论:MongoDB的解决方案中要远远好于MySQL的解决方案. 一.从开发工序角度 MySQL的文章读写方式 方式一:文 ...

  10. docker 部署mysql服务之后,执行sql脚本

    1,先将.sql文件copy到docker容器里 docker ps //找到容器的短ID或者指定的name. docker inspect  -f '{{.Id}}' id or name 得到指定 ...