快播精简版官方下载-怎么用电脑给手机杀毒

allocatedirect
2023年4月4日发(作者:ap隔离)

JAVA堆外内存的简介和使⽤

内存分析

最近看了⼀篇⽂章《蚂蚁消息中间件(MsgBroker)在YGC优化上的探索》。

⽂章涉及JVM的垃圾回收,主要讲的是通过使⽤「堆外内存」对YoungGC进⾏优化。

⽂章中介绍,MsgBroker消息中间件会对消息进⾏缓存,JVM需要为被缓存的消息分配内存,⾸先会被分配到年轻代。

当缓存中的消息由于各种原因,⼀直投递不成功,这些消息会进⼊⽼年代。

最终呈现的问题是YGC时间太长。

随着新特性的开发和消息量的增长,我们发现MsgBroker的YGC平均耗时已缓慢增长⾄50ms~60ms,甚⾄部分机

房的YGC平均耗时已⾼达120ms。

有⼀个疑问,消息进⼊⽼年代,出现堆积,为何会导致YGC时间过长呢?

按着⽂章中的叙述,回答这个问题。

1.在YGC阶段,涉及到垃圾标记的过程,从GCRoot开始标记。

2.因为YGC不涉及到⽼年代的回收,⼀旦从GCRoot扫描到引⽤了⽼年代对象时,就中断本次扫描。这样做可以减少扫描范围,加速YGC。

3.存在被⽼年代对象引⽤的年轻代对象,它们没有被GCRoot直接或者间接引⽤。

阶段中的old-genscanning即⽤于扫描被⽼年代引⽤的年轻代对象。

-genscanning扫描时间与⽼年代内存占⽤⼤⼩成正⽐。

6.得到结论,⽼年代内存占⽤增⼤会导致YGC时间变长。

总的来说,将消息缓存在JVM内存会对垃圾回收造成⼀定影响:

1.消息最初缓存到年轻代,会增加YGC的频率。

2.消息被提升到⽼年代,会增加FGC的频率。

3.⽼年代的消息增长后,会延长old-genscanning时间,从⽽增加YGC耗时。

⽂章使⽤「堆外内存」减少了消息对JVM内存的占⽤,并使⽤基于Netty的⽹络层框架,达到了理想的YGC时间。

注:Netty中也使⽤了堆外内存。

通过引⼊⾃适应投递限流,在实验室测试环境下,MsgBroker在异常场景下的YGC耗时进⼀步从83ms降低到

40ms,恢复了正常的⽔平。

⼀:堆外内存是什么?

在JAVA中,JVM内存指的是堆内存。

机器内存中,不属于堆内存的部分即为堆外内存。

堆外内存也被称为直接内存。

堆内存和堆外内存

堆外内存并不神秘,在C语⾔中,分配的就是机器内存,和本⽂中的堆外内存是相似的概念。

在JAVA中,可以通过Unsafe和NIO包下的ByteBuffer来操作堆外内存。

Unsafe类操作堆外内存

提供了⼀组⽅法来进⾏堆外内存的分配,重新分配,以及释放。

nativelongallocateMemory(longsize);——分配⼀块内存空间。

nativelongreallocateMemory(longaddress,longsize);——重新分配⼀块内存,把数据从address指向的缓存中拷贝到新的内

存块。

nativevoidfreeMemory(longaddress);——释放内存。

参考:Unsafe类操作JAVA内存

⼀顿操作猛如虎,直接psvm⾛起。

publicstaticvoidmain(String[]args){

Unsafeunsafe=newUnsafe();

teMemory(1024);

}

然⽽Unsafe类的构造器是私有的,报错。

⽽且,allocateMemory⽅法也不是静态的,不能通过teMemory调⽤。

幸运的是可以通过afe()取得Unsafe的实例。

publicclassUnsafeTest{

publicstaticvoidmain(String[]args){

Unsafeunsafe=afe();

teMemory(1024);

cateMemory(1024,1024);

mory(1024);

}

}

