JVM初探使用堆外内存减少FullGC.docx
《JVM初探使用堆外内存减少FullGC.docx》由会员分享,可在线阅读,更多相关《JVM初探使用堆外内存减少FullGC.docx(18页珍藏版)》请在冰豆网上搜索。
JVM初探使用堆外内存减少FullGC
JVM初探-使用堆外内存减少FullGC
引入
这个idea最初来源于TaobaoJVM对OpenJDK定制开发的GCIH部分(详见撒迦的分享-JVM定制改进@淘宝),其中GCIH就是将CMSOldHeap区的一部分划分出来,这部分内存虽然还在堆内,但已不被GC所管理.将长生命周期Java对象放在Java堆外,GC不能管理GCIH内Java对象(GCInvisibleHeap):
这样做有两方面的好处:
减少GC管理内存:
由于GCIH会从Old区“切出”一块,因此导致GC管理区域变小,可以明显降低GC工作量,提高GC效率,降低FullGCSTW时间(且由于这部分内存仍属于堆,因此其访问方式/速度不变-不必付出序列化/反序列化的开销).
GCIH内容进程间共享:
由于这部分区域不再是JVM运行时数据的一部分,因此GCIH内的对象可供对个JVM实例所共享(如一台Server跑多个MR-Job可共享同一份Cache数据),这样一台Server也就可以跑更多的VM实例.
(实际测试数据/图示可下载撒迦分享PPT).
但是大部分的互联公司不能像阿里这样可以有专门的工程师针对自己的业务特点定制JVM,因此我们只能”眼馋”GCIH带来的性能提升却无法”享用”.但通用的JVM开放了接口可直接向操作系统申请堆外内存(ByteBufferorUnsafe),而这部分内存也是GC所顾及不到的,因此我们可用JVM堆外内存来模拟GCIH的功能(但相比GCIH不足的是需要付出serialize/deserialize的开销).
JVM堆外内存
在JVM初探-JVM内存模型一文中介绍的Java运行时数据区域中是找不到堆外内存区域的:
因为它并不是JVM运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,这部分内存区域直接被操作系统管理.
在JDK1.4以前,对这部分内存访问没有光明正大的做法:
只能通过反射拿到Unsafe类,然后调用allocateMemory()/freeMemory()来申请/释放这块内存.1.4开始新加入了NIO,它引入了一种基于Channel与Buffer的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,ByteBuffer提供了如下常用方法来跟堆外内存打交道:
下面我们就用通用的JDKAPI来使用堆外内存来实现一个localcache.
示例1.:
使用JDKAPI实现堆外Cache
注:
主要逻辑都集中在方法invoke()内,而AbstractAppInvoker是一个自定义的性能测试框架,在后面会有详细的介绍.
/**
*@authorjifang
*@since2016/12/31下午6:
05.
*/
publicclassDirectByteBufferAppextendsAbstractAppInvoker{
@Test
@Override
publicvoidinvoke(Object...param){
Mapmap=createInHeapMap(SIZE);
//moveinoff-heap
byte[]bytes=serializer.serialize(map);
ByteBufferbuffer=ByteBuffer.allocateDirect(bytes.length);
buffer.put(bytes);
buffer.flip();
//forgc
map=null;
bytes=null;
System.out.println("writedown");
//moveoutfromoff-heap
byte[]offHeapBytes=newbyte[buffer.limit()];
buffer.get(offHeapBytes);
MapdeserMap=serializer.deserialize(offHeapBytes);
for(inti=0;iStringkey="key-"+i;
FeedDOfeedDO=deserMap.get(key);
checkValid(feedDO);
if(i%10000==0){
System.out.println("read"+i);
}
}
free(buffer);
}
privateMapcreateInHeapMap(intsize){
longcreateTime=System.currentTimeMillis();
Mapmap=newConcurrentHashMap<>(size);
for(inti=0;iStringkey="key-"+i;
FeedDOvalue=createFeed(i,key,createTime);
map.put(key,value);
}
returnmap;
}
}
由JDK提供的堆外内存访问API只能申请到一个类似一维数组的ByteBuffer,JDK并未提供基于堆外内存的实用数据结构实现(如堆外的Map、Set),因此想要实现Cache的功能只能在write()时先将数据put()到一个堆内的HashMap,然后再将整个Map序列化后MoveIn到DirectMemory,取缓存则反之.由于需要在堆内申请HashMap,因此可能会导致多次FullGC.这种方式虽然可以使用堆外内存,但性能不高、无法发挥堆外内存的优势.
幸运的是开源界的前辈开发了诸如Ehcache、MapDB、ChronicleMap等一系列优秀的堆外内存框架,使我们可以在使用简洁API访问堆外内存的同时又不损耗额外的性能.
其中又以Ehcache最为强大,其提供了in-heap、off-heap、on-disk、cluster四级缓存,且Ehcache企业级产品(BigMemoryMax/BigMemoryGo)实现的BigMemory也是Java堆外内存领域的先驱.
示例2:
MapDBAPI实现堆外Cache
publicclassMapDBAppextendsAbstractAppInvoker{
privatestaticHTreeMapmapDBCache;
static{
mapDBCache=DBMaker.hashMapSegmentedMemoryDirect()
.expireMaxSize(SIZE)
.make();
}
@Test
@Override
publicvoidinvoke(Object...param){
for(inti=0;iStringkey="key-"+i;
FeedDOfeed=createFeed(i,key,System.currentTimeMillis());
mapDBCache.put(key,feed);
}
System.out.println("writedown");
for(inti=0;iStringkey="key-"+i;
FeedDOfeedDO=mapDBCache.get(key);
checkValid(feedDO);
if(i%10000==0){
System.out.println("read"+i);
}
}
}
}
结果&分析
DirectByteBufferApp
S0S1EOPYGCYGCTFGCFGCTGCT
0.000.005.2278.5759.85192.902137.25110.153
thelastonejstatofMapDBApp
S0S1EOPYGCYGCTFGCFGCTGCT
0.000.038.020.3844.461710.23800.0000.238
运行DirectByteBufferApp.invoke()会发现有看到很多FullGC的产生,这是因为HashMap需要一个很大的连续数组,Old区很快就会被占满,因此也就导致频繁FullGC的产生.
而运行MapDBApp.invoke()可以看到有一个DirectMemory持续增长的过程,但FullGC却一次都没有了.
实验:
使用堆外内存减少FullGC
实验环境
java-version
javaversion"1.7.0_79"
Java(TM)SERuntimeEnvironment(build1.7.0_79-b15)
JavaHotSpot(TM)64-BitServerVM(build24.79-b02,mixedmode)
VMOptions
-Xmx512M
-XX:
MaxDirectMemorySize=512M
-XX:
+PrintGC
-XX:
+UseConcMarkSweepGC
-XX:
+CMSClassUnloadingEnabled
-XX:
CMSInitiatingOccupancyFraction=80
-XX:
+UseCMSInitiatingOccupancyOnly
实验数据
170W条动态(FeedDO).
实验代码
第1组:
in-heap、affectbyGC、noserialize
ConcurrentHashMapApp
publicclassConcurrentHashMapAppextendsAbstractAppInvoker{
privatestaticfinalMapcache=newConcurrentHashMap<>();
@Test
@Override
publicvoidinvoke(Object...param){
//write
for(inti=0;iStringkey=String.format("key_%s",i);
FeedDOfeedDO=createFeed(i,key,System.currentTimeMillis());
cache.put(key,feedDO);
}
System.out.println("writedown");
//read
for(inti=0;iStringkey=String.format("key_%s",i);
FeedDOfeedDO=cache.get(key);
checkValid(feedDO);
if(i%10000==0){
System.out.println("read"+i);
}
}
}
}
GuavaCacheApp类似,详细代码可参考完整项目.
第2组:
off-heap、notaffectbyGC、needserialize
EhcacheApp
publicclassEhcacheAppextendsAbstractAppInvoker{
privatestaticCachecache;
static{
ResourcePoolsresourcePools=ResourcePoolsBuilder.newResourcePoolsBuilder()
.heap(1000,EntryUnit.ENTRIES)
.offheap(480,MemoryUnit.MB)
.build();
CacheConfigurationconfiguration=CacheConfigurationBuilder
.newCacheConfigurationBuilder(String.class,FeedDO.class,resourcePools)
.build();
cache=CacheManagerBuilder.newCacheManagerBuilder()
.withCache("cacher",configuration)
.build(true)
.getCache("cacher",String.class,FeedDO.class);
}
@Test
@Override
publicvoidinvoke(Object...param){
for(inti=0;iStringkey=String.format("key_%s",i);
FeedDOfeedDO=createFeed(i,key,System.currentTimeMillis());
cache.put(key,feedDO);
}
System.out.println("writedown");
//read
for(inti=0;iStringkey=String.format("key_%s",i);
Objecto=cache.get(key);
checkValid(o);
if(i%10000==0){
System.out.println("read"+i);
}
}
}
}
MapDBApp与前同.
第3组:
off-process、notaffectbyGC、serialize、affectbyprocesscommunication
LocalRedisApp
publicclassLocalRedisAppextendsAbstractAppInvoker{
privatestaticfinalJediscache=newJedis("localhost",6379);
privatestaticfinalIObjectSerializerserializer=newHessian2Serializer();
@Test
@Override
publicvoidinvoke(Object...param){
//write
for(inti=0;iStringkey=String.format("key_%s",i);
FeedDOfeedDO=createFeed(i,key,System.currentTimeMillis());
byte[]value=serializer.serialize(feedDO);
cache.set(key.getBytes(),value);
if(i%10000==0){
System.out.println("write"+i);
}
}
System.out.println("writedown");
//read
for(inti=0;iStringkey=String.format("key_%s",i);
byte[]value=cache.get(key.getBytes());
FeedDOfeedDO=serializer.deserialize(value);
checkValid(feedDO);
if(i%10000==0){
System.out.println("read"+i);
}
}
}
}
结果分析
对比前面几组数据,可以有如下总结:
将长生命周期的大对象(如cache)移出heap可大幅度降低FullGC次数与耗时;
使用off-heap存储对象需要付出serialize/deserialize成本;
将cache放入分布式缓存需要付出进程间通信/网络通信的成本(UNIXDomain/TCPIP)
附:
off-heap的Ehcache能够跑出比in-heap的HashMap/Guava更好的成绩确实是我始料未及的O(∩_∩)O~,但确实这些数据和堆内存的搭配导致in-heap的FullGC太多了,当heap堆开大之后就肯定不是这个结果了.因此在使用堆外内存降低FullGC前,可以先考虑是否可以将heap开的更大.
附:
性能测试框架
在main函数启动时,扫描com.vdian.se.apps包下的所有继承了AbstractAppInvoker的类,然后使用Javassist为每个类生成一个代理对象:
当invoke()方法执行时首先检查他是否标注了@Test注解(在此,我们借用junit定义好了的注解),并在执行的前后记录方法执行耗时,并最终对比每个实现类耗时统计.
依赖
mons
commons-proxy
${commons.proxy.version}
org.javassist
javassist
${javassist.version}
com.caucho
hessian
${hessian.version}
com.google.guava
guava
${guava.version}
junit
junit
${junit.version}
启动类:
OffHeapStarter
/**
*@authorjifang
*@since2017/1/1上午10:
47.
*/
publicclassOffHeapStarter{
privatestaticfinalMapSTATISTICS_MAP=newHashMap<>();
publicstaticvoidmain(String[]args)throwsIOException,IllegalAccessException,InstantiationException{
Set>>classes=PackageScanUtil.scanPackage("com.vdian.se.apps");
for(Class
>clazz:
classes){
AbstractAppInvokerinvoker=createProxyInvoker(clazz.newInstance());
invoker.invoke();
//System.gc();
}
System.out.println("*********************statistics**********************");
for(Map.Entryentry:
STATISTICS_MAP.entrySet()){
System.out.println("method["+entry.getKey()+"]totalcost["+entry.getValue()+"]ms");
}
}
privatestaticAbstractAppInvokercreateProxyInvoker(Objectinvoker){
ProxyFactoryfactory=wJavassistProxyFactory();
Class
>superclass=invoker.getClass().getSuperclass();
Objectproxy=factory
.createInterceptorProxy(invoker,newProfileInterceptor(),newClass[]{superclass});
return(AbstractAppInvoker)proxy;
}
privatestaticclassProfileInterceptorimplementsInterceptor{
@Override
publicObjectintercept(Invocationinvocation)throwsThrowable{
Class
>clazz=invocation.getProxy().getClass();
Methodmethod=clazz.getMethod(invocation.getMethod().getName(),Object[].class);
Objectresult=null;
if(method.isAnnotationPresent(Test.class)
&&method.getName().equals("invoke"))