一.结论

  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 的使用与研究的更多相关文章

  1. java nio 缓冲区(一)

      本文来自于我的个人博客:java nio 缓冲区(一) 我们以Buffer类開始对java.nio包的浏览历程.这些类是java.nio的构造基础. 这个系列中,我们将尾随<java NIO ...

  2. Java NIO ByteBuffer 的使用与源码研究

    一.结论 ByteBuffer 是Java NIO体系中的基础类,所有与Channel进行数据交互操作的都是以ByteBuffer作为数据的载体(即缓冲区).ByteBuffer的底层是byte数组, ...

  3. 【Java NIO的深入研究】 ServerSocketChannel

    Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样.ServerSocketChannel类在 jav ...

  4. 【Java NIO的深入研究6】JAVA NIO之Scatter/Gather

    Java NIO开始支持scatter/gather,scatter/gather用于描述从Channel(译者注:Channel在中文经常翻译为通道)中读取或者写入到Channel的操作. 分散(s ...

  5. 【JavaNIO的深入研究4】内存映射文件I/O,大文件读写操作,Java nio之MappedByteBuffer,高效文件/内存映射

    内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件.有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问.这种解决办法能大大简化修改文件的代码.fileC ...

  6. 【Java NIO深入研究3】文件锁

    1.1概述——文件锁 文件锁定初看起来可能让人迷惑.它 似乎 指的是防止程序或者用户访问特定文件.事实上,文件锁就像常规的 Java 对象锁 — 它们是 劝告式的(advisory) 锁.它们不阻止任 ...

  7. 深入理解Java NIO

    初识NIO: 在 JDK 1. 4 中 新 加入 了 NIO( New Input/ Output) 类, 引入了一种基于通道和缓冲区的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存 ...

  8. JAVA NIO学习笔记1 - 架构简介

    最近项目中遇到不少NIO相关知识,之前对这块接触得较少,算是我的一个盲区,打算花点时间学习,简单做一点个人学习总结. 简介 NIO(New IO)是JDK1.4以后推出的全新IO API,相比传统IO ...

  9. Java NIO使用及原理分析(1-4)(转)

    转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...

随机推荐

  1. Linux使用daemontools

    功能: 在使用memcached时候,怕因为一些不可预知的因素导致memcached进程死掉,而又不能及时的发现重启,可以通过daemontools来管理memcached的启动,当memcached ...

  2. Flume NG高可用集群搭建详解

    .Flume NG简述 Flume NG是一个分布式,高可用,可靠的系统,它能将不同的海量数据收集,移动并存储到一个数据存储系统中.轻量,配置简单,适用于各种日志收集,并支持 Failover和负载均 ...

  3. Tido 习题-二叉树-区间查询

    题目描述 食堂有N个打饭窗口,现在正到了午饭时间,每个窗口都排了很多的学生,而且每个窗口排队的人数在不断的变化.现在问你第i个窗口到第j个窗口一共有多少人在排队? 输入 输入的第一行是一个整数T,表示 ...

  4. Spring Cloud Gateway使用

    简介 Spring Cloud Gateway是Spring Cloud官方推出的网关框架,网关作为流量入口,在微服务系统中有着十分重要的作用,常用功能包括:鉴权.路由转发.熔断.限流等. Sprin ...

  5. Apple官文中的KVO 与 FBKVOController

    前言 本文将主要介绍以下内容: 详细列出Apple官文中KVO的注意事项(Apple KVO相关的引用皆摘自Apple官文). 介绍FBKVOController,以及它如何避免系统提供的KVO坑点. ...

  6. 高并发架构系列:Redis缓存和MySQL数据一致性方案详解

    一.需求起因 在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节.所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库. 这个业务场景, ...

  7. kindeditor在线文本编辑器过滤HTML的方法

    在使用kindeditor文本编辑器时遇到的问题,客户直接从Excel里粘贴文本内容到文本编辑器中(能不能再懒一些),然后不调整粘贴内容直接就保存(你敢不敢再懒一些)!对于这种很无语的行径,我只能对他 ...

  8. 虚拟机linux下git clone 报SSL connect error错误

    今天在安装azkaban时,用git clone https://github.com/azkaban/azkaban.git,虚拟机报了SSL connect error,翻了很多博客,有的说是gi ...

  9. 再见Jenkins,从Gitlab代码提交到k8s服务持续交付只需七毛三(走过路过不要错过)

    Gitlab runner 快速搭建CICD pipeline 背景 日常开发中,相信大家已经做了很多的自动化运维环境,用的最多的想必就是利用Jenkins实现代码提交到自动化测试再到自动化打包,部署 ...

  10. Bzoj 3874: [Ahoi2014&Jsoi2014]宅男计划 三分+贪心

    3874: [Ahoi2014&Jsoi2014]宅男计划 Time Limit: 1 Sec  Memory Limit: 256 MBSubmit: 861  Solved: 336[Su ...