此外,也可以通过反射获取unsafe对象实例

参考:危险代码:如何使⽤Unsafe操作内存中的Java类和对象

NIO类操作堆外内存

⽤NIO包下的ByteBuffer分配直接内存则相对简单。

publicclassTestDirectByteBuffer{

publicstaticvoidmain(String[]args)throwsException{

ByteBufferbuffer=teDirect(10*1024*1024);

}

}

然⽽运⾏时报错了。

java(51146,0x7000023ed000)malloc:***errorforobject0x400:pointerbeingrealloc'dwasnotallocated

***setabreakpointinmalloc_error_breaktodebug

错误信息

参考:JAVA堆外内存

然⽽在⼩伙伴的电脑上跑这段的代码是可以成功运⾏的。

⼆:堆外内存垃圾回收

对于内存,除了关注怎么分配,还需要关注如何释放。

从JAVA出发,习惯性思维是堆外内存是否有垃圾回收机制。

考虑堆外内存的垃圾回收机制,需要了解以下两个问题:

1.堆外内存会溢出么?

2.什么时候会触发堆外内存回收?

问题⼀

通过修改JVM参数:-XX:MaxDirectMemorySize=40M,将最⼤堆外内存设置为40M。

既然堆外内存有限,则必然会发⽣内存溢出。

为模拟内存溢出,可以设置JVM参数:-XX:+DisableExplicitGC,禁⽌代码中显式调⽤()。

可以看到出现OOM。

得到的结论是,堆外内存会溢出,并且其垃圾回收依赖于代码显式调⽤()。

参考:JAVA堆外内存

问题⼆

关于堆外内存垃圾回收的时机,⾸先考虑堆外内存的分配过程。

JVM在堆内只保存堆外内存的引⽤,⽤DirectByteBuffer对象来表⽰。

每个DirectByteBuffer对象在初始化时,都会创建⼀个对应的Cleaner对象。

这个Cleaner对象会在合适的时候执⾏mory(address),从⽽回收这块堆外内存。

当DirectByteBuffer对象在某次YGC中被回收,只有Cleaner对象知道堆外内存的地址。

当下⼀次FGC执⾏时,Cleaner对象会将⾃⾝Cleaner链表上删除,并触发clean⽅法清理堆外内存。

此时,堆外内存将被回收,Cleaner对象也将在下次YGC时被回收。

如果JVM⼀直没有执⾏FGC的话,⽆法触发Cleaner对象执⾏clean⽅法,从⽽堆外内存也⼀直得不到释放。

其实,在teDirect⽅式中,会主动调⽤()强制执⾏FGC。

JVM觉得有需要时,就会真正执⾏GC操作。

显式调⽤

参考:堆外内存的回收机制分析—占⼩狼

三:为什么⽤堆外内存?

堆外内存的使⽤场景⾮常巧妙。

第三⽅堆外缓存管理包ohc(off-heap-cache)给出了详细的解释。

摘了其中⼀段。

Whenusingaveryhugenumberofobjectsinaverylargeheap,Virtualmachineswillsufferfromincreased

GCpressuresinceitbasicallyhastoinspecteachandeveryobjectwhetheritcanbecollectedandhasto

shallkeepahotsetofobjectsaccessibleforfastaccess(sk

ornetworkroundtrips).Theonlysolutionistousenativememory-andthereyouwillendupwiththe

choiceeithertousesomenativecode(C/C++)viaJNIorusedirectmemoryaccess.

⼤概的意思如下:

考虑使⽤缓存时,本地缓存是最快速的,但会给虚拟机带来GC压⼒。

使⽤硬盘或者分布式缓存的响应时间会⽐较长,这时候「堆外缓存」会是⼀个⽐较好的选择。

参考:OHC-Anoff-heap-cache—Github

四:如何⽤堆外内存?

