1. 程式人生 > 實用技巧 >JVM系列之:String.intern和stringTable

JVM系列之:String.intern和stringTable

目錄

簡介

StringTable是什麼?它和String.intern有什麼關係呢?在字串物件的建立過程中,StringTable有起到了什麼作用呢?

一切的答案都在本文中,快來看看吧。

intern簡介

intern是String類中的一個native方法,所以它底層是用c++來實現的。感興趣的同學可以去檢視下JVM的原始碼瞭解更多的內容。

這裡我們主要談一下intern的作用。

intern返回的是這個String所代表的物件,怎麼理解呢?

String class維護了一個私有的String pool, 這個String pool也叫StringTable,中文名字叫做字串常量池。

當我們呼叫intern方法的時候,如果這個StringTable中已經包含了一個相同的String物件(根據equals(Object)方法來判斷兩個String物件是否相等),那麼將會直接返回儲存在這個StringTable中的String。

如果StringTable中沒有相同的物件,那麼這個String物件將會被加入StringTable,並返回這個String物件的引用。

所以,當且僅當 s.equals(t) 的時候s.intern() == t.intern()。

intern和字串字面量常量

我們知道在類檔案被編譯成class檔案時,每個class檔案都有一個常量池,常量池中存了些什麼東西呢?

字串常量,類和介面名字,欄位名,和其他一些在class中引用的常量。

看一個非常簡單的java類:

public class SimpleString {
    public String site="www.flydean.com";
}

然後看一下編譯出來的class檔案中的Constant Pool:

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = String             #8             // www.flydean.com
   #8 = Utf8               www.flydean.com
   #9 = Fieldref           #10.#11        // com/flydean/SimpleString.site:Ljava/lang/String;
  #10 = Class              #12            // com/flydean/SimpleString
  #11 = NameAndType        #13:#14        // site:Ljava/lang/String;
  #12 = Utf8               com/flydean/SimpleString
  #13 = Utf8               site
  #14 = Utf8               Ljava/lang/String;
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcom/flydean/SimpleString;
  #20 = Utf8               SourceFile
  #21 = Utf8               SimpleString.java

上面的結果,我們可以看到class常量池中的index 7存放了一個字串,這個字串的實際內容存放在index 8中,是一個變種的Utf8的編碼。

   #7 = String             #8             // www.flydean.com
   #8 = Utf8               www.flydean.com

好了,現在問題來了,class檔案中的常量池在執行時需要轉換成為JVM能夠識別的執行時常量池,這個執行時的常量池和StringTable和intern有什麼關係呢?

在java物件的例項化過程中,所有的字串字面量都會在例項化的時候自動呼叫intern方法。

如果是第一次呼叫,則會建立新的String物件,存放在String Table中,並返回該String物件的引用。

分析intern返回的String物件

從上面的圖中,我們也可以出來String Table中儲存的是一個String物件,它和普通的String物件沒有什麼區別,也分為物件頭,底層的byte陣列引用,int hash值等。

如果你不相信,可以使用JOL來進行分析:

log.info("{}", ClassLayout.parseInstance("www.flydean.com".intern()).toPrintable());

看下輸出結果:

INFO com.flydean.StringInternJOL - java.lang.String object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           77 1a 06 00 (01110111 00011010 00000110 00000000) (399991)
     12     4    byte[] String.value                              [119, 119, 119, 46, 102, 108, 121, 100, 101, 97, 110, 46, 99, 111, 109]
     16     4       int String.hash                               0
     20     1      byte String.coder                              0
     21     1   boolean String.hashIsZero                         false
     22     2           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total

分析實際的問題

有了上面的知識,讓我們分析一下下面的實際問題吧:

        String a =new String(new char[]{'a','b','c'});
        String b = a.intern();
        System.out.println(a == b);

        String x =new String("def");
        String y = x.intern();
        System.out.println(x == y);

兩個很簡單的例子,答案是什麼呢? 答案是true和false。

第一個例子按照上面的原理很好理解,在構建String a的時候,String table中並沒有”abc“這個字串例項。所以intern方法會將該物件新增到String table中,並返回該物件的引用。

所以a和b其實是一個物件,返回true。

那麼第二個例子呢?初始化String的時候,不是也沒有”def“這個字串嗎?為什麼回返回false呢?

還記得我們上面一個小節分析的嗎?所有的字串字面量在初始化的時候會預設呼叫intern方法。

也就是說”def“在初始化的時候,已經呼叫了一次intern了,這個時候String table中已經有”def“這個String了。

所以x和y是兩個不同的物件,返回的是false。

注意,上面的例子是在JDK7+之後執行的,如果你是在JDK6中執行,那麼得到的結果都是false。

JDK6和JDK7有什麼不同呢?

在JDK6中,StringTable是存放在方法區中的,而方法區是放在永久代中的。每次呼叫intern方法,如果String Table中不存在該String物件,則會將該String物件進行一次拷貝,並返回拷貝後String物件的引用。

因為做了一次拷貝,所以引用的不是同一個物件了。結果為false。

在JDK7之後,StringTable已經被轉移到了java Heap中了,呼叫intern方法的時候,StringTable可以直接將該String物件加入StringTable,從而指向的是同一個物件。

G1中的去重功能

如果頻繁的進行String的複製,實際上是非常消耗記憶體空間的。所以在G1垃圾回收器中,可以使用下面的:

-XX:+UseStringDeduplication

來開啟String的去重功能。

我們還記得String物件的底層結構吧,就是一個byte[]陣列,String去重的原理就是讓多個字串物件底層的byte陣列指向同一個地方。從而節省記憶體。

我們可以通過使用:

-XX:+PrintStringTableStatistics

引數來檢視StringTable的大小。並通過:

-XX:StringTableSizen=n

來指定StringTable的大小。

總結

本文講了String.intern和String table的關係,如果有什麼錯誤或者遺漏的地方,歡迎大家留言給我!

本文作者:flydean程式那些事

本文連結:http://www.flydean.com/jvm-string-intern/

本文來源:flydean的部落格

歡迎關注我的公眾號:程式那些事,更多精彩等著您!