文章目录
- 1. 直接内存概述
- 2. 直接内存的使用
- 2.1 Java缓冲区
- 2.2 直接内存
- 3. 直接内存的释放
- 3.1 直接内存释放原理
- 4. 禁用显式回收对直接内存的影响
1. 直接内存概述
下面是 《深入理解 Java 虚拟机 第三版》2.2.7 小节 关于 Java 直接内存的描述。
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致
OutOfMemoryError
异常出现,所以我们放到这里一起讲解。 在 JDK 1.4 中新加入了NIO(New Input/Output)
类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx
等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError
异常。
直接内存常用于NIO操作,用于数据缓冲区。
但是直接内存分配回收成本较高,但读写性能高。
最后就是不受JVM内存回收管理
2. 直接内存的使用
下面的例子使用了两种方式来讲文件拷贝到另外一个地方
- 传统的Java缓冲区
- 直接内存
static final String FROM = "D:\\BaiduNetdiskDownload\\《MYSQL内核:INNODB存储引擎 卷1》.zip";
static final String TO = "D:\\BaiduNetdiskDownload\\《MYSQL内核:INNODB存储引擎 卷1》(1).zip";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io();
directBuffer();
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
2.1 Java缓冲区
Java本身并不具备磁盘读写能力,若想使用磁盘读写的能力,就必须调用操作系统提供的函数
也就是内部会调用本地方法,同时CPU的状态会从用户态切换成内核态
同时内存也会作出相应变化,当切换到内核态的时候,会将磁盘文件先读进系统缓冲区(分次读取),Java是无法使用系统缓冲区,Java就会在堆内存中创建一个Java缓冲区(byte[] buf = new byte[_1Mb]
),Java要想读取到系统缓冲区,就需要将系统缓冲区的数据间接读入到Java缓冲区,Java就能对Java缓冲区进行操作了。
之所以用传统IO效率比较低,是因为磁盘文件需要先读入系统缓冲区,系统缓冲区再读入Java缓冲区,Java才能对磁盘文件进行处理,这里造成了不必要的数据复制。效率因而较低。
2.2 直接内存
当执行了ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
之后,操作系统会划分出一个1MB的内存。
这块内存Java代码可以直接访问,操作系统也可以直接访问。也就相当于Java和操作系统共享的一块内存。
这时候磁盘文件可以读入进直接内存,接着Java代码可以对直接内存进行操作,也就是比传统IO少了一次复制的操作,因而效率较高。
3. 直接内存的释放
下面的代码分配一块1G的直接内存
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
当分配成功之后,在任务管理器可以看见Java程序的内存为1G多
接着将byteBuffer
设置为NULL
,开始垃圾回收
Java程序的内存直接下降1G左右,说明直接内存被释放了。
3.1 直接内存释放原理
前面不是说直接内存不受JVM内存回收管理嘛?为什么垃圾回收之后,直接内存就被释放了?
别急,先来介绍一下直接内存的释放原理
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
Unsafe
是Java底层用来分配直接内存和释放直接的内存的类。但是一般并不建议使用这个类。
分配直接内存是靠Unsafe
类的allocateMemory
方法来实现的,其返回值就是分配内存的地址
而释放内存是靠Unsafe
类的freeMemory
方法实现的。这个方法需要传入需要释放的内存的地址。
也就是说,想要释放直接内存,需要主动调用freeMemory
方法
接着,查看ByteBuffer.allocateDirect(_1Gb)
的源码
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
其内部是new DirectByteBuffer(capacity)
,接着查看这个构造方法
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
//使用Unsafe类分配直接内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
可以看见构造方法里使用unsafe.allocateMemory(size)
来分配直接内存。
那么什么时候调用释放直接内存的方法呢?
需要关注这一行代码
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
其中Deallocator
是一个回调任务对象
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
//释放直接内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
查看其源码,可以发现其实现了Runnable
,它的run
方法调用了释放直接内存的方法unsafe.freeMemory(address);
看完这些,也就是说,想要释放直接内存,就必须调用Deallocator
中的run
方法
接着继续来说说Clear
,Clear
在Java类库中是一个特殊的类型,称为虚引用类型
当虚引用关联的对象被回收时,就会触发虚引用对象的clean
方法
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}
public void clean() {
if (remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
查看clean
源码,可以发现在执行Cleaner.create
方法的时候,会将new Deallocator(base, size, cap)
作为参数传递给Runnable var1
,在create
方法调用new Cleaner(var0, var1)
,将this.thunk
赋值Runnable var1
。
也就是说在clean
方法中的this.thunk.run();
调用的就是Deallocator
中的run
方法。从而释放直接内存。
也就是ByteBuffer
的实现类内部,使用了 Cleaner
(虚引用)来监测 ByteBuffer
对象,一旦 ByteBuffer
对象被垃圾回收,那么就会由 ReferenceHandler
线程通过 Cleaner
的 clean
方法调 用 freeMemory
来释放直接内存
4. 禁用显式回收对直接内存的影响
可以通过下面的参数禁用显式回收
-XX:+DisableExplicitGC 显式的
什么是显式回收?下面就是显式回收的一个例子
System.gc(); // 显式的垃圾回收,Full GC
禁用显式回收之后,这行代码就变成无效了
这行代码无效,可能会直接影响到直接内存的释放
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
同样,就这个例子来说,当执行到System.gc();
并不会触发垃圾回收。
于是,byteBuffer
对象虽然为NULL
,但是并不会被回收掉,于是前面申请的1GB直接内存也不会被释放。
这个byteBuffer
只能等到真正的垃圾回收触发时才会被回收,这个直接内存也随之释放。
这样造成的后果就是直接内存占用比较大。
对应的解决办法就是手动通过Unsafe
释放直接内存。