likes
comments
collection
share

使用缓存框架OHC操作堆外内存,减少GC

作者站长头像
站长
· 阅读数 22

简介

通过本文入门堆外内存,使用缓存框架OHC操作堆外内存,优化内存使用,减少GC。文章包含OHC源码的简单分析和本人在项目中对OHC的应用。

快速开始

堆(heap)是JVM运行时数据区中的一部分,咱们new出来的对象都存放在堆内存中,由JVM使用GC等手段帮大家管理。

JVM帮咱们管是省事了,但是如果堆中数据量很大,GC的频率就会增加,可能会影响系统的效率,这时咱们就可以考虑下堆外内存。堆内内存会受到GC的影响,但是堆外内存GC可管不着了。这样我们就可以根据业务需求或是存储对象的特点,选择不同的地方(堆内or堆外)存储对象。

下面咱们三步走战略快速开始OHC:引入依赖 -> 创建实例 -> 使用实例,把堆外内存用起来。


第一步,引入依赖

<dependency>
    <groupId>org.caffinitas.ohc</groupId>
    <artifactId>ohc-core</artifactId>
    <version>0.7.4</version>
</dependency>

第二步,创建OHC实例OHCache ohCache

OHCache ohCache = OHCacheBuilder.newBuilder()
                                .keySerializer(yourKeySerializer)
                                .valueSerializer(yourValueSerializer)
                                .build();

创建实例时需要设置yourKeySerializeryourValueSerializer两个参数,这两个玩意用于序列化OHC中存储的key和value,与key和value的类型有关。这两个参数是两个类,需要我们自己实现,咱先不管,现在只需要知道创建实例时,需要传入一些参数。


第三步,使用OHC实例OHCache ohCache

ohCache.put("hello", "world");
String value = ohCache.get("hello");

这么一看,缓存框架OHC用起来就像个Map。咱们就先把这东西当成个Map,只不过存储的位置不是咱们熟悉的JVM堆内存,而是堆外内存。


马蜂窝技术团队提供了一个使用OHC的Demo,有兴趣可以先试试运行起来:chebacca/ohc-example (github.com)

浅析源码

最关心的肯定是内存如何分配,其次是内存如何释放,咱们就依次看一下这两个部分的源码。

内存分配

探索下OHC是如何分配堆外内存的。

点开put方法,发现有两个实现类,咱们需要的的是OHCacheLinkedImpl,另一个实现类OHC的作者说现在还处于实验阶段。

一路点下去,发现这行代码:

oldValueAdr = Uns.allocate(oldValueLen, throwOOME);

再进去,发现这个:

long address = allocator.allocate(bytes);

这个allocator是个接口,点进去发现两个实现类,分别是UnsafeAllocatorJNANativeAllocator,这两个就是分配堆外内存的两种方法,OHC默认使用JNANativeAllocator,但是我们也可以通过配置,使用UnsafeAllocator分配堆外内存。下面分别看一下这两种分配堆外内存的方式。

UnsafeAllocator

看这个实现类的名字就能猜到,这东西跟unsafe类脱不了关系,点开一看果然。

先是经典反射拿到unsafe类,然后一行代码:

return unsafe.allocateMemory(size);

人家unsafe注释写的很明白,申请的是native memory

Allocates a new block of native memory, of the given size in bytes.

JNANativeAllocator

点开JNANativeAllocator,一眼就看到这行:

return Native.malloc(size);

虽然以前没见过,看名字也能猜到,这东西是个本地方法(native method)。点开一看,果然:public static native long malloc(long var0);

还有一个问题,com.sun.jna.Native#malloc是在jna包下,JNANativeAllocator中也有jna,这jna到底是个什么东西。

说jna前先说jni,jni是Java Native Interface,sun.misc.Unsafe#allocateMemory就是jni的方式,而JNA是建立在JNI之上的一个高级库,简化了Java和本地代码之间的交互过程。

但是无论如何,两者底层都是调用了C语言中的malloc() 函数来分配本地内存。

内存释放

释放就很好说了,一个用的是sun.misc.Unsafe#freeMemory,一个用的是com.sun.jna.Native#free,但是可以肯定的是,都需要我们手动释放。如果写C或者C++可能觉得这是天经地义的,毕竟你向操作系统借了一块内存,那肯定要还的。但是还内存这一步,Java程序员平时都是全权交由JVM管理的。

其他方法

抛开上面提到的两种操作堆外内存的方式,可能java.nio.ByteBuffer#allocateDirect这个方法会听的更多。

简单说,这是对sun.misc.Unsafe#allocateMemory的封装,让咱们可以在较安全的情况下操作堆外内存。只需一行代码,如下:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);

ByteBuffer.allocateDirect可以通过Cleaner(虚引用)+显示GC(system.gc())+unsafe.freeMemory的方式可以自动释放内存。这部分就不展开讲了,感兴趣可以了解下。

项目实战

优化前

我先介绍下业务场景:在广告检索系统中,我使用ConcurrentHashMap构建hash索引。Map中key为广告id,value为广告对象,这样可以构造广告id -> 广告对象这样一个映射,达到广告id快速拿取广告对象的目的。

注意这是一个索引,意味着全量广告数据存储在内存中,那么数据量一大,咱们的堆内内存就必然不够用了(后面有测试)。

简单来说,使用OHC优化前,咱们的索引长这样:

