歡迎光臨
每天分享高質量文章

聊聊Java String.intern 背後你不知道的知識

導讀:String.intern是一個JDK中的常用方法,通常用於快取字串,最佳化記憶體使用,然而頻繁使用該方法也會導致別的問題,本文從該方法的實現入手,深入分析了可能出現的問題和解決方案。

 

Java的 String類有個有意思的public方法:

public String intern()

傳回標準表示的字串物件。String類維護私有字串池。

呼叫此方法時,如果字串池已經包含等於此字串物件的字串(透過equals方法確定),則傳回池中的字串。 否則,將此String物件新增到池中,並傳回對此String物件的取用。

 

這個功能為String提供了字串池,我們可以使用它來最佳化記憶體。 但是,這有一個缺點:在OpenJDK中,String.intern()是本地方法,它實際上呼叫了JVM的相關方法來實現該功能。這樣實現的原因是,當VM和JDK程式碼必須就特定String物件的標識達成一致時,String interning就必須是JDK-VM介面的一部分。


這樣的實現意味著:

  1. 您需要在每個intern呼叫使用JDK-JVM介面,這會浪費CPU。
  2. 效能受本地HashTable實現的影響,可能落後於高效能Java版本,特別是在併發訪問的情況下。
  3. 由於Java Strings是來自VM的取用,因此它們成為GC root set的一部分。 在許多情況下,這需要在GC停頓期間執行額外的工作。

 

吞吐量實驗

 

我們可以構建簡單的實驗來說明問題。 使用HashMap和ConcurrentHashMap實現intern方法,這為我們提供了一個非常好的JMH基準:

該測試試圖在很多字串上執行intern方法,但實際的intern僅在第一次遍歷迴圈時發生,之後只訪問map中的字串。 size引數用於控制我們intern的字串數量,從而限制我們正在處理的字串表大小。 對於intern來說,通常都這樣使用。

使用JDK 8u131執行它:

可以看出 String.intern()明顯更慢。慢的原因在於本地實現,這在perf record -g中清晰可見:

雖然JNI轉換成本相當高,但似乎在StringTable實現上也花了相當多的時間。 使用 -XX:+PrintStringTableStatistics,將輸出如下內容:

註意最後一行,平均每個bucket 16個元素表示已經過載。 更糟糕的是,字串表不可調整大小(雖然有實驗工作使它們可以調整大小,但是因為“其他原因”而被移除)。 透過設定更大的-XX:StringTableSize可能會減輕該問題:

然而這隻能暫時緩解問題,因為你必須提前做好規劃。 如果盲目地將String表大小設定為較大值,並且不使用它,則會浪費記憶體。 即使您使用很大的StringTable,JNI本地呼叫仍然會消耗CPU。

 

GC停頓實驗

 

本地字串表最大問題在於它是GC root的一部分。也就是說,它應該需要垃圾收集器進行特殊掃描/更新。 在OpenJDK中,這意味著在暫停期間額外工作。 實際上,對於Shenandoah(譯者註:對於ZGC也如此),暫停主要依賴於GC root set大小,在String表中存在1M記錄會導致以下結果:

因為我們在root set中添加了內容,每次暫停會增加13ms。

某些GC實現僅在完成重要操作時執行String表清理。 比如,如果不進行解除安裝類,從JVM角度來看清理String表是沒有意義的(因為載入的類是intern字串的主要來源)。 因此,此工作負載在G1和CMS中會也會表現出有趣的行為:

用CMS跑一遍:

看起來結果還可以。 遍歷多載的字串表需要一段時間。 蛋疼的事情會在使用-XX:-ClassUnloading禁用類解除安裝後發生。你猜猜接下來會發生什麼:

FULL GC! 對於CMS,假設使用者會呼叫System.gc(),使用ExplicitGCInvokesConcurrentAndUnloadsClasses會緩解這一情況。

 

意見

 

在假設改進記憶體佔用空間或低階==最佳化的情況下,我們討論了實現intern的方法。有關Java String的更多詳細資訊,可以參考我的演講“java.lang.String Catechism”。

對於OpenJDK,String.intern()是本機JVM字串表的代理,使用它需要註意:吞吐量,記憶體佔用,暫停時間等問題。 很容易低估這些問題的影響。 手動控制的intern工作更加可靠,因為它們在Java端工作,只是普通Java物件,通常更容易調整大小,並且在不再需要時也可以完全丟棄。 GC輔助字串去重覆資料(http://openjdk.java.net/jeps/192)確實可以減少很多問題。

幾乎在在我們進行每個專案中,從熱路徑中刪除String.intern(),或者用手動方式替代它,都有很大的效能提升。 不要無腦使用String.intern(),好嗎?

 

原文地址:

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

本文作者Aleksey Shipilёv,由方圓翻譯。轉載本文請註明出處,歡迎更多小夥伴加入翻譯及投稿文章的行列,詳情請戳公眾號選單「聯絡我們」。

    贊(0)

    分享創造快樂