快播精简版官方下载-怎么用电脑给手机杀毒
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还⽀持多种缓存策略。
其仓库坐标如下:
接下来就是写代码进⾏验证:
publicclassHelloHeapServiceImplimplementsHelloHeapService{
privatestaticMap
privatestaticCache
static{
ResourcePoolsresourcePools=ourcePoolsBuilder()
.offheap(1,)
.build();
CacheConfiguration
.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
发布评论