JAVA堆外内存的简介和使用

3 篇文章 0 订阅
3 篇文章 0 订阅

一:堆外内存是什么?

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

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

堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,也被称为直接内存。

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

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

Unsafe类操作堆外内存

sun.misc.Unsafe提供了一组方法来进行堆外内存的分配,重新分配,以及释放。

1.public native long allocateMemory(long size); —— 分配一块内存空间。

2.public native long reallocateMemory(long address, long size); —— 重新分配一块内存,把数据从address指向的缓存中拷贝到新的内存块。

3.public native void freeMemory(long address); —— 释放内存。

参考:《深读源码-java魔法类之Unsafe解析》

通过反射获取unsafe对象实例,然后进行堆外内存操作:

public class UnsafeTest {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        //获取Unsafe实例
        Field f = Unsafe.class.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);

        //直接操作堆外内存
        unsafe.allocateMemory(100);
        unsafe.reallocateMemory(100, 200);
        unsafe.freeMemory(100);
    }
}

NIO类操作堆外内存

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

public class TestDirectByteBuffer {

    public static void main(String[] args) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
    }
}

注:可能个别读者运行此代码会报错,大概率是内存溢出,建议读者要么把代码里分配的内存大小调小,要么修改JVM参数,设置合理的堆外内存大小。


二:堆外内存垃圾回收

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

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

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

1.堆外内存会溢出么?

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

问题一

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

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

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

可以看到出现OOM。

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

问题二

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

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

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

这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。

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

当下一次FGC执行时,Cleaner对象会将自身Cleaner链表上删除,并触发clean方法清理堆外内存。

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

如果JVM一直没有执行FGC的话,无法触发Cleaner对象执行clean方法,从而堆外内存也一直得不到释放。

其实,在ByteBuffer.allocateDirect方式中,会主动调用System.gc()强制执行FGC。

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

显式调用

三:为什么用堆外内存?

堆外内存的使用场景非常巧妙。

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

摘了其中一段。

When using a very huge number of objects in a very large heap, Virtual machines will suffer from increased GC pressure since it basically has to inspect each and every object whether it can be collected and has to access all memory pages. A cache shall keep a hot set of objects accessible for fast access (e.g. omit disk or network roundtrips). The only solution is to use native memory - and there you will end up with the choice either to use some native code (C/C++) via JNI or use direct memory access.

大概的意思如下:

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

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

参考:OHC - An off-heap-cache — Github

四:如何用堆外内存?

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

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

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

那是否有一个包,支持分配堆外内存,又支持KV操作,还无需关心GC。

答案当然是有的。

有一个很知名的包,Ehcache

Ehcache被广泛用于Spring,Hibernate缓存,并且支持堆内缓存,堆外缓存,磁盘缓存,分布式缓存。

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

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

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.28</version>
</dependency>

 

import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.ehcache.Cache;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.ResourcePools;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.MemoryUnit;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * @author suidd
 * @name HelloHeapServiceImpl
 * @description Ehcach测试堆外内存demo
 * @date 2020/5/20 16:53
 * Version 1.0
 **/
public class HelloHeapService {
    //堆内缓存map
    private static Map<String, InHeapClass> inHeapCache = new HashMap<>();
    //堆外内存缓存
    private static Cache<String, OffHeapClass> offHeapCache;

    static {
        //用Ehcache新建了一个堆外缓存,缓存大小为1MB
        ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
                .offheap(1, MemoryUnit.MB)
                .build();
        CacheConfiguration<String, OffHeapClass> configuration = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(String.class, OffHeapClass.class, resourcePools)
                .build();
        offHeapCache = CacheManagerBuilder.newCacheManagerBuilder()
                .withCache("cacher", configuration)
                .build(true)
                .getCache("cacher", String.class, OffHeapClass.class);

        //在两种缓存中,都放入10000个对象
        for (int i = 1; i < 10001; i++) {
            inHeapCache.put("InHeapKey" + i, new InHeapClass("InHeapKey" + i, "InHeapValue" + i));
            offHeapCache.put("OffHeapKey" + i, new OffHeapClass("OffHeapKey" + i, "OffHeapValue" + i));
        }
    }

    @Data
    @AllArgsConstructor
    private static class InHeapClass implements Serializable {
        private String key;
        private String value;
    }

    @Data
    @AllArgsConstructor
    private static class OffHeapClass implements Serializable {
        private String key;
        private String value;
    }

    public void helloHeap() {
        System.out.println(JSON.toJSONString(inHeapCache.get("InHeapKey1")));
        System.out.println(JSON.toJSONString(offHeapCache.get("OffHeapKey1")));
        Iterator iterator = offHeapCache.iterator();
        int sum = 0;
        while (iterator.hasNext()) {
            System.out.println(JSON.toJSONString(iterator.next()));
            sum++;
        }
        System.out.println(sum);
    }

    public static void main(String[] args) {
        //helloHeap方法做get测试,并统计堆外内存数量,验证先插入的对象是否被淘汰
        HelloHeapService service = new HelloHeapService();
        service.helloHeap();
    }
}

其中.offheap(1, MemoryUnit.MB)表示分配的是堆外缓存。

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

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

2.用Ehcache新建了一个堆外缓存,缓存大小为1MB。

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

4.helloHeap方法做get测试,并统计堆外内存数量,验证先插入的对象是否被淘汰。

使用Java VisualVM工具Dump一个内存镜像。

Java VisualVM是JDK自带的工具。

工具位置如下:

/Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/Contents/Home/bin/jvisualvm

也可以使用JProfiler工具。

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

内存镜像

接着测试helloHeap方法。

输出:

{"key":"InHeapKey1","value":"InHeapValue1"}
null
……(此处有大量输出)
5887

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

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

五、总结

使用堆外内存的优点:

1、减少了垃圾回收
因为垃圾回收会暂停其他的工作。

2、加快了复制的速度
堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。

同样任何一个事物使用起来有优点就会有缺点,堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。

 

理解的不是很透彻,后续会继续阅读相关技术博客并整理!


原文链接:https://www.jianshu.com/p/17e72bb01bf1

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值