在第⼀章中介绍了两种分配堆外内存的⽅法,Unsafe和NIO。

对于两种⽅法只是停留在分配和回收的阶段,距离真正使⽤的⽬标还很遥远。

在第三章中提到堆外内存的使⽤场景之⼀是缓存。

那是否有⼀个包,⽀持分配堆外内存,⼜⽀持KV操作,还⽆需关⼼GC。

答案当然是有的。

有⼀个很知名的包,Ehcache。

Ehcache被⼴泛⽤于Spring,Hibernate缓存,并且⽀持堆内缓存,堆外缓存,磁盘缓存,分布式缓存。

此外,Ehcache还⽀持多种缓存策略。

其仓库坐标如下:

e

ehcache

3.4.0

接下来就是写代码进⾏验证:

publicclassHelloHeapServiceImplimplementsHelloHeapService{

privatestaticMapinHeapCache=hMap();

privatestaticCacheoffHeapCache;

static{

ResourcePoolsresourcePools=ourcePoolsBuilder()

.offheap(1,)

.build();

CacheConfigurationconfiguration=CacheConfigurationBuilder

.newCacheConfigurationBuilder(,,resourcePools)

.build();

offHeapCache=heManagerBuilder()

.withCache("cacher",configuration)

.build(true)

.getCache("cacher",,);

for(inti=1;i<10001;i++){

("InHeapKey"+i,newInHeapClass("InHeapKey"+i,"InHeapValue"+i));

("OffHeapKey"+i,newOffHeapClass("OffHeapKey"+i,"OffHeapValue"+i));

}

}

@Data

@AllArgsConstructor

privatestaticclassInHeapClassimplementsSerializable{

privateStringkey;

privateStringvalue;

}

@Data

@AllArgsConstructor

privatestaticclassOffHeapClassimplementsSerializable{

privateStringkey;

privateStringvalue;

}

@Override

publicvoidhelloHeap(){

n(String(("InHeapKey1")));

n(String(("OffHeapKey1")));

Iteratoriterator=or();

intsum=0;

while(t()){

n(String(()));

sum++;

}

n(sum);

}

}

其中

.offheap(1,)

表⽰分配的是堆外缓存。

Demo很简单,主要做了以下⼏步操作:

1.新建了⼀个Map,作为堆内缓存。

2.⽤Ehcache新建了⼀个堆外缓存,缓存⼤⼩为1MB。

3.在两种缓存中,都放⼊10000个对象。

eap⽅法做get测试,并统计堆外内存数量,验证先插⼊的对象是否被淘汰。

使⽤JavaVisualVM⼯具Dump⼀个内存镜像。

JavaVisualVM是JDK⾃带的⼯具。

⼯具位置如下:

/Library/Java/JavaVirtualMachines/jdk1.7.0_/Contents/Home/bin/jvisualvm

也可以使⽤JProfiler⼯具。

打开镜像,堆⾥有10000个InHeapClass,却没有OffHeapClass,表⽰堆外缓存中的对象的确没有占⽤JVM内存。

内存镜像

接着测试helloHeap⽅法。

输出:

{"key":"InHeapKey1","value":"InHeapValue1"}

null

……(此处有⼤量输出)

5887

输出表⽰堆外内存启⽤了淘汰机制,插⼊10000个对象,最后只剩下5887个对象。

如果堆外缓存总量不超过最⼤限制,则可以顺利get到缓存内容。

总体⽽⾔,使⽤堆外内存可以减少GC的压⼒,从⽽减少GC对业务的影响。

参考

《蚂蚁消息中间件(MsgBroker)在YGC优化上的探索》

Unsafe类操作JAVA内存

危险代码:如何使⽤Unsafe操作内存中的Java类和对象

JAVA堆外内存

堆外内存的回收机制分析—占⼩狼

OHC-Anoff-heap-cache—Github

Ehcache官⽹

更多推荐

allocatedirect