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

发布时间:2022-03-01 10:08:45 作者:yexindonglai@163.com 阅读(767)

堆内内存

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

c++ 直接IO

什么是堆外内存

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

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

为什么需要堆外内存

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

1、减少垃圾回收次数

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

2、 加快复制的速度

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

堆外内存的缺点

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

实验代码

1、 Unsafe类

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

  1. @Test
  2. public void test1() throws Exception {
  3. // 查看内存使用情况
  4. showHeapSpace();
  5. // 创建unsafe实例
  6. Constructor<Unsafe> declaredConstructor = Unsafe.class.getDeclaredConstructor();
  7. declaredConstructor.setAccessible(true);
  8. Unsafe unsafe = declaredConstructor.newInstance();
  9. // 1G内存空间
  10. int size = 1024 * 1024* 1024;
  11. // 创建堆外内存大小为1G,此时只是配置堆外内存的大小,并未申请内存
  12. long address = unsafe.allocateMemory(size);
  13. // 初始化堆外内存,传入基础地址address、长度为size,也就是说从address地址开始,一直到 address + size的地址都设为0;
  14. unsafe.setMemory( address,size,(byte)0);
  15. // 睡5秒
  16. TimeUnit.SECONDS.sleep(5);
  17. // 传入地址位置,设置byte值
  18. unsafe.putByte(address+1, (byte) 66);
  19. unsafe.putByte(address+2, (byte) 77);
  20. unsafe.putByte(address+3, (byte) 88);
  21. // 查看堆内存使用情况
  22. showHeapSpace();
  23. // 获取值
  24. System.out.println(unsafe.getByte(address+1));
  25. System.out.println(unsafe.getByte(address+2));
  26. System.out.println(unsafe.getByte(address+3));
  27. // 释放堆外内存
  28. unsafe.freeMemory(address);
  29. // 查看堆内存使用情况
  30. showHeapSpace();
  31. }
  32. /**
  33. * 展示堆空间大小
  34. */
  35. public void showHeapSpace(){
  36. long coreSize = Runtime.getRuntime().totalMemory() / 1024 / 1024;
  37. long maxSize = Runtime.getRuntime().maxMemory() / 1024 / 1024;
  38. long freeSize = Runtime.getRuntime().freeMemory() / 1024 / 1024;
  39. System.out.println("当前堆内已申请内存:" + coreSize + "M," +
  40. "最大内存:"+maxSize+ "M,已申请空闲内存:" + freeSize+ "M");
  41. }

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

  1. 当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:235M
  2. 当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:233M
  3. 66
  4. 77
  5. 88
  6. 当前堆内已申请内存:245M,最大内存:3616M,已申请空闲内存:233M

既然在java层面看不到直接内存的使用情况,那我们就只能看任务管理器了,在win10的任务栏右键打开任务管理器,然后在运行一遍上面的代码;任务管理器的绘图图表如下
在这里插入图片描述
根据图片可以看到,图中凸起的部分就是我们刚刚申请到的1G直接内存;因为让绘图的时间长一些,所以延时了5秒,执行到unsafe.setMemory( address,size,(byte)0);就会往操作系统申请内存,执行到unsafe.freeMemory(address);时会立马释放内存;

2、ByteBUffer

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

  1. @Test
  2. public void test() throws Exception {
  3. showHeapSpace();
  4. // 使用堆外内存创建1G空间
  5. ByteBuffer buffer1 = ByteBuffer.allocateDirect(1024*1024*1024);
  6. showHeapSpace();
  7. buffer1.put(new byte[]{123});
  8. // 睡5秒
  9. TimeUnit.SECONDS.sleep(5);
  10. showHeapSpace();
  11. // 释放堆外内存
  12. DirectBuffer directBuffer = (DirectBuffer) buffer1;
  13. directBuffer.cleaner().clean();
  14. // gc方法并不能释放堆外内存
  15. // System.gc();
  16. }

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

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

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

ByteBuffer释放堆外内存

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

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

关键字Java