JVM:32G以上的堆会发生什么

这篇短文主要是想告诉你若是给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中须要多大的堆空间。这个工做就留给读者来完成了:-) spa

测试的代码很是简单:
public class Mem32Test {
public static void main(String[] args) {
List 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 
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之间所发生的现象)——这点我想再深刻地探讨一下。 code

测试结果

||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字节。 it

内存调优的一些技巧

正如我前面所说的,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)。

相关文章
相关标签/搜索