JVM:32G以上的堆會發生什么

jopen 9年前發布 | 29K 次閱讀 JVM Java開發

這篇短文主要是想告訴你如果給Oracle JVM配置超過32G的堆會發生什么事情。默認情況下,堆大小在32G以下的話JVM中的引用會占用4個字節。這是JVM在啟動的時候就已經決定了的。如果你去掉了-XX:-UseCompressedOops選項的話,當然也可以在較小的堆上使用8字節的引用(但在生產系統中這么做是毫無意義的!)。

一旦堆超過了32G,你就進入到64位的世界里了,因此對象引用就只能是8字節而非4字節了。正如Scott Oaks在他的Java性能:終極指南一書中所說的(234到236頁,這里有我對該書的一個評價),Java 程序的堆中平均會有20%的空間是被對象引用占據了。也就是說,如果堆的配置是介于Xmx32G到Xmx37G——Xmx38G之間的話,實際上是減少了應用程序的可用堆的大小(當然了,具體的數字要取決于你的程序)。對于很多人而言,增加了額外的內存是為了能讓程序可以多處理一些數據,而這樣的結果會令他感到意外。

測試——生成LinkedList

我決定測試一下最壞的場景——生成一個值遞增的LinkedList。這個測試非常有意思:看一下將2億個Integer插入到LinkedList中需要多大的堆空間。這個工作就留給讀者來完成了:-)

測試的代碼非常簡單:

public class Mem32Test {
    public static void main(String[] args) {
        List<Integer> lst = new LinkedList<>();
        int i = 0;
        while ( true )
        {
            lst.add( new Integer( i++ ) );
            if ( ( i & 0xFFFF ) == 0 )
                System.out.println( i ); //shows where you are <img src="https://simg.open-open.com/show/b2984729c3b6cdc07508b88b5c0a4d1e.gif" alt=":)" class="wp-smiley" />
            if ( i == System.currentTimeMillis() )
                break; //otherwise will not compile
        }
        System.out.println( lst.size() ); //needed to avoid dead code optimizations
    }
}

你可以結合Xmx以及verbose:gc(或者是-XX:+PrintGCDetails)選項來運行這個程序。同時還得查看下GC的日志來確認下內存何時會被用完(距離真正拋出OOM還需要相當長的一段時間)。

首先,我發現JVM切換到64位引用的一個確切的臨界點是——Xmx32767M(很奇怪,正好比32G要少1M)。除此之外我還發現應用程序的可用內存與堆的變化并不是線性增長的。而是階段性的增長(你可以看下Xmx49200M和49500M之間所發生的現象)——這點我想再深入地探討一下。

測試結果

||LinkedList中元素的個數||堆大小|| ||666,697,728 ||Xmx32700M|| ||667,287,552 ||Xmx32730M|| ||667,680,768 ||Xmx32750M|| ||667,877,376 ||Xmx32760M|| ||668,008,448 ||Xmx32764M|| ||668,139,520 ||Xmx32765M|| ||668,008,448 ||Xmx32766M|| ||422,510,592 ||Xmx32767M|| ||429,391,872 ||Xmx33700M|| ||535,166,976 ||Xmx42000M|| ||639,041,536 ||Xmx48700M|| ||643,039,232 ||Xmx49200M|| ||731,578,368 ||Xmx49500M|| ||734,658,560 ||Xmx49700M|| ||1,442,119,680 ||Xmx110000M||

不難看出,列表元素的個數在Xmx32767M也就是切換到64位引用的時候開始戲劇性地從6億下降到了4億。

我們來看下為什么插入到LinkedList中的元素的數量會發生銳減。JDK中的LinkedList是一個雙向鏈表。因此,每個Node對象除了包含數據以外(當然了,只是個引用),還有prev及next引用。

在32位模式下,每個Java對象會包含12字節的對象頭,然后才是對象本身的字段。每個對象所占用的內存還會按8個字節來對齊。因此,32位模式下一個Node對象會占用12+4*3=24個字節。一個Integer對象需要12+4=16字節(這兩種情況都不需要補齊填充)。

但是,一旦切換到了64位之后,LinkedList中的每個元素的大小會從40字節漲到64字節。

內存調優的一些技巧

正如我前面所說的,JVM使用超過32G堆的話就意味著有一個不小的性能損耗。除了增加應用程序的內存使用量以外,JVM的垃圾回收器還要去回收這些對象(你可以添加-XX:+PrintGCDetails選項來看下對程序的GC所造成的影響)。

對于那些沒有調優過的應用程序,我這里列舉出了一些簡單的技巧可以用來減少它的內存使用量(千萬不要覺得這些建議沒什么,某些情況下可以節省的內存相當可觀):

  • 應用程序中可能包含許多內容一樣的字符串對象。如果使用的是Java 7及更新的版本,可以考慮下字符串內聯——這是消除冗余字符串的終極武器,但必須得謹慎使用——只有那些生命周期適中或較長的字符串才應該進行內聯,因為它們更有可能會出現冗余。如果你用的是Java 8 update 20以后的版本,可以嘗試使用字符串去重——JVM會自己去處理字符串冗余的問題(使用這個特性必須得啟用G1垃圾回收器)。
  • 如果堆中有大量的數值型包裝類譬如Integer或者Double的話,最好是將它們存儲在集合里。現在都已經是2015年了,沒有什么理由拒絕使用原始集合(Primitive Collection)了。最近我寫了篇文章介紹了下不同的原始集合庫實現的哈希表的概況。你還可以看一下我之前關于Trove的一篇文章
  • 最后,可以看一下我之前寫過的有關內存消耗和節省內存的一系列文章(第1篇第2篇第3篇第4篇)。

總結

  • 如果應用程序的堆超過32G的話需要謹慎對待(從低于32G升級到32G以上)——JVM會切換到64位的對象引用,也就是說應用程序的可用堆的空間會減小。解決方法就是不要從32G加起,而是直接加到37~38G以上。這個灰色區域具體是多少取決于你的應用程序——對象的平均大小較大的話,損耗就要少一些。

  • 更明智的做法或許就是不要使用太大的堆,而是將使用的內存限制在32G以內。看一下我的這幾篇文章:字符串內聯字符串去重哈希表及其它原始集合Trove)。

原創文章轉載請注明出處:JVM:32G以上的堆會發生什么

英文原文鏈接

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!