private static final Map<Long, CreativeObject> MAP;
static {
    MAP = new ConcurrentHashMap<>();
}

public long size(){
    return MAP.size();
}
@Override
public CreativeObject get(Long key) {
    return MAP.get(key);
}
@Override
public void add(Long key, CreativeObject value) {
    MAP.put(key, value);
}

通过索引取数据就是MAP.get(key),添加索引数据就是MAP.put(key, value)


前面咱们也提到了,主要就是内存空间的问题,才想着大费周章操作堆外内存。以我使用的64位JVM为例,估算下每个广告id -> 广告对象映射,到底需要多少内存空间。

引入依赖,用这个能知道某个对象总共占用了多少内存空间:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

测试代码:

测试结果:

CreativeObject对象总大小为304byte,Long对象总大小为24byte,一组映射占用对内存大小为328byte(根据存储数据不同,映射大小也会不同)。我的测试数据有350万条广告,如果全部存储到内存中,大约需要1.069GB的空间。这个数对不对呢,先记住这个数时1GB多一点,咱们后面会验证。

数据记录

数据量:350万CreativeObject对象


  1. 静息时内存使用情况: 使用缓存框架OHC操作堆外内存,减少GC

  2. 堆外内存使用时:used heap 1.171GB 使用缓存框架OHC操作堆外内存,减少GC

  3. 堆内内存使用时:used heap 2.287GB 使用缓存框架OHC操作堆外内存,减少GC

通过对比2和3的堆内存使用情况(used heap),OHC确实起到了减少堆内内存的使用。

使用OHC改进

直接上代码:

private static final OHCache<Long, CreativeObject> OHC;
static {
    OHC = OHCacheBuilder.<Long, CreativeObject>newBuilder()
            .keySerializer(new LongSerializer())
            .valueSerializer(new CreativeObjectSerializer())
            .build();
}

public long size(){
    return OHC.size();
}
@Override
public CreativeObject get(Long key) {
    return OHC.get(key);
}
@Override
public void add(Long key, CreativeObject value) {
    OHC.put(key, value);
}

类比Map来看OHC的操作,发现也就是get()set()这些方法。

但是咱们还有个小问题需要解决,就是keySerializer、valueSerializer这两个参数。其实也简单,我给出Long类型的序列化类,CreativeObject的序列化类太长了就不给了,你只需要根据自己需要存储的类型,写序列化类就OK了。

public class LongSerializer implements CacheSerializer<Long> {
    @Override
    public void serialize(Long key, ByteBuffer buf) {
        buf.putLong(key);
    }

    @Override
    public Long deserialize(ByteBuffer buf) {
        return buf.getLong();
    }

    @Override
    public int serializedSize(Long key) {
        return Long.BYTES;
    }
}

必须考虑的问题

申请堆外内存就必须考虑释放内存,就像使用ReentrantLock必须考虑unlock,就像使用threadlocal必须考虑remove,就像吸气必须呼气。

结合业务场景(广告检索中的一个hash索引),让我们想想什么时候删除堆外内存中的存储的广告数据?应该是广告数据过期的时候。

基于此,我们使用惰性删除,即使用索引时再判断数据是否过期,那么问题来了,此时应该直接将数据从堆外内存中删除么?可别忘了,堆外内存也是个共享数据啊,操作一切共享数据,都要考虑并发问题。

为了并发安全的删除堆外内存,我将使用一个删除队列异步删除堆外内存,设计如下:

  1. 选择广告id(这一步是业务相关,就当是从广告池中随机选了一个就行)
  2. 判断是否在删除队列中
    • 在,重新选择广告id
    • 不在,继续下一步
  3. 根据广告id从OHC中拿到广告对象
  4. 判断是否过期
    • 过期,加入删除队列,重新选择广告id
    • 不过期,返回广告对象

然后我们只需要处理删除队列就行了。

回顾

回顾一下我们干了什么。开始时我们的数据存放在堆内内存的Map里,为了降低GC频率,我们使用OHC把这些数据从堆内内存挪到了堆外内存。没了,我们其实就干了这一件事。

其他

原理知识

JNA

java-native-access/jna: Java Native Access (github.com)

JNA(Java Native Access)是一个开源的Java框架,是Sun公司推出的一种调用本地方法的技术,是建立在经典的JNI基础之上的一个框架。之所以说它是JNI的替代者,是因为JNA大大简化了调用本地方法的过程,使用很方便,基本上不需要脱离Java环境就可以完成。

JNI(Java Native Interface)即Java本地接口,它建立了Java与其他编程语言的桥梁,允许Java程序调用其他语言(尤其是C/C++)编写的程序或者代码库。并且,JDK本身的实现也大量用到JNI技术来调用本地C程序库。

本文灵感

Java堆外缓存OHC在马蜂窝推荐引擎的应用 (qq.com)

参考资料

snazy/ohc: Java large off heap cache (github.com)

Java 内存之直接内存(堆外内存) · 日常学习 · 看云 (kancloud.cn)

Java 堆内内存与堆外内存 - 腾讯云开发者社区-腾讯云 (tencent.com)

10 双刃剑:合理管理 Netty 堆外内存.md (lianglianglee.com)

JVM源码分析之堆外内存完全解读-阿里云开发者社区 (aliyun.com)

转载自:https://juejin.cn/post/7261237299713900604
评论
请登录