案例出于《深入理解Java虚拟机》第二版
场景
一个后台RPC服务器,使用64位虚拟机,内存配置-Xms 4G -Xmx 8G -Xmn 1G,使用ParNew+CMS的收集器组合。
问题
- 平时对外服务的Minor GC时间约在30毫秒以内,完全可以接收。
- 业务需要每10分钟加载一个约80MB的数据文件到内存进行数据分析,这些数据会在内存中形成超过100万个HashMap<Long,Long>Entry,在这段时间里Minor GC就会造成500毫秒的停顿,就接收不了。
分析
- 平时Minor GC时间很短,原因为新生代的绝大部分对象都可清除,在Minor GC之后Eden和Survivor基本上出于完全空闲状态。
- 分析数据文件期间,800MB的Eden空间很快被填满从而引发GC,而Minor GC之后,新生代中绝大部分对象依然是存活的。
- ParNew收集器使用的是复制算法,这算法的高效建立在大部分对象都“朝生夕死”的特性上,若存活对象过多,把这些对象复制到Survivor并维持这些对象引用的正确就成为一个沉重的负担,因此导致GC暂停时间明显变长。
解决方案
- 若不修改程序,仅从GC调优的角度去解决这个问题,可考虑将Survivor空间去掉(加入参数-XX:SurvivorRatio=65536、-XX:MaxTenuringThreshold=0或-XX:+AlwaysTenure),让新生代中存活的对象在第一次Minor GC后立即进入老年代,等到Major GC时再清理它们。【治标方案,副作用很大】。
- 修改程序,因为这里的问题产生的根本原因是用HashMap<Long,Long>结构来存储数据文件空间效率太低【治本方案】。
空间效率
在HashMap<Long,Long>结构中,只有Key和Value所存放的两个长整型数据是有效数据,共16B(2X8B)。这两个长整数数据包装成java.lang.Long对象之后,就分别具有8B的MarkWord、8B的Klass指针,在加8B存储数据的long值。在两个Long对象组成Map.Entry之后,又多了16B的对象头,然后一个8B的next字段和4B的int型的hash字段,为了对齐,还必须添加4B的空白填充,最后还有HashMap中对这个Entry的8B的引用,这样增加两个长整数型数字,实际耗费的内存为(Long(24B)X2)+Entry(32B)+HashMap Ref(8B)=88B,空间效率为16B/88B=18%,实在太低了。
网友评论