主頁 > 後端開發 > 從HotSpot VM原始碼看字串常量池(StringTable)和intern()方法

從HotSpot VM原始碼看字串常量池(StringTable)和intern()方法

2021-04-11 06:11:39 後端開發

引言

字串常量池(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中的物件參考,如下圖所示:

image-20210205234423774

執行程序如下:

  • 執行第一行代碼時,發現"abc"不存在StringTable中,會在PermGen新建一個String物件,并回傳其參考
  • 執行第二行代碼時,發現"abc"已經存在于StringTable中,會在Heap中新建一個String物件,并且這個物件會共享之前s1的value陣列

在JDK7中,StringTable被移動到Heap中,在執行第一行代碼時,創建"abc"字串也是在Heap中進行,看起來區別并不大,僅僅是從PermGen移動到了Heap中,但這一改動會影響intern()方法的執行邏輯,后面會具體解釋,

image-20210205235109679

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里面確實是沒錯的,如下圖所示:

image-20210206000048585

但是在JDK6里面,因為字串物件s1是直接通過傳入char陣列new出來的,這個String物件是在Heap上的,

而StringTable是在PermGen里面的,無法直接將s1放入StringTable,jvm會在PermGen創建一個新的String物件,再把這個新的String物件放入StringTable中,

所以后面String s2 = "abc"時,會通過StringTable回傳新的String物件給s2,因此此時結果為false,如下圖所示:

image-20210206000711788

可以通過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找國內的鏡像,

參考資料

  • 從字串到常量池,一文看懂String類
  • 深入決議String#intern
  • JEP 310: Application Class-Data Sharing
  • JEP 341: Default CDS Archives
  • Java12新特性 -- 默認生成類資料共享(CDS)歸檔檔案
  • OpenJDK源代碼

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/274669.html

標籤:其他

上一篇:Paxos 協議簡單介紹

下一篇:面向物件編程(OOP:Object-OrientedProgramming)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more