日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

string会传null吗_JVM 解剖公园(10): String.intern()

發布時間:2023/12/15 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 string会传null吗_JVM 解剖公园(10): String.intern() 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

(給ImportNew加星標,提高Java技能)

編譯:ImportNew/唐尤華

shipilev.net/jvm/anatomy-quarks/10-string-intern/

1. 寫在前面

“JVM 解剖公園”是一個持續更新的系列迷你博客,閱讀每篇文章一般需要5到10分鐘。限于篇幅,僅對某個主題按照問題、測試、基準程序、觀察結果深入講解。因此,這里的數據和討論可以當軼事看,不做寫作風格、句法和語義錯誤、重復或一致性檢查。如果選擇采信文中內容,風險自負。

Aleksey Shipilёv,JVM 性能極客

推特 @shipilev

問題、評論、建議發送到 aleksey@shipilev.net"">aleksey@shipilev.net

2. 問題

String.intern() 的工作機制究竟是怎樣的?要不要避免使用 String.intern()?

3. 理論

如果仔細讀過 String Javadoc,你應該會注意到 public API 中有一個非常有意思的方法:

public String intern()

返回字符串對象的規范表示。String 類維護一個內部字符串池,初始為空。

調用 intern(),如果池中有字符串與調用的字符串 equals(Object) 結果相等,直接返回池中的字符串;否則,加入字符串池并返回對象引用。

— JDK Javadoc

java.lang.String

看起來 String 提供的接口可以操作內部字符串池進而優化內存,對嗎?然而,這里有一個缺點:OpenJDK 的 String.intern() 是本地(native)實現,執行時會調用 JVM 把 String 存入本地 JVM 字符串池。由于 intern 是一個 JDK 與 VM 之間的接口,所以 VM 本地代碼和 JDK 代碼都需要處理字符串對象。

