使用String的intern方法節省內存

Attila Szegedis 在他講述 JVM 知識的文檔中一直強調,清楚知道內存中存儲的數據量是很是重要的。我一開始感到十分驚訝,由於通常狀況下,在企業開發中並非常常須要關注對象的大小。他對此給出了 Twitter 的一個例子。java

先思考一個內存佔用的問題:字符串 「Hello World」 會佔用多少字節內存?數據庫

答案:在 32 位虛擬機上是 62 字節,在 64 位虛擬機上是 86 字節。數組

分別爲 8/16 (字符串的對象頭) + 11 * 2 (字符) + [8/16 (字符數組的對象頭) + 4 (數組長度),加上字節對齊所需的填充,共爲 16/24 字節] + 4 (偏移) + 4 (偏移長度) + 4 (哈希碼) + 4/8 (指向字符數組的引用)【在 64 位虛擬機上,String 對象的內存佔用會由於字節對齊而填充爲 40 字節】bash

假如如今有許多推特消息的地點信息須要存儲。app

地點信息對應的類也許會像這樣實現。ide

class Location {
    String city;
    String region;
    String countryCode;
    double long;
    double lat;
}

很明顯的一點,當加載地點信息時,其實是加載了許多的字符串,而以 Twitter 的用戶規模,確定有許多字符串是重複的。按照 Attila 的說法,即便是 32 GB 大小的堆,也放不下全部數據。如今的問題是:可以經過什麼方法來減小內存的佔用,從而全部數據都能被加載進內存中?工具

咱們先來看兩個解決方案,它們二者是相輔相成的。oop

Attilas 提出的方法

能夠看出,在地點類所存儲的信息裏,總有一部分是重複的,因此能夠很簡單地以非技術手段解決這個問題。咱們能夠把地點類拆分紅下面的兩個類:性能

class SharedLocation {
    String city;
    String region;
    String countryCode;
}

class Location {
    SharedLocation sharedLocation;
    double long;
    double lat;
}


由於不多有城市會改變所在的地區和國家,因此這個簡單的方法可以起做用。這些字符串的組合是惟一的。這種方法也很靈活,因此也可以進行處理上面所提惟一性不知足的狀況。特別是對於用戶輸入的地點信息,這點顯得更加劇要。這樣子的話,若是多條 Twitter 消息是來自同一個地點,例如 「Solingen, NRW, DE」 (DE 指德國,NRW 爲德國北萊茵邦,Solingen 與以後的 Ratingen 爲德國城市名,譯者注)的話,也只須要使用一個 SharedLocation 對象。優化

可是,其它的信息,如 「Ratingen, NRW, DE」,仍然須要在內存中存儲額外的 3 個字符串,而不是單獨的一個 「Ratingen」。上面的方法可使內存中的數據總量降低到 20 GB。

使用 String intern() 方法

可是在不想或者不可以修改數據類的狀況下怎麼辦呢?又或者是 Twitter 的那些人並無 20 GB 大小的堆。這種狀況下可使用 intern() 方法,它可以使內存中的不一樣字符串都只有一個實例對象。對於 intern() 方法,存在着許多誤解。許多人會問道,intern() 方法是否是能夠在字符串進行等價比較時,提升效率,畢竟在使用 intern 時,相等的字符串實際上都是同一個對象。確實如此,intern 能夠作到這一點。(對於其餘的任何對象來講,這個規律也是成立的。)(在進行 equals 比較時,若是兩個對象是同一個的話,在 「==」 比較時就能得出結果,因此能夠提升 equals 比較的效率,而無論比較的對象是字符串仍是其餘類型的對象,譯者注。)

// java.lang.String
public boolean equals(Object anObject) {
  if (this == anObject) {
    return true;
  }
  //...
}


但在等價比較上的性能提高並非應該使用 intern 的理由。實際上,intern 的目的在於複用字符串對象以節省內存。

在明確知道一個字符串會出現屢次時才使用 intern(),而且只用它來節省內存。

