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 转载自:李会军•宁静致远 最近由于工作关系要做一 ...
随机推荐
- python下SQLAlchemy的使用
SQLAlchemy是python中orm常用的框架.支持各种主流的数据库,如SQLite.MySQL.Postgres.Oracle.MS-SQL.SQLServer 和 Firebird. 在安装 ...
- win2008环境mysql主从配置
一.主库相关配置.设置 step1:主库配置文件 [mysqld] # 数据库id,唯一 server-id = 1# 二进制日志文件,必填项,否则不能同步数据;如果不取名字的话,会以计算机的名字加编 ...
- vmware vSphere Data Protection 6.1 使用备份、恢复、报告
一.6个选项卡说明 1.getting started 开始,提供VDP功能概述以及指向创建备份作业向导.恢复向导.报告选项卡的快速连接 2.backup 提供已计划备份作业的列表以及有关备份作业的详 ...
- Hive 学习之路(三)—— Hive CLI和Beeline命令行的基本使用
一.Hive CLI 1.1 Help 使用hive -H或者 hive --help命令可以查看所有命令的帮助,显示如下: usage: hive -d,--define <key=value ...
- Azkaban学习之路(一)—— Azkaban 简介
一.Azkaban 介绍 1.1 背景 一个完整的大数据分析系统,必然由很多任务单元(如数据收集.数据清洗.数据存储.数据分析等)组成,所有的任务单元及其之间的依赖关系组成了复杂的工作流.复杂的工作流 ...
- 跟我学SpringCloud | 第八篇:Spring Cloud Bus 消息总线
SpringCloud系列教程 | 第八篇:Spring Cloud Bus 消息总线 Springboot: 2.1.6.RELEASE SpringCloud: Greenwich.SR1 如无特 ...
- [apue] popen/pclose 疑点解惑
问题请看这里: [apue] 使用 popen/pclose 的一点疑问 当时怀疑是pclose关闭了使用完成的管道,因此在pclose之前加一个足够长的sleep,再次观察进程文件列表: 哈哈,这下 ...
- POJ 2728:Desert King(最优比率生成树)
http://poj.org/problem?id=2728 题意:有n个点,有三个属性代表每个点在平面上的位置,和它的高度.点与点之间有一个花费:两点的高度差:还有一个长度:两点的距离.现在要让你在 ...
- BZOJ 2957:楼房重建(分块)
http://www.lydsy.com/JudgeOnline/problem.php?id=2957 题意:…… 思路:对于每一个块,维护一个单调递增的斜率(因为小于前面的斜率的话是肯定看不见的) ...
- HttpUtility.UrlEncode讲解
hello 大家好,今天讲讲HttpUtility.UrlEncode编码 HttpUtility.UrlEncode方法有4个重载分别如下 我们有这么一个字符串 string str = " ...