這種實現會帶來下列影響:

  • 每次調用 intern() 都要在 JDK 與 JVM 之間的接口,浪費時間。

  • intern() 性能取決于 HashTable 本地實現,落后于高性能 Java 實現,在并發訪問情況下尤其如此。

  • 由于 Java 字符串是本地 VM 結構的引用,它們成為 GC root set 一部分。許多情況下,需要在 GC 暫停時進行額外處理。

  • 這些影響重要嗎?

    4. 吞吐量實驗

    下面是我們設計的一個簡單的實驗,用 HashMap 和 ConcurrentHashMap 實現去重與 intern 操作。JMH 運行得到的結果很好。

    @State(Scope.Benchmark)
    public class StringIntern {
    @Param({"1", "100", "10000", "1000000"})
    private int size;
    private StringInterner str;
    private CHMInterner chm;
    private HMInterner hm;
    @Setuppublic void setup() {
    str = new StringInterner();
    chm = new CHMInterner();
    hm = new HMInterner();
    }
    public static class StringInterner {
    public String intern(String s) {
    return s.intern();
    }
    }
    @Benchmarkpublic void intern(Blackhole bh) {
    for (int c = 0; c < size; c++) {
    bh.consume(str.intern("String" + c));
    }
    }
    public static class CHMInterner {
    private final Mapmap;public CHMInterner() {map = new ConcurrentHashMap<>();
    }public String intern(String s) {
    String exist = map.putIfAbsent(s, s);return (exist == null) ? s : exist;
    }
    }
    @Benchmarkpublic void chm(Blackhole bh) {for (int c = 0; c < size; c++) {
    bh.consume(chm.intern("String" + c));
    }
    }public static class HMInterner {private final Mapmap;public HMInterner() {map = new HashMap<>();
    }public String intern(String s) {
    String exist = map.putIfAbsent(s, s);return (exist == null) ? s : exist;
    }
    }
    @Benchmarkpublic void hm(Blackhole bh) {for (int c = 0; c < size; c++) {
    bh.consume(hm.intern("String" + c));
    }
    }
    }

    上面的測試對大量字符串執行 intern 操作,但實際上只有第一次循環會發生 intern,其他循環都從已有 map 中檢查。size 參數控制執行 intern 字符串數量以及 StringTable 的大小。

    使用 JDK 8u131 運行,結果如下:

    Benchmark (size) Mode Cnt Score Error Units
    StringIntern.chm 1 avgt 25 0.038 ± 0.001 us/op
    StringIntern.chm 100 avgt 25 4.030 ± 0.013 us/op
    StringIntern.chm 10000 avgt 25 516.483 ± 3.638 us/op
    StringIntern.chm 1000000 avgt 25 93588.623 ± 4838.265 us/op
    StringIntern.hm 1 avgt 25 0.028 ± 0.001 us/op
    StringIntern.hm 100 avgt 25 2.982 ± 0.073 us/op
    StringIntern.hm 10000 avgt 25 422.782 ± 1.960 us/op
    StringIntern.hm 1000000 avgt 25 81194.779 ± 4905.934 us/op
    StringIntern.intern 1 avgt 25 0.089 ± 0.001 us/op
    StringIntern.intern 100 avgt 25 9.324 ± 0.096 us/op
    StringIntern.intern 10000 avgt 25 1196.700 ± 141.915 us/op
    StringIntern.intern 1000000 avgt 25 650243.474 ± 36680.057 us/op

    為什么會產生這樣的結果?很明顯 String.intern() 執行的速度更慢!答案是 intern 采用本地實現(“本地 native”并不等于“更好”)。使用 perf record -g 可以清晰地看到:

    - 6.63% 0.00% java [unknown] [k] 0x00000006f8000041
    - 0x6f8000041
    - 6.41% 0x7faedd1ee354
    - 6.41% 0x7faedd170426
    - JVM_InternString
    - 5.82% StringTable::intern
    - 4.85% StringTable::intern
    0.39% java_lang_String::equals
    0.19% Monitor::lock
    + 0.00% StringTable::basic_add
    - 0.97% java_lang_String::as_unicode_string
    resource_allocate_bytes
    0.19% JNIHandleBlock::allocate_handle
    0.19% JNIHandles::make_local

    雖然 JNI 轉換本身開銷很大,但似乎在 StringTable 上也花費了很多時間。通過 -XX:+PrintStringTableStatistics 可以了解到關聯信息:

    StringTable statistics:
    Number of buckets : 60013 = 480104 bytes, avg 8.000
    Number of entries : 1002714 = 24065136 bytes, avg 24.000
    Number of literals : 1002714 = 64192616 bytes, avg 64.019
    Total footprint : = 88737856 bytes
    Average bucket size : 16.708 ;

    HashTable 內部每個 bucket 包含 16 個元素,采用鏈式組合,在上面的結果中都報告“超載”。更糟糕的是 StringTable 不支持調整大小,盡管有些實驗性工作可以支持調整大小,但處于“某些原因”被否決了。通過 -XX:StringTableSize 參數可以讓 -XX:StringTableSize 變大,比如設為10M:

    Benchmark (size) Mode Cnt Score Error Units
    # Default, copied from above
    StringIntern.chm 1 avgt 25 0.038 ± 0.001 us/op
    StringIntern.chm 100 avgt 25 4.030 ± 0.013 us/op
    StringIntern.chm 10000 avgt 25 516.483 ± 3.638 us/op
    StringIntern.chm 1000000 avgt 25 93588.623 ± 4838.265 us/op
    # Default, copied from above
    StringIntern.intern 1 avgt 25 0.089 ± 0.001 us/op
    StringIntern.intern 100 avgt 25 9.324 ± 0.096 us/op
    StringIntern.intern 10000 avgt 25 1196.700 ± 141.915 us/op
    StringIntern.intern 1000000 avgt 25 650243.474 ± 36680.057 us/op
    # StringTableSize = 10M
    StringIntern.intern 1 avgt 5 0.097 ± 0.041 us/op
    StringIntern.intern 100 avgt 5 10.174 ± 5.026 us/op
    StringIntern.intern 10000 avgt 5 1152.387 ± 558.044 us/op
    StringIntern.intern 1000000 avgt 5 130862.190 ± 61200.783 us/op

    但這只是一種權宜之計,你必須事先計劃好。如果盲目地增大 StringTable 會造成浪費。即使充分使用了增大后的 StringTable,本地調用還會同樣增加開銷。

    5. GC 暫停實驗

    本地 StringTable 最大的問題在于,它是 GC root 的一部分。也就是說,需要由垃圾收集器專門對其進行掃描和更新。在 OpenJDK 中,這意味著需要在 GC 暫停期間完成繁雜的工作。實際上,對于 Shenandoah,GC 暫停的時長主要取決于 root set 大小。StringTable 包含1M記錄時,執行結果如下:

    $ ... StringIntern -p size=1000000 --jvmArgs "-XX:+UseShenandoahGC -Xlog:gc+stats -Xmx1g -Xms1g"
    ...
    Initial Mark Pauses (G) = 0.03 s (a = 15667 us) (n = 2) (lvls, us = 15039, 15039, 15039, 15039, 16260)
    Initial Mark Pauses (N) = 0.03 s (a = 15516 us) (n = 2) (lvls, us = 14844, 14844, 14844, 14844, 16088)
    Scan Roots = 0.03 s (a = 15448 us) (n = 2) (lvls, us = 14844, 14844, 14844, 14844, 16018)
    S: Thread Roots = 0.00 s (a = 64 us) (n = 2) (lvls, us = 41, 41, 41, 41, 87)
    S: String Table Roots = 0.03 s (a = 13210 us) (n = 2) (lvls, us = 12695, 12695, 12695, 12695, 13544)
    S: Universe Roots = 0.00 s (a = 2 us) (n = 2) (lvls, us = 2, 2, 2, 2, 2)
    S: JNI Roots = 0.00 s (a = 3 us) (n = 2) (lvls, us = 2, 2, 2, 2, 4)
    S: JNI Weak Roots = 0.00 s (a = 35 us) (n = 2) (lvls, us = 29, 29, 29, 29, 42)
    S: Synchronizer Roots = 0.00 s (a = 0 us) (n = 2) (lvls, us = 0, 0, 0, 0, 0)
    S: Flat Profiler Roots = 0.00 s (a = 0 us) (n = 2) (lvls, us = 0, 0, 0, 0, 0)
    S: Management Roots = 0.00 s (a = 1 us) (n = 2) (lvls, us = 1, 1, 1, 1, 1)
    S: System Dict Roots = 0.00 s (a = 9 us) (n = 2) (lvls, us = 8, 8, 8, 8, 11)
    S: CLDG Roots = 0.00 s (a = 75 us) (n = 2) (lvls, us = 68, 68, 68, 68, 81)
    S: JVMTI Roots = 0.00 s (a = 0 us) (n = 2) (lvls, us = 0, 0, 0, 0, 1)

    從上面的結果可以看到,由于 root set 中加入了更多數據,每次暫停增加了額外13ms。

    這表明,一些 GC 實現只有在重負載情況下才會清理 StringTable。例如,從 JVM 的角度來看,如果類沒有卸載(unloaded),清理 StringTable 是沒有意義的。只有已經加載的類是 intern 字符串的主要來源。以 G1 和 CMS 為例,上面的測試負載會產生有趣的結果:

    public class InternMuch {
    public static void main(String... args) {
    for (int c = 0; c < 1_000_000_000; c++) {
    String s = "" + c + "root";
    s.intern();
    }
    }
    }

    使用 CMS 運行:

    $ java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g -verbose:gc -XX:StringTableSize=6661443 InternMuch
    GC(7) Pause Young (Allocation Failure) 349M->349M(989M) 357.485ms
    GC(8) Pause Initial Mark 354M->354M(989M) 3.605ms
    GC(8) Concurrent Mark
    GC(8) Concurrent Mark 1.711ms
    GC(8) Concurrent Preclean
    GC(8) Concurrent Preclean 0.523ms
    GC(8) Concurrent Abortable Preclean
    GC(8) Concurrent Abortable Preclean 935.176ms
    GC(8) Pause Remark 512M->512M(989M) 512.290ms
    GC(8) Concurrent Sweep
    GC(8) Concurrent Sweep 310.167ms
    GC(8) Concurrent Reset
    GC(8) Concurrent Reset 0.404ms
    GC(9) Pause Young (Allocation Failure) 349M->349M(989M) 369.925ms

    目前為止運行結果還算不錯,遍歷過載的 StringTable 需要耗費一段時間。但如果使用 -XX:-ClassUnloading 屏蔽類卸載,運行結果會變得糟糕。這實際上在常規 GC 循環中禁用 StringTable 清理!可以預測接下來的運行結果:

    $ java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g -verbose:gc -XX:-ClassUnloading -XX:StringTableSize=6661443 InternMuch
    GC(11) Pause Young (Allocation Failure) 273M->308M(989M) 338.999ms
    GC(12) Pause Initial Mark 314M->314M(989M) 66.586ms
    GC(12) Concurrent Mark
    GC(12) Concurrent Mark 175.625ms
    GC(12) Concurrent Preclean
    GC(12) Concurrent Preclean 0.539ms
    GC(12) Concurrent Abortable Preclean
    GC(12) Concurrent Abortable Preclean 2549.523ms
    GC(12) Pause Remark 696M->696M(989M) 133.920ms
    GC(12) Concurrent Sweep
    GC(12) Concurrent Sweep 175.949ms
    GC(12) Concurrent Reset
    GC(12) Concurrent Reset 0.463ms
    GC(14) Pause Full (Allocation Failure) 859M->0M(989M) 1541.465ms GC(13) Pause Young (Allocation Failure) 859M->0M(989M) 1541.515ms

    看到了完整的 STW(Stop The World 萬物靜止)GC。CMS 中包含了 ExplicitGCInvokesConcurrentAndUnloadsClasses,假設用戶不時調用 System.gc() 能夠有效緩解這個問題。

    6. 觀察

    這里我們只討論?intern?或去重方法的實現,滿足改進內存占用、底層優化或其他模糊的需求。這些需求可以另行討論,挑戰或接納。更多有關 Java String 的討論,推薦我的演講?“java.lang.String 問答”。

    String.intern() 為 OpenJDK 提供了訪問本地 JVM StringTable 方法。使用 intern 時需要關注吞吐量、內存占用和暫停時間,這些都有可能讓用戶等待。人們很容易低估這些警告帶來的影響。手工實現去重或 intern 方法運行更加可靠。因為它們工作在 Java 端,只是普通的 Java 對象,可以更好地設置和重新調整大小。而且在不再需要時也可以完全丟棄。GC 輔助的字符串去重的確更好地減輕了負擔。

    在實際項目中,從性能開銷的熱點路徑上去除 String.intern() 或者采用手工實現去重方法有助于性能優化。請不要沒有深思熟慮就使用 String.intern(),好嗎?

    推薦閱讀

    (點擊標題可跳轉閱讀)

    深入理解 Java String.intern() 內存模型

    String 常量池和 String#intern()

    深入解析 String.intern

    看完本文有收獲?請轉發分享給更多人

    關注「ImportNew」,提升Java技能

    好文章,我在看??

    總結

    以上是生活随笔為你收集整理的string会传null吗_JVM 解剖公园(10): String.intern()的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。