使用 intern() 方法的效率,取決於重複的字符串與惟一的字符串的比值。另外,還要看在產生字符串對象的地方,代碼是否是容易進行修改。

intern 原理

intern() 方法須要傳入一個字符串對象(已存在於堆上),而後檢查 StringTable 裏是否是已經有一個相同的拷貝。StringTable 能夠看做是一個 HashSet,它將字符串分配在永久代上。StringTable 存在的惟一目的就是維護全部存活的字符串的一個對象。若是在 StringTable 裏找到了可以找到所傳入的字符串對象,那就直接返回它,不然,把它加入 StringTable :

// OpenJDK 6 code
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  JVMWrapper("JVM_InternString");
  JvmtiVMObjectAllocEventCollector oam;
  if (str == NULL) return NULL;
  oop string = JNIHandles::resolve_non_null(str);
  oop result = StringTable::intern(string, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END

oop StringTable::intern(Handle string_or_null, jchar* name,int len, TRAPS) {
  unsigned int hashValue = hash_string(name, len);
  int index = the_table()->hash_to_index(hashValue);
  oop string = the_table()->lookup(index, name, len, hashValue);
  // Found
  if (string != NULL) return string;
  // Otherwise, add to symbol to table
  return the_table()->basic_add(index, string_or_null, name, len, hashValue, CHECK_NULL);
}

所以,相同字符串的對象只會有一個。

intern 用法

intern 適合用在須要讀取數據並將這些對象或者字符串歸入一個更大範圍做用域的狀況。須要注意的是,硬編碼在代碼中的字符串(例如常量等等)都會被編譯器自動的執行 intern 操做。

看一個例子:

String city = resultSet.getString(1);
String region = resultSet.getString(2);
String countryCode = resultSet.getString(3);
double city = resultSet.getDouble(4);
double city = resultSet.getDouble(5);

Location location = new Location(city.intern(), region.intern(), countryCode.intern(), long, lat);
allLocations.add(location);

全部新建立的地點對象都會使用 intern 獲得的字符串。而從數據庫讀取到的臨時字符串則會被垃圾回收。

如何肯定 intern 的效率

最好的方法是對整個堆執行一次堆轉儲。堆轉儲也會在發生 OutOfMemoryError 時執行。

在 MAT (內存分析工具,譯者注)中打開轉儲文件,而後選擇 java.lang.String,依次點擊「Java Basics」、「Group By Value」。

根據堆的大小,上面的操做可能耗費比較長的時間。最後能夠看到類型這樣的結果。按 「Retained Heap」 或者是 「Objects」 列進行排序,能夠發現一些有趣的東西:

從這快照中咱們能夠看到,空的字符串佔用了大量的內存!兩百萬個空字符串對象佔用了總共 130 MB 的空間。另外能夠看到一部分被加載的 JavaScript 腳本,一些做爲鍵的字符串,它們被用於定位。另外,還有一些與業務邏輯相關的字符串。

這些與業務邏輯相關的字符串是最容易進行 intern 操做的,由於咱們清楚地知道它們是在什麼地方被加載進內存的。對於其餘字符串,能夠經過 「Merge shortest Path to GC Root」 選項來找到它們被存儲的位置,這個信息也許可以幫助咱們找到該使用 intern 的地方。

intern 的利弊

既然 intern() 方法有這些好處,爲何不常用呢?緣由在於它會下降代碼效率。下面給出一個例子:

private static final int MAX = 40000000;

public static void main(String[] args) throws Exception {
    long t = System.currentTimeMillis();
    String[] arr = new String[MAX];
    for (int i = 0; i < MAX; i++) {
        arr[i] = new String(DB_DATA[i % 10]);
        // and: arr[i] = new String(DB_DATA[i % 10]).intern();
    }
    System.out.println((System.currentTimeMillis() - t) + "ms");
    System.gc();
    System.out.println(arr[0]);
}

代碼中使用了字符串數組來維護到字符串對象的強引用,另外咱們還打印了數組的第一個元素來避免數組因爲代碼優化而將數組給銷燬了。接着從數據庫加載 10 個不一樣的字符串,但在這裏我使用了 new String() 來建立一個臨時的字符串,這和從數據庫裏讀是同樣的。最後咱們調用了系統的 GC() 方法,這樣就能排除其餘不相關對象的影響,保證結果的正確。 在 64 位,8 G 內存,i5-2520M 處理器的 Windows 系統上運行上面的代碼, 環境爲 JDK 1.6.0_27,指定虛擬機參數 -XX:+PrintGCDetails -Xmx6G -Xmn3G 記錄垃圾回收日誌。結果以下:

沒有使用 intern() 方法的結果:

1519ms
[GC [PSYoungGen: 2359296K->393210K(2752512K)] 2359296K->2348002K(4707456K), 5.4071058 secs] [Times: user=8.84 sys=1.00, real=5.40 secs]
[Full GC (System) [PSYoungGen: 393210K->392902K(2752512K)] [PSOldGen: 1954792K->1954823K(1954944K)] 2348002K->2347726K(4707456K) [PSPermGen: 2707K->2707K(21248K)], 5.3242785 secs] [Times: user=3.71 sys=0.20, real=5.32 secs]
DE
Heap
 PSYoungGen      total 2752512K, used 440088K [0x0000000740000000, 0x0000000800000000, 0x0000000800000000)
  eden space 2359296K, 18% used [0x0000000740000000,0x000000075adc6360,0x00000007d0000000)
  from space 393216K, 0% used [0x00000007d0000000,0x00000007d0000000,0x00000007e8000000)
  to   space 393216K, 0% used [0x00000007e8000000,0x00000007e8000000,0x0000000800000000)
 PSOldGen        total 1954944K, used 1954823K [0x0000000680000000, 0x00000006f7520000, 0x0000000740000000)
  object space 1954944K, 99% used [0x0000000680000000,0x00000006f7501fd8,0x00000006f7520000)
 PSPermGen       total 21248K, used 2724K [0x000000067ae00000, 0x000000067c2c0000, 0x0000000680000000)
  object space 21248K, 12% used [0x000000067ae00000,0x000000067b0a93e0,0x000000067c2c0000)

使用了 intern() 方法的結果:

1519ms
[GC [PSYoungGen: 2359296K->393210K(2752512K)] 2359296K->2348002K(4707456K), 5.4071058 secs] [Times: user=8.84 sys=1.00, real=5.40 secs] 
[Full GC (System) [PSYoungGen: 393210K->392902K(2752512K)] [PSOldGen: 1954792K->1954823K(1954944K)] 2348002K->2347726K(4707456K) [PSPermGen: 2707K->2707K(21248K)], 5.3242785 secs] [Times: user=3.71 sys=0.20, real=5.32 secs] 
DE
Heap
 PSYoungGen      total 2752512K, used 440088K [0x0000000740000000, 0x0000000800000000, 0x0000000800000000)
  eden space 2359296K, 18% used [0x0000000740000000,0x000000075adc6360,0x00000007d0000000)
  from space 393216K, 0% used [0x00000007d0000000,0x00000007d0000000,0x00000007e8000000)
  to   space 393216K, 0% used [0x00000007e8000000,0x00000007e8000000,0x0000000800000000)
 PSOldGen        total 1954944K, used 1954823K [0x0000000680000000, 0x00000006f7520000, 0x0000000740000000)
  object space 1954944K, 99% used [0x0000000680000000,0x00000006f7501fd8,0x00000006f7520000)
 PSPermGen       total 21248K, used 2724K [0x000000067ae00000, 0x000000067c2c0000, 0x0000000680000000)
  object space 21248K, 12% used [0x000000067ae00000,0x000000067b0a93e0,0x000000067c2c0000)

能夠看到結果差異十分的大。在使用 intern() 方法的時候,程序耗時多了 3 秒,但節省了很大一塊內存。使用 intern() 方法的程序佔用了 253472K(250M) 內存,而不使用的佔用了 2397635K (2.4G)。從這些能夠看出使用 intern 的利弊。

相關文章
相關標籤/搜索