堆外内存这玩意是真不错,我要写进简历了
你好呀,我是歪歪。
之前在《3 招将吞吐量提升了 100%,现在它是我的了》这篇文章中,我在 OHC 堆外缓存上插了个眼:
这次就把这个眼给回收了吧,给你盘一下 OHC。
之前的文章里面说的是啥场景呢,我们先简单回顾一下。
就是一个服务的各项 JVM 的配置都比较合理的情况下,它的 GC 情况还是不容乐观。
然后 dump 了一把内存,一顿分析之后发现有 2 个对象特别巨大,占了总存活堆内存的 76.8%。其中第 1 大对象是本地缓存, GC 之后依旧存活,干都干不掉。
怎么办呢?
把缓存对象移到堆外。
因为堆外内存并不在 GC 的工作范围内,所以避免了缓存过大对 GC 的影响。
堆外内存不受堆内内存大小的限制,只受服务器物理内存的大小限制。这三者之间的关系是这样的:物理内存=堆外内存+堆内内存。
对于堆外内存的使用,有个现成开源项目就是 OHC,开箱即用,香的一比。
当时就是这样做了一个简单的介绍,也没有深入的去分析,我个人对这个 OHC 还是比较感兴趣,但是有一说一,这玩意应用场景是真的不丰富,但是如果恰巧碰到了可以使用的应用场景。就可以开始你的表(装)演(逼)了
什么是可以使用的应用场景?
就是你的本地缓存对象特别多,多到都把你"堆"里面都快塞满了,从而 GC 频繁、时间长,都影响到服务的正常运行了。
这个时候一拨人说:我要求调整 JVM 参数,调大堆内存。
还有一拨人说:依我看,这个本地缓存干脆就别滥用了,非必要,不缓存,减少内存占用。
另外一拨人说:又不是不能用?
大家争得面红耳赤的时候,你轻飘飘的来一句:这个问题我觉得可以用堆外内存来解决,比如有个开源项目叫做 OHC,就比较好,可以调研一下。
事了拂衣去,深藏身与名。
所以为了以后能更好的装这个逼,这篇文章我准备盘一盘它,但是先说好,本文不会带你去盘源码,只是让你知道有这个框架的存在,做个简单的导读而已。 Demo
老规矩,对于自己不了解的技术,都是先会简单使用,再深入了解。
所以还是得搞个 Demo 才行,直接到它的 github 上找 Quickstart 就完事它的 Quickstart 就这么一行代码:
https://github.com/snazy/ohc
我看到的第一眼就是觉得这也太简陋了,在我想象中,一个好的 Quickstart 是我自己粘贴过来就能直接跑,很显然,它这个不行,本资深白嫖党表示强烈的谴责以及极度愤怒。
但是没办法,还是先粘过来再说。
对了,记得先导入 maven 依赖: org.caffinitas.ohc ohc-core 0.7.4
粘贴过来之后,我发现它这属于一个填空题啊:
key 和 value 的序列化方式并没有给我们提供,而是需要我们进行自定义,这一点在它的 README 中也提到了:
它说 key 和 value 的序列化需要去实现 CacheSerializer 接口,这个接口三个方法,分别是对象序列化之后的长度,序列化和反序列化方法。
需要自己去实现一个序列化方式,一瞬间我的脑海里面蹦出了好几个关键词:Protobuf、Thrift、kryo、hessian 什么的。
但是都太麻烦了,还得自己去编码,我只是想搞个 Demo 尝个味道而已,要是能从哪儿直接借鉴一个过来就好了。
所以,我把 OHC 的源码拉下来了,因为直觉告诉我,它的测试用例里面肯定有现成的序列化方案。
果不其然,测试案例非常的多,而我找到了这个:
org.caffinitas.ohc.linked.TestUtils
这个序列化方式就是测试用例里面广泛使用的方式:
现在序列化方式有了,那么整个完整的代码就是这样的,我也给你搞个舒服的 Quickstart,粘过去就能用那种: public class OhcDemo { public static void main(String[] args) { OHCache ohCache = OHCacheBuilder.newBuilder() .keySerializer(OhcDemo.stringSerializer) .valueSerializer(OhcDemo.stringSerializer) .build(); ohCache.put("hello","why"); System.out.println("ohCache.get(hello) = " + ohCache.get("hello")); } public static final CacheSerializer stringSerializer = new CacheSerializer() { public void serialize(String s, ByteBuffer buf) { // 得到字符串对象UTF-8编码的字节数组 byte[] bytes = s.getBytes(Charsets.UTF_8); // 用前16位记录数组长度 buf.put((byte) ((bytes.length >>> 8) & 0xFF)); buf.put((byte) ((bytes.length) & 0xFF)); buf.put(bytes); } public String deserialize(ByteBuffer buf) { // 获取字节数组的长度 int length = (((buf.get() & 0xff) << 8) + ((buf.get() & 0xff))); byte[] bytes = new byte[length]; // 读取字节数组 buf.get(bytes); // 返回字符串对象 return new String(bytes, Charsets.UTF_8); } public int serializedSize(String s) { byte[] bytes = s.getBytes(Charsets.UTF_8); // 设置字符串长度限制,2^16 = 65536 if (bytes.length > 65535) throw new RuntimeException("encoded string too long: " + bytes.length + " bytes"); return bytes.length + 2; } }; }
从上面的 Demo 你也能看出来,OHCache 这个东西,和 Map 差不多,基本方法也是 put,get。
只是 put 的对象,也就是缓存的对象,是由用户自定义的序列化方法决定的。比如我上面这个只能缓存字符串类型,如果你想要放个自定义对象进去,就得实现一个自定义对象的系列化方法,很简单的,网上搜一下,多的很。
现在我们已经有一个可以运行的 Demo 了,运行之后输出是这样的,没有任何毛病:
Demo 跑起来了,我们就算是找到"抓手"了,接下来就是分析它,结合自己的实际业务,沉淀出一套"可迁移、可复用"的组合拳,用来给自己"赋能"。
对比
为了让你能更加直观的看到堆外内存和堆内内存的区别,我给你搞两段程序跑跑。
首先是我们堆内内存的代表选手,HashMap: /** * -Xms100m -Xmx100m */ public class HashMapCacheExample { private static HashMap HASHMAP = new HashMap<>(); public static void main(String[] args) throws InterruptedException { hashMapOOM(); } private static void hashMapOOM() throws InterruptedException { //准备时间,方便观察 TimeUnit.SECONDS.sleep(10); int num = 0; while (true) { //往 map 中存放 1M 大小的字符串 String big = new String(new byte[1024 * 1024]); HASHMAP.put(num + "", big); num++; } } }
通过 JVM 参数控制堆内存大小为 100m,然后不断的往 Map 中存放 1M 大小的字符串,那么这个程序很快就会出现 OOM:
其对应的在 visualvm 里面的内存走势图是这样的:
程序基本上属于一启动,然后内存就被塞满了,接着立马就凉了。
属于秒了,被秒杀了。
但是,当我们同样的逻辑,用堆外内存的时候,情况就不一样了: /** * -Xms100m -Xmx100m */ public class OhcCacheDemo { public static void main(String[] args) throws InterruptedException { //准备时间,方便观察 TimeUnit.SECONDS.sleep(10); OHCache ohCache = OHCacheBuilder.newBuilder() .keySerializer(stringSerializer) .valueSerializer(stringSerializer) .build(); int num = 0; while (true) { String big = new String(new byte[1024 * 1024]); ohCache.put(num + "", big); num++; } } public static final CacheSerializer stringSerializer = new CacheSerializer() {//前面写过,这里略了}; }
关于上面程序中的 stringSerializer 需要注意一点的是我做测试的时候把这个大小的限制取消掉了,目的是和 HashMap 做测试是用同样大小为 1M 的字符串:
这是程序运行了 3 分钟之后的内存走势图:
这个图怎么说呢?
丑是丑了点,但是咱就是说至少没秒,程序没崩。
当这两个内存走势图一对比,是不是稍微就有那么一点点感觉了。
但是另外一个问题就随之而来了:我怎么看 OHCache 这个玩意占用的内存呢?
前面说了,它属于堆外内存。JVM 的堆外,那就是我本机的内存了。
打开任务管理器,切换到内存的走势图,正常来说走势图是这样的,非常的平稳:
从上面截图可以看到,我本机是 16G 的内存大小,目前还有 9.9G 的内存可以使用。
也就是说截图的这个时刻,我能使用的堆外内存顶天了也就是 9.9G 这个数。
那么我先用它个 6G,程序一启动,走势图就会变成这样:
而程序一关闭,内存占用立马就释放了:
也许你没注意到,前面我说了一句"用它个 6G",我怎么控制这个 6G 的呢?
因为我在程序里面加了这样一行代码:
如果你不加的话,默认只会使用 64M 的堆外内存,看不出啥曲线。
如果你要想自己玩一玩,想亲眼看看这个走势图,记得加上这行代码,具体的值按照你机器的情况给就行了,个人建议是先做好保存工作,最好是意思意思就行了,别把值给的太大,电脑玩坏了你来找我,我不仅不赔钱,我还会笑你。
然后除了这个 "6G" 可以自定义外,还有一些很多可以自定义的参数,清单如下,可以自己研究一波:
源码
前面说了,本文也不会带你去阅读源码,因为这个项目的源码写的已经很通俗易懂了,你自己去看,就知道主干逻辑写的非常的顺畅,没必要做太多的源码解析。
我最多在这里指个路。
我看源码是从 put 方法开始看的,但是 put 方法有两个实现类:
关于这两个实现类,github 上进行了介绍:
linked 实现方式:为每个需要缓存的对象单独分配堆外内存,对中、大条目效果最好。 chunked 实现方式:为每个哈希段整体分配堆外内存,相当于有个预分配的意思,适用于小条目。
但是这里你只需要看 linked 实现方式就行了。
为啥?
别问,问就是作者建议的,在 github 的 README 里面有这样一个 NOTE:
Note: the chunked implementation should still be considered experimental.
翻译一下就是说:目前,chunked 实现方式应被当做是 experimental。
experimental,放在句末,你就知道这是一个形容词了,什么意思呢?
四级词汇,如果不认识的话赶紧背一背哈,考试要考的。
作者说 chunked 实现方式还是实验阶段,肯定是有什么"暗坑"在里面的,不踩坑的最好方式,就是不用它。
然后,你看着看着会发现,这个数据结构,和 ConcurrentHashMap 好像啊。是的,有 Segment,有 bucket,有 entry,所以不要怀疑自己,确实很像。
接着,你看源码的时候,肯定是 Debug 的方式效率更高嘛。
当你 debug 的对象是 put 方法的时候,要不了几下你就能看看这个地方:
这个地方是申请堆外内存的操作,对应的是 IAllocator 这个接口:
接口里面有三个方法: allocate:申请内存 free:释放内存 getTotalAllocated:获取已申请内存(空方法,未实现)
主要关心前两个方法,因为我前面说了,这个是堆外内存,需要自己管理内存。管理就分为申请和释放,对应的就是这两个方法。
所以,这里可以说是整个 OHC 框架的核心。
带你盘一下这部分。
操作堆外内存
其实堆外内存这个东西,你一定是接触过的,只不过一般是框架封装好了,它是自己悄悄咪咪的使用,你没注意到而已罢了。
一般我们申请堆外内存,就会这样去写:
这个方法最终会调用 Unsafe 里面的 allocateMemory 这个 native 方法,它相当于 C++ 的 malloc 函数:
这个方法会为我们在操作系统的内存中去分配一个我们指定大小的内存供我们使用,这个内存就叫做堆外内存,不由 JVM 控制,即不在 gc 管理范围内的。
这个方法返回值是 long 类型数值,也就是申请的内存对应的首地址。
但是需要注意的是,JVM 有个叫做 -XX:MaxDirectMemorySize (最大堆外内存)的配置,如果使用 ByteBuffer.allocateDirect 申请堆外内存,大小会受到这个配置的限制,因为会调用这个方法:
OHC 要使用堆外内存,必然也是通过某个方法向操作系统申请了一部分内存,那么它申请内存的方法,是不是也是 allocateMemory 呢?
这个问题,在 github 上作者给出了否认三连:
不仅告诉了你没有使用,还告诉了你为什么没有使用:
首先,开头的这个玩意 "TL;DR" 就直接把我干懵逼了。然后我查了一下,原来是 "Too long; Don"t read" 的缩写,直译过来的意思就是:太长了,读不下去。
但是我觉得结合语境分析,作者放在的意思应该是一种类似于"长话短说"的意思。
这个短语,一般用于在文章开头,先给出干货。
你看,又学一个小知识。
然后,我大概给你解释一下这一段 English 在说个什么意思。
作者说,绕过 ByteBuffer.allocateDirect 方法,直接分配堆外内存,对 GC 来说是更加平稳的,因为我们可以明确控制内存分配,更重要的是可以由我们自己完全控制内存的释放。
如果使用 ByteBuffer.allocateDirect 方法,可能在垃圾回收期间,就释放了堆外内存。
这句话对应到代码中就是这里,而这样的操作,在 OHC 里面是不需要的。OHC 希望由框架自己来全权掌握什么时候应该释放:
然后作者接着说:此外,如果分配内存的时候,没有更多的堆外内存可以使用,它可能会触发一个 Full GC,如果多个申请内存的线程同时遇到这种情况,这是有问题的,因为这意味着大量 Full GC 的连续发生。
这句话对应的代码是这里:
如果堆外内存不足的时候,会触发一次 Full GC。可以想象,在机器内存吃紧的时候,程序还在不停的申请堆外内存,继而导致 Full GC 的频繁出现,是一种什么样的"灾难性"的后果,基本上服务就处于不可用状态了。
OHC 需要避免这种情况的发生。
除了这两个原因之后,作者还说:
Further, the stock implementation uses a global, synchronized linked list to track off-heap memory allocations.
在 ByteBuffer.allocateDirect 方法的实现里面,还使用了一个全局的、同步的 linked List 这个数据结构来跟踪堆外内存的分配。
这里我不清楚它说的这个 "linked list" 对应具体是什么东西,所以我也不乱解释了,你要知道的话可以在评论区给我指个路,我也学习学习。
综上,作者最后一句说:这就是为什么 OHC 直接分配堆外内存的原因。
This is why OHC allocates off-heap memory directly。
然后他还提了一个建议:
and recommends to preload jemalloc on Linux systems to improve memory managment performance.
建议在 Linux 系统上预装 jemalloc 以提高内存管理性能。
弦外之音就是要拿它来替换 glibc 的 malloc 嘛,jemalloc 基本上是碾压 malloc。
关于 jemalloc 和 malloc 网上有很多相关的文章了,有兴趣的也可以去找找,我这里就不展开了。
现在我们知道 OHC 并没有使用常规的 ByteBuffer.allocateDirect 方法来完成堆外内存的申请,那么它是怎么实现这个"骚操作"的呢?
在 UnsafeAllocator 实现类里面是这样写的:
org.caffinitas.ohc.alloc.UnsafeAllocator
通过反射直接获取到 Unsafe 并进行操作,没有任何多余的代码。
而在 JNANativeAllocator 实现类里面,则采用的是 JNA 的方式操作内存:
OHC 框架默认采用的是 JNA 的方式,这一点通过代码或者日志输出也能进行验证:
关于 Unsafe 和 JNA 这两种操作堆外内存的方式,到底谁更好,我在网上找到了这个链接:
https://mail.openjdk.org/pipermail/hotspot-dev/2015-February/017089.html
这封邮件的是 Aleksey Shipilev 针对一个叫做 Robert 的网友提出问题进行的回复。
问题是这样的,Robert 他用对 Native.malloc() 和 Unsafe.allocateMemory() 进行了基准测试,发现前者的性能是后者的三倍。想知道为什么:
然后 Aleksey Shipilev 针对这个问题进行了解析。
这哥们是谁?
他是基准测试的爸爸:
所以他的回答还是比较权威的,但是需要注意的是,他并没有正面说明两个方法岁更好,只是解释了为什么用 JMH 出现了性能差 3 倍这个现象。
另外,我必须得多说一句,通过反射拿 Unsafe 这段代码可是个好东西啊,建议熟读、理解、融会贯通:
在 OHC 里面不就是一个非常好的例子嘛,虽然有现成的方法,但是和我的场景不是非常的匹配,我并不需要一些限制性的判断,只是想要简简单单的要一个堆外内存来用一用而已。
那我就绕过中间商,自己直接调用 Unsafe 里面的方法。
怎么拿到 Unsafe 呢?
就是前面这段代码,就是通过反射,你在其他的开源框架里面可以看到非常多类似的或者一模一样的代码片段。
背下来就完事。
好了,文章就到这里了,如果对你有一丝丝的帮助,帮我点个免费的赞,不过分吧?
陶瓷牙的血泪历程今天我的牙齿又开始疼了,或许我又要把四个陶瓷牙去掉检查一下自己那一点点可怜的真牙根是不是又作什么妖了。人生,会有很多后悔的事,对于我来说放弃了好的工作的机会,放弃了家境一般前途渺茫
不知不觉已过而立之年,记录生活随笔再一次翻开这本日记本,竟然已经过了十余年了,早已不记得曾经写过的日记曾经的心路历程。从2005年初中到高中大学,现在已工作第六年,刚好大年初六,明天又要开年准备上班了!疫情三年,2
霸屏朋友圈!潮汕英歌舞把年味儿拉满视频加载中作为广东潮汕地区的传统民俗舞蹈,英歌舞是集舞蹈戏剧武术于一体的民间表演艺术。它主要流行于广东省潮汕地区,以普宁市潮阳区潮南区最盛。英歌舞在进化过程中,吸纳了水浒传等来自中
今年发微信,发朋友圈人数急转直下,必将迎来一个全新的交往模式也许是大家阳康之后都很疲惫也许是阳康之后大彻大悟,看透了一切也许是阳康之后,心灰意冷不管怎么说,今年发微信,发朋友圈人数急转直下的一年,以前,吃个年夜饭都吃不安生,手机一个劲儿响,
女生穿抹胸为啥不会往下掉?(男生禁入)今年11月可谓是明星红毯月前后举行了飞天金鹰和金鸡三大影视节的颁奖典礼红毯造型也可谓让人看花了眼但巧的是,两位影后都选择了黑色抹胸长裙于是,我就收到了观众老爷们这样的疑问女明星穿的
全身涂满红色镶嵌3万颗水晶看完想到了什么美国流行歌手DojaCat在巴黎高定时装周夏帕瑞丽(Schiaparelli)秀场上惊艳现身,她从头到脚涂满红色,并贴有3万颗水晶,吸引了无数人的目光。这场时装秀场上的她简直就是一
小米狂砸600亿打造的首款汽车曝光,或为遛背轿跑车新春聊聊车小米公司自从2021年决定拿出600亿启动小米汽车项目,目前已经距离小米定下的2024年上市目标只有一年的时间。最近,有网友在北京拍到小米首款车谍照。小米首款车从外观方面
普尔库里在比赛最后三分钟被驱逐毫无道理他是历史最佳之一直播吧1月26日讯NBA常规赛勇士122120绝杀灰熊。赛后普尔在场边接受了采访。在最后时刻,普尔先是一记无脑三分,气得库里怒摔牙套被驱逐,但是比赛最后时刻,他篮下接球上篮绝杀了灰
话年再说小时候的年,最浓的年味儿永远在小时候小时候期盼着过年,穿新衣戴新帽,提着灯笼满街跑。现在感觉过年越来越没意思了。那时候的我们,没有什么经济能力,但是也没有压力和烦恼!每天都会抬头仰望天空,看着流云和星星,总是在盼望长
闫学晶儿媳徐梦迪产子,51岁的她荣升为奶奶,孙子只比女儿小6岁闫学晶儿子林傲霏在个人社交账号上宣布当爸,妻子徐梦迪顺利产下男婴,他称老婆很坚强,孩子双顶径是10,一般都是剖腹产,梦迪很坚强,坚持顺产。林傲霏在分享喜悦的同时,言语间也充满了心疼
汪明荃到长辈家中拜年,94岁白雪仙雍容华贵显年轻,不像耄耋老人农历新年期间,拜年是最传统的习俗之一。不少香港艺人在与家人团圆之后,都会到前辈艺人的家中拜年,这是一种敬意的体现。现年75岁的汪明荃在娱乐圈中德高望重,受到众人敬仰,每年农历新年都
3月去哪里?抓住冬游西藏的尾巴,邂逅一场冰雪奇缘3月的阿里高原,玉砌冰雕。一种空无的宏阔的美,更加的深邃和诗意。在冰雪融化之前,走在这片辽阔的大地上,邂逅一场美妙的冰雪奇缘。极致风景神圣美丽的雪山湖泊,触手可及的浩瀚星空神秘古老
白宫清洁能源高级顾问中国太阳能电池板通关量增加美国正放行中国太阳能电池板进入美国?据外媒报道,当地时间7日(周一),美国总统拜登的白宫清洁能源事务高级顾问波德斯塔(JohnPodesta)在剑桥能源周(CERAWeek)国际能
公益体彩助力20222023全国滑雪定向挑战赛(吉林磐石莲花山站)3月5日,由公益体彩助力的20222023全国滑雪定向挑战赛(吉林磐石莲花山站)在磐石莲花山滑雪场举行。国家体育总局航管中心运动五部副主任谷兴东,吉林省体育局副局长雷鸣,吉林省无线
国家动真格的了!精减机关编制养老工作改革背后有何深意?近日,党的二十届二中全会通过了党和国家机构改革方案,国务院机构公布了改革方案,涉及多部门调整,包括国家机关人员编制精减老龄工作银行收缩等等。改革开放以来,国务院一共进行了八次机构改
国务院机构改革的这些重要内容,背后有何深意?国务院机构改革方案(以下简称方案)7日公布,引起社会各界高度关注。金融是此次机构改革的关键词,方案共有13条主要内容,其中6条与金融有关。中国人民大学财政金融学院副院长赵锡军告诉长
烟火人间岁月轻转岁月轻轻转,微风暖暖迎。一场花事,一场梦一段光阴,一段情。人到中年,总想放下俗世中的磨炼场,安静的回归自我,安然的待在喧嚣繁华的对面。不想再因生活而计较,不想被左右,心不在焉的劳累
挤掉日本队顺便避开韩国?U20男足早已表明立场,球迷瞬间清醒了北京时间3月8日,目前中国U20男足正在备战明天与吉尔吉斯斯坦的小组赛最后一轮的比赛。而随着此前结束的B组的较量结束,积6分的越南队竟在小组内上演了从天堂到谷底小组出局的命运,令人
有些痛,无人能懂,唯有心疼作者王娜原创作品禁止转载生活很简单,吃饱穿暖睡好,健康长寿。人生不简单,熬来熬去熬到老,才发现没有人在乎你。你在爱的路上付出很多很多,最后毫无收获。想放弃生命又太多牵扯,想活下去又
古人挖完井后,为何要放一只乌龟?其中有何深意,古人真的有智慧在阅读此文前,诚邀您点击一下关注,既方便您进行讨论与分享,又给您带来不一样的参与感,感谢您的支持。引言在人类文明的发展历程中,四大文明古国代表了远古时期的巅峰水平。不管是古代中国还
科技呼啸而来,数字人间让中年的我感受到了恐惧对于年过35的人来说,不知道有没有和我同样的感受,就是渐渐的学习能力下降了,以前看一遍能理解,看两三遍能记住的书再也没办法记住了,办公软件也很多不会操作,需要公司的年轻人帮忙了,连
什么体质的人容易怀孕怀孕的能力主要受到女性身体内的生理和生理变化的影响,因此体质健康的女性更容易怀孕。以下是一些与容易怀孕相关的体质特征正常的月经周期女性的月经周期通常为2835天,周期规律,月经量适