java堆外内存详解(又名直接内存)和ByteBuffer

Posted java叶新东老师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java堆外内存详解(又名直接内存)和ByteBuffer相关的知识,希望对你有一定的参考价值。

堆内内存

java的内存分为堆内内存和堆外内存,在了解堆外内存之前,先看看堆内内存是啥,堆内内存是受jvm管控的,也就是说,堆内内存由jvm负责创建和回收;创建和回收都是自动进行的,不需要人为干预;

什么是堆外内存

堆外内存又叫直接内存,是和操作系统内存直接挂钩的,堆外内存不受jvm的管制,所以可以认为堆外内存是jvm以外的内存空间,虽然不受jvm管控,但是堆外内存还是在java进程里面的,而不是由系统内核直接管理;所以它还是在java进程里面的;(终究逃不出java的手掌心);

堆外内存和堆内内存他俩是没有任何关系的;当我们在使用堆内内存的对象时,如果对象内存占用超过了申请的堆内存,就会产生OOM异常(内存溢出);而堆外内存是直接向操作系统申请新的内存空间,理论上只要操作系统的内存足够,堆外内存想申请多少都行!

为什么需要堆外内存

因为堆外内存不受jvm的管控,因此,它有以下几个优点:

1、减少垃圾回收次数

垃圾回收机制不会回收堆外内存,所以使用堆外内存可以减少垃圾回收次数,提升运行效率;因为垃圾回收工作时会暂停工作线程;

2、 加快复制的速度

因为堆内在flush到远程时,会先复制到堆内内存,在复制到堆外内存,然后在发送;操作系统是不可直接访问堆内内存的,而堆外内存省去了堆内到堆外的复制工作;比如netty框架就是用了直接内存才会如此之快;

堆外内存的缺点

  1. 因为不受jvm管控,所以垃圾回收机制不会回收直接内存的空间,需要用户自己释放内存空间
  2. 堆外内存一旦发生泄漏,很难排查,所以,一定要对堆外内存足够了解再去使用堆外内存;
  3. 不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合;

实验代码

1、 Unsafe类

接下来我们使用Unsafe类来申请1G的直接内存,并且在末尾释放内存,在这期间我们观测堆内内存和操作系统的使用情况;

@Test
    public void test1() throws Exception {
        // 查看内存使用情况
        showHeapSpace();

        // 创建unsafe实例
        Constructor<Unsafe> declaredConstructor = Unsafe.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Unsafe unsafe = declaredConstructor.newInstance();

        // 1G内存空间
        int size = 1024 * 1024* 1024;
        // 创建堆外内存大小为1G,此时只是配置堆外内存的大小,并未申请内存
        long address = unsafe.allocateMemory(size);

        // 初始化堆外内存,传入基础地址address、长度为size,也就是说从address地址开始,一直到 address + size的地址都设为0;
        unsafe.setMemory( address,size,(byte)0);
        // 睡5秒
        TimeUnit.SECONDS.sleep(5);

        // 传入地址位置,设置byte值
        unsafe.putByte(address+1, (byte) 66);
        unsafe.putByte(address+2, (byte) 77);
        unsafe.putByte(address+3, (byte) 88);

        // 查看堆内存使用情况
        showHeapSpace();

        // 获取值
        System.out.println(unsafe.getByte(address+1));
        System.out.println(unsafe.getByte(address+2));
        System.out.println(unsafe.getByte(address+3));

        // 释放堆外内存
        unsafe.freeMemory(address);

        // 查看堆内存使用情况
        showHeapSpace();
    }

    /**
     * 展示堆空间大小
     */
    public void showHeapSpace(){
        long coreSize = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        long maxSize = Runtime.getRuntime().maxMemory() / 1024 / 1024;
        long freeSize = Runtime.getRuntime().freeMemory() / 1024 / 1024;
        System.out.println("当前堆内已申请内存:" + coreSize + "M," +
                "最大内存:"+maxSize+ "M,已申请空闲内存:" + freeSize+ "M");
    }

运行后控制台打印结果如下,可以看到堆内存没什么变化,所以可以得出结论直接内存并未使用到堆内内存;

当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:235M
当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:233M
66
77
88
当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:233M

既然在java层面看不到直接内存的使用情况,那我们就只能看任务管理器了,在win10的任务栏右键打开任务管理器,然后在运行一遍上面的代码;任务管理器的绘图图表如下

根据图片可以看到,图中凸起的部分就是我们刚刚申请到的1G直接内存;因为让绘图的时间长一些,所以延时了5秒,执行到unsafe.setMemory( address,size,(byte)0);就会往操作系统申请内存,执行到unsafe.freeMemory(address);时会立马释放内存;

2、ByteBUffer

相信学习过IO的童鞋们都知道这个类,ByteBUffer 有2种模式,可以使用堆外也可以使用堆内内存,以为本文章的主题是堆外内存,所以在这里我们只测试堆外内存;

    @Test
    public void test() throws Exception {
        showHeapSpace();
        // 使用堆外内存创建1G空间
        ByteBuffer buffer1 = ByteBuffer.allocateDirect(1024*1024*1024);
        showHeapSpace();

        buffer1.put(new byte[]{123});
        // 睡5秒
        TimeUnit.SECONDS.sleep(5);
        showHeapSpace();

        // 释放堆外内存
        DirectBuffer directBuffer = (DirectBuffer) buffer1;
        directBuffer.cleaner().clean();  
        
        // gc方法并不能释放堆外内存
//        System.gc();
    }

运行后打印结果如下,也是是用了直接内存

当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:235M
当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:235M
当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:235M

在来看看任务管理器,也有一个凸起的部分,代表这段代码也使用了直接内存;

ByteBuffer释放堆外内存

网上有些文章说使用cleanerSystem.gc()都可以释放直接内存,但是经过博主试验后发现只有cleaner才可以释放直接内存;调用System.gc()方法后未起作用;这一点也是需要注意的;

另外,如果在运行过程中直接终止java进程的话也会释放直接内存;所以博主认为虽然是直接向操作系统申请的内存,但是这一块内存并不是由操作系统管理的,而是在java进程里面的;

以上是关于java堆外内存详解(又名直接内存)和ByteBuffer的主要内容,如果未能解决你的问题,请参考以下文章

Netty基础必备知识,ByteBuffer和ByteBuf底层原理

Netty基础必备知识,ByteBuffer和ByteBuf底层原理

jvm堆外内存排查详解

jvm堆外内存排查详解

jvm堆外内存排查详解

java jvm之直接内存释放过程