Java NIO DirectByteBuffer 的使用与研究
一.结论
DirectByteBuffer 与 ByteBuffer 最大区别就在于缓冲区内存管理的方式。ByteBuffer使用的是堆内存,DirectByteBuffer 使用的是堆外内存,堆外内存的优点就是在执行I/O操作时数据拷贝的次数相对较少,因此也获得了较高的性能。凡事总有但是,由于将缓冲区分配在堆外内存也引入一系列与内存分配和回收的问题,所幸JDK提供了一系列方案来解决问题,这些也是本文所要阐述的重点。
二.ByteBuffer 的缺点
I/O基本上可以视为一系列倒腾数据的操作。举例来说,商品经中间商倒腾的次数越少,其价格越便宜;对于I/O来说,拷贝字节数组的次数越少,其I/O性能也就越高。而ByteBuffer性能低下的原因就是在使用ByteBuffer进行I/O操作时会执行以下操作:
1.将堆内存中缓冲区数据拷贝到临时缓冲区
2.对临时缓冲区的数据执行低层次I/O操作
3.临时缓冲区对象离开作用域,并最终被回收成为无用数据
与之相对,DirectByteBuffer 由于将内存分配在了堆外内存因此可以直接执行较低层次的I/O操作数据,减少了拷贝次数因此也获得了较高的性能。
问题来了,为什么不直接在堆内存中的缓冲区执行低层次的I/O操作呢?
推测最主要的原因就是,JVM的垃圾回收操作会移动对象在堆内存中的位置,以实现内存的清理,因此,如果直接在堆内存中的缓冲区执行可能会发现缓冲区内存地址变化的情况,也就无从执行I/O操作。
三.DirectByteBuffer 内存申请与回收
由于DirectByteBuffer的 API使用与ByteBuffer并无太大的区别,因此本文将集中研究DirectByteBuffer是如何执行内存申请操作,以及如何对其进行内存回收操作。
3.1.内存申请
在构造DirectByteBuffer时就已经执行了内存申请操作,其中我们主要关注 Bits.reserveMemory(size, cap) 以及 Cleaner.create(this, new Deallocator(base, size, cap))。
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 {
//申请堆外内存,返回缓冲区内存的首地址
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;
}
//此行代码用于实现当DirectByteBuffer被回收时,堆外内存也会被释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
Bits.reserveMemory
static void reserveMemory(long size, int cap) {
// 设置最大内存设置
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// 乐观地尝试预定直接内存(DirectMemory)的内存
// optimist!
if (tryReserveMemory(size, cap)) {
return;
}
// 如果预定内存失败,则对直接内存中无用的内存执行回收操作
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// retry while helping enqueue pending Reference objects
// which includes executing pending Cleaner(s) which includes
// Cleaner(s) that free direct buffer memory
while (jlra.tryHandlePendingReference()) {
if (tryReserveMemory(size, cap)) {
return;
}
}
// 触发GC操作
// trigger VM's Reference processing
System.gc();
// 执行多次循环,尝试进行内存回收操作,如果多次尝试失败之后,则抛出OutOfMemory异常
// a retry loop with exponential back-off delays
// (this gives VM some time to do it's job)
boolean interrupted = false;
try {
long sleepTime = 1;
int sleeps = 0;
while (true) {
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
if (!jlra.tryHandlePendingReference()) {
try {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
}
// no luck
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
tryReserveMemory 此方法的主要功能就是检查当前DirectMemory内存是否足够构建DirectByteBuffer的缓冲区,并通过CAS的方式设置当前已使用的内存
//尝试预定内存
private static boolean tryReserveMemory(long size, int cap) {
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
long totalCap;
//检查内存是否足够
while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
//如果内存足够,则尝试CAS设置totalCapacity
if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) {
reservedMemory.addAndGet(size);
count.incrementAndGet();
return true;
}
}
return false;
}
jlra.tryHandlePendingReference 为什么可以执行内存回收操作呢?其原理如下节所示。
3.2.内存释放
结论:DirectByteBuffer中的直接内存缓冲区释放的方式有两种
1.ReferenceHandler线程会自动检查有无被回收的DirectByteBuffer,如果有则执行Cleaner.clean方法释放其对应的直接内存
2.通过调用SharedSecrets.getJavaLangRefAccess()方法来释放内存,具体见Reference类代码分析。
3.2.1代码分析
此句代码便是直接内存释放的关键了。
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);
} }
Cleaner 内部维护着一个双向队列,此类的定义如下所示。
请注意以下关键点:
Cleaner 继承了PhantomReference幽灵引用,并且维护了一个ReferenceQueue<Object> 队列。
public class Cleaner extends PhantomReference<Object> {
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
private static Cleaner first = null;
private Cleaner next = null;
private Cleaner prev = null;
private final Runnable thunk;
private static synchronized Cleaner add(Cleaner var0) {
if (first != null) {
var0.next = first;
first.prev = var0;
}
first = var0;
return var0;
}
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));
}
// 执行Dealloactor.run()
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;
}
});
}
}
}
}
而幽灵引用的定义如下所示:
public class PhantomReference<T> extends Reference<T> {
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
问题来了,说了这么多到底是谁在调用DirectByteBuffer的内存回收代码(Cleaner.clean() -> Deallocator.run())
Reference中代码说明了一切:
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
}
ReferenceHandler的主要代码如下所示,主要是不断的执行tryHandlePending 方法
public void run() {
while (true) {
tryHandlePending(true);
}
}
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// Cleaner.clean 方法调用处
// Fast path for cleaners
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
Java NIO DirectByteBuffer 的使用与研究的更多相关文章
- java nio 缓冲区(一)
本文来自于我的个人博客:java nio 缓冲区(一) 我们以Buffer类開始对java.nio包的浏览历程.这些类是java.nio的构造基础. 这个系列中,我们将尾随<java NIO ...
- Java NIO ByteBuffer 的使用与源码研究
一.结论 ByteBuffer 是Java NIO体系中的基础类,所有与Channel进行数据交互操作的都是以ByteBuffer作为数据的载体(即缓冲区).ByteBuffer的底层是byte数组, ...
- 【Java NIO的深入研究】 ServerSocketChannel
Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样.ServerSocketChannel类在 jav ...
- 【Java NIO的深入研究6】JAVA NIO之Scatter/Gather
Java NIO开始支持scatter/gather,scatter/gather用于描述从Channel(译者注:Channel在中文经常翻译为通道)中读取或者写入到Channel的操作. 分散(s ...
- 【JavaNIO的深入研究4】内存映射文件I/O,大文件读写操作,Java nio之MappedByteBuffer,高效文件/内存映射
内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件.有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问.这种解决办法能大大简化修改文件的代码.fileC ...
- 【Java NIO深入研究3】文件锁
1.1概述——文件锁 文件锁定初看起来可能让人迷惑.它 似乎 指的是防止程序或者用户访问特定文件.事实上,文件锁就像常规的 Java 对象锁 — 它们是 劝告式的(advisory) 锁.它们不阻止任 ...
- 深入理解Java NIO
初识NIO: 在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 类, 引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存 ...
- JAVA NIO学习笔记1 - 架构简介
最近项目中遇到不少NIO相关知识,之前对这块接触得较少,算是我的一个盲区,打算花点时间学习,简单做一点个人学习总结. 简介 NIO(New IO)是JDK1.4以后推出的全新IO API,相比传统IO ...
- Java NIO使用及原理分析(1-4)(转)
转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...
随机推荐
- 老雷socket编程之PHP利用socket扩展实现聊天服务
老雷socket编程之PHP利用socket扩展实现聊天服务 socket聊天服务原理 PHP有两个socket的扩展 sockets和streamssockets socket_create(AF_ ...
- 小白也能看懂的 Laravel 核心概念讲解
自动依赖注入 什么是依赖注入,用大白话将通过类型提示的方式向函数传递参数. 实例 1 首先,定义一个类: /routes/web.php class Bar {} 假如我们在其他地方要使用到 Bar ...
- 带你手写基于 Spring 的可插拔式 RPC 框架(一)介绍
概述 首先这篇文章是要带大家来实现一个框架,听到框架大家可能会觉得非常高大上,其实这和我们平时写业务员代码没什么区别,但是框架是要给别人使用的,所以我们要换位思考,怎么才能让别人用着舒服,怎么样才能让 ...
- MxNet 模型转Tensorflow pb模型
用mmdnn实现模型转换 参考链接:https://www.twblogs.net/a/5ca4cadbbd9eee5b1a0713af 安装mmdnn pip install mmdnn 准备好mx ...
- python函数之 range()
Python range() 函数用法 python range() 函数可创建一个整数列表,一般用在 for 循环中. 函数语法 range(start, stop[, step]) 参数说明: ...
- JavaScript 基础知识 变量与数据类型
一.区分大小写 JS中一切(变量/函数名/操作符)都是严格区分大小写的 二.标识符 变量.函数.属性的名字以及函数的参数 命名规则:1.第一个字符可以是字母.下划线.美元符号$ 2.其他的字符可以是字 ...
- 记一次SQL优化。
程序是数据库的用户,为打造良好的用户体验,我们一直在努力. 此次介绍一个基于SQL的数据库优化.SQL的优劣对数据库的性能影响非常关键. 查询只涉及如下表结构中的三个字段.如下 开发原始SQL SEL ...
- 认识Mybatis的一二级缓存
认识Mybatis的一二级缓存 一次完整的数据库请求,首先根据配置文件生成SqlSessionFactory,再通过SqlSessionFactory开启一次SqlSession,在每一个SqlSes ...
- Modbus 指令
本节内容: 一.S7-1200 作为Modbus RTU 主站 二.S7-1200 作为Modbus RTU 从站 三.S7-1200 作为Modbus RTU 主站 S7-1200 作为Modbus ...
- 关于网页授权access_token和普通access_token的区别
关于网页授权access_token和普通access_token的区别 1.微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授 ...