堆内内存
java的内存分为堆内内存和堆外内存,在了解堆外内存之前,先看看堆内内存是啥,堆内内存是受jvm管控的,也就是说,堆内内存由jvm负责创建和回收;创建和回收都是自动进行的,不需要人为干预;
c++ 直接IO
什么是堆外内存
堆外内存又叫直接内存,是和操作系统内存直接挂钩的,堆外内存不受jvm的管制,所以可以认为堆外内存是jvm以外的内存空间,虽然不受jvm管控,但是堆外内存还是在java进程里面的,而不是由系统内核直接管理;所以它还是在java进程里面的;(终究逃不出java的手掌心);
堆外内存和堆内内存他俩是没有任何关系的;当我们在使用堆内内存的对象时,如果对象内存占用超过了申请的堆内存,就会产生OOM异常(内存溢出);而堆外内存是直接向操作系统申请新的内存空间,理论上只要操作系统的内存足够,堆外内存想申请多少都行!
为什么需要堆外内存
因为堆外内存不受jvm的管控,因此,它有以下几个优点:
1、减少垃圾回收次数
垃圾回收机制不会回收堆外内存,所以使用堆外内存可以减少垃圾回收次数,提升运行效率;因为垃圾回收工作时会暂停工作线程;
2、 加快复制的速度
因为堆内在flush到远程时,会先复制到堆内内存,在复制到堆外内存,然后在发送;操作系统是不可直接访问堆内内存的,而堆外内存省去了堆内到堆外的复制工作;比如netty框架就是用了直接内存才会如此之快;
堆外内存的缺点
- 因为不受jvm管控,所以垃圾回收机制不会回收直接内存的空间,需要用户自己释放内存空间
- 堆外内存一旦发生泄漏,很难排查,所以,一定要对堆外内存足够了解再去使用堆外内存;
- 不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合;
实验代码
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释放堆外内存
网上有些文章说使用cleaner
和System.gc()
都可以释放直接内存,但是经过博主试验后发现只有cleaner
才可以释放直接内存;调用System.gc()
方法后未起作用;这一点也是需要注意的;
另外,如果在运行过程中直接终止java进程的话也会释放直接内存;所以博主认为虽然是直接向操作系统申请的内存,但是这一块内存并不是由操作系统管理的,而是在java进程里面的;