我们在开发过程中会遇到这样的场景:就是一个服务的各项 JVM 的配置都比较合理的情况下,它的 GC 情况还是不容乐观。分析之后发现有 2 个对象特别巨大,占了总存活堆内存的 90%以上。其中第 1 大对象是本地缓存, GC 之后对象一直存活。然后不久应用就会抛出OutOfMemoryError,那怎样避免这种情况呢?
这次给大家推荐一个比较好用的技术:堆外缓存。哈哈哈,顾名思义就是在Java堆之外的缓存,也就是把一些大的难以被GC回收的对象放到堆之外。PS堆外内存不受,堆内内存大小的限制,只受服务器物理内存的大小限制。这三者之间的关系是这样的:物理内存=堆外内存+堆内内存。
技术大佬的GITHUB地址:https://github.com/snazy/ohc 。
要使用他的技术就先要引用对应的jar包,Maven坐标如下:
<dependency> <groupId>org.caffinitas.ohc</groupId> <artifactId>ohc-core</artifactId> <version>0.7.4</version> </dependency>
//大神给的使用方式如下: //Quickstart: OHCache ohCache = OHCacheBuilder.newBuilder() .keySerializer(yourKeySerializer) .valueSerializer(yourValueSerializer) .build();
上面是Quickstart 看起来使用如此的丝滑(简单),但是上面的代码是填空题,我们看到复制粘贴后代码不能使用后开始。。。。。。此处省略一万字。其实大神写的代码怎么不能用的呢,不要怀疑大神一定是自己的方法不对,我们的口号是?
如果提供的代码复制粘贴不能直接用的工程师不能称之为大神工程师。
但是github上的大神写的东西怎么不能直接用呢?一定是你的思路不对,老司机都知道,大神写代码一定有写单元测试的,要不然不会有那么多人用的(所以要想成为大神单元测试一定要写好),所以把代码拉下来,复制单元测试的东西应该能直接使用 。下面是CTRL+C来的代码。
public static void main(String[] args) { OHCache ohCache = OHCacheBuilder.<String, String>newBuilder() .keySerializer(new StringSerializer()) .valueSerializer(new StringSerializer()) .build(); ohCache.put("name","xiaozhang"); System.out.println(ohCache.get("name")); // 结果 xiaozhang } static class StringSerializer implements CacheSerializer<String>{ @Override public void serialize(String value, ByteBuffer buf) { // 得到字符串对象UTF-8编码的字节数组 byte[] bytes = value.getBytes(Charsets.UTF_8); // 用前16位记录数组长度 buf.put((byte) ((bytes.length >>> 8) & 0xFF)); buf.put((byte) ((bytes.length) & 0xFF)); buf.put(bytes); } @Override 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); } @Override public int serializedSize(String value) { byte[] bytes = value.getBytes(Charsets.UTF_8); // 设置字符串长度限制,2^16 = 65536 if (bytes.length > 65536) throw new RuntimeException("encoded string too long: " + bytes.length + " bytes"); // 设置字符串长度限制,2^16 = 65536 return bytes.length + 2; } }
上面的代码我们看到这家伙类似Java中的Map 。也就是一个key和value 结构的对象。感觉So easy ,没什么大的用途。简单?那是你想简单了,后面大招来了。
很简单的代码演示如下:
public class MapCasheTest { static HashMap<String,String> map = new HashMap<>(); public static void main(String[] args) throws Exception { oomTest(); } private static void oomTest() throws Exception{ // 休眠几秒,便于观察堆内存使用情况 TimeUnit.SECONDS.sleep(30); int result = 0 ; while (true){ String string = new String(new byte[1024*1024]) ; map.put(result+"",string) ; result++; } } }
运行一小会就报这个错了,也是文章中刚开始说的那个错误。
然后我们去监控系统的堆栈使用情况如下图(只用一小会就把堆内存快用满了,然后自然系统就报错了)
下面我们使用同样的逻辑写如下代码,很神奇的事情出现了,大跌眼镜的事情出现了,先亮出代码如下:
public static void main(String[] args) throws Exception{ TimeUnit.SECONDS.sleep(30); OHCache ohCache = OHCacheBuilder.<String, String>newBuilder() .keySerializer(new Test.StringSerializer()) .valueSerializer(new Test.StringSerializer()) .build(); int result = 0 ; while (true){ String string = new String(new byte[2048*2048]) ; ohCache.put(result+"",string) ; result++; } }
程序一直很稳定的运行,没有报错,堆栈运行一上一下,至少程序没报错:
为什么会出现这种情况呢?因为文章开始就讲了这个用的是堆外内存,也就是用的自己电脑的内存,自己电脑的内存目前普通的电脑也有2个G那么大,所以程序一直运行稳定。下面看看我们CPU运行的情况,刚开始有点高,当我把程序关闭CPU使用立马下来了。
哈哈哈,如果是自己测试的时候记得要把自己的工作相关的东西都先保存了,免得你的电脑内存太小,有可能会造成电脑关机。
那么我们在什么情况会使用这个大神的工具呢?就是自己程序有大的对象,并且GC一直无法把这个大对象回收,就可以使用上面的方法,能保证程序稳定运行。具体OH大神是怎么实现这种方式的呢?本文不做研究,他用的技术太深了。
最近ChatGPT比较火,我也问了些问题,回得很好,给个赞。你如果有啥想问的我也可以帮你问问它。
欢迎关注微信公众号:程序员xiaozhang 。会更新更多精彩内容。