Direct Buffer

前言

上篇文章Buffer末尾中谈到堆内Buffer(Heap Buffer)和直接Buffer(Direct Buffer)的概念,但是却一笔带过,并未涉及其细节,这篇文章继续聊聊Buffer——Direct Buffer

  • Direct Buffer是什么
  • Direct Buffer和Heap Buffer的区别
  • 用来干什么
  • Direct Buffer和JVM

另外需要说明的是,为了叙述的方便,和上篇Buffe文章类似,本文中以DirectByteBuffer为例介绍。

二.是什么?

Direct vs. non-direct buffers

A direct byte buffer may be created by invoking the allocateDirect factory method of this class. The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers. The contents of direct buffers may reside outside of the normal garbage-collected heap, and so their impact upon the memory footprint of an application might not be obvious. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations. In general it is best to allocate direct buffers only when they yield a measureable gain in program performance.

以上内容引用ByteBuffer的java api描述,可移步ByteBuffer,这里先根据以上内容总结下直接内存是个啥。

说到底,DirectByteBuffer引用空间就是一块内存,不过该块内存通常被分配在jvm的堆区域外,即该块内存不在堆区域内,常称作:堆外内存,下面以堆外内存叙述。

还可以参考R大(RednaxelaFX)的回复Java NIO中,关于DirectBuffer,HeapBuffer的疑问?

三.区别

上面说到堆外内存是什么,那么它和堆内内存有啥区别?

  • 堆内内存,jvm运行时区域的一部分,即堆区,分为年轻代、老年代。该块的内存管理委托给jvm。

  • 堆外内存,堆内内存以外的都可以称为堆外内存。关于堆外内存的理解,可以参考笨神(你假笨)的一篇文章JVM源码分析之堆外内存完全解读

如下图,描述了它们之间的关系:

jvm说到底也是运行在操作系统上的应用程序,java应用程序运行在jvm上时,jvm为其分配相应的内存空间,包括:程序计数器、堆、栈等等。java的对象都是在堆区的,所以DirectByteBuffer作为java对象,它自然也是在堆区域,只是DirectByteBuffer对象表示的存储空间是放在堆区域外部的。

下面再来看下java的具体实现:

// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;

address长整型变量是Buffer类的属性域,只仅仅被堆外内存使用。该address指向的java堆区域以外的一段内存空间的起始地址。每个DirectByteBuffer持有一个这样的address变量,即可以快速的寻址。

  • 堆外内存的分配回收代价要高于堆内内存
  • 通过ByteBuffer.allocateDirect(int capacity)堆外堆内创建,创建java对象时即创建了堆内内存
  • 堆外内存可以用于操作系统的本地io操作,而堆内内存由于gc的缘故暂不支持,具体细节可以参考上面R大对于"pin"的说法

三.应用场景

  1. A direct byte buffer may also be created by mapping a region of a file directly into memory.
  1. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.
  1. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations.

Java中非常典型的两种用法:

  1. 作为内存文件映射,直接将文件中区域的内容映射至内存,java中的代表类MappedByteBuffer的实现类就是使用DirectByteBuffer(关于MappedByteBuffer的原理可以移步至 ——> 狼哥文章:深入浅出MappedByteBufferNIO包-Buffer类:内存映射文件DirectByteBuffer与MappedByteBuffer(二

    如果想更深入的理解MappedByteBuffer的原理,推荐了解zero-copy机制,可移步至通过零拷贝实现有效数据传输零复制(zero copy)技术

  2. 操作系统本地I/O直接存取数据至堆外内存,避免拷贝中间缓冲区。Java中Channel的read/write都是用到了DirecByteBuffer作为本地I/O操作

    由于是堆外内存,不属于jvm运行区域,减少了gc影响,所以可用于本地I/O操作,参考R大的“pin”观念。

四.分配与回收

虽然是堆外内存,也是有分配和回收策略的,接下来简单的了解下堆外内存的分配与回收策略。

在以前学习c语言的时候,学习过其两个关于内存分配与回收的函数:malloc和free,分别用来申请分配内存/回收释放内存。

由于java的内存管理是委托给jvm处理的,所以基本上java应用开发者不需要要太关系内存这块的处理,那么堆外内存又是如何分配与回收呢。

java的中提供的unsafe类,相信读者们都不陌生。java提供出来的不安全操作的封装类,该类提供的操作在java中认为是不安全的,比如内存的直接分配与回收,有违内存自动管理的机制,所以这个类的访问控制只能由rt.jar访问。堆外内存的分配与回收正是使用了

public native long allocateMemory(long var1);
public native void freeMemory(long var1);

它的以上两个api直接完成分配与回收,由于是jni,底层基于malloc和和free实现。

作为java应用的开发者,我们一般通过Buffer实现类的的静态方法

ByteBuffer.allocatDirect(int capacity)

分配指定大小的堆外内存空间。

但是堆外内存肯定不是无限分配,这样容易引起内存溢出,系统异常。java中堆外内存大小可以通过jvm参数-XX:MaxDirectMemorySize指定,如果该参数未被指定则使用-Xmx参数指定的大小堆大小作为堆外内存大小。

下面分析下源码的方式来更深入的理解下堆外内存的分配过程。allocatDirect的方法如下

/**
* Allocates a new direct byte buffer.
*
* <p> The new buffer's position will be zero, its limit will be its
* capacity, its mark will be undefined, and each of its elements will be
* initialized to zero. Whether or not it has a
* {@link #hasArray backing array} is unspecified.
*
* @param capacity
* The new buffer's capacity, in bytes
*
* @return The new byte buffer
*
* @throws IllegalArgumentException
* If the <tt>capacity</tt> is a negative integer
*/
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}

从java doc中知道,分配的buffer的position为0(如果对Buffer还不甚了解,可以移步至我的上篇关于《Buffer》博文),limit为capacity,mark未被定义,buffer中存储元素被初始为0。

下面更进一步看下DirectByteBuffer的构造方法

// Primary constructor
//
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;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

从以上源码可以看出,首先构造方法访问控制为包权限,这样可以避免让应用代码入侵。又不失jdk可以进行自由实现。

整个初始过程可以分为以下几个过程:

  1. 调用父类构造方法进行初始化处理
  2. 获取是为页对齐并获取页大小
  3. 分配大小至少为1byte。如果分配容量小于1byte,则分配大小设置为1byte。如果是页对齐分配,则设置容量为大于cap的最小的页大小整数倍。否则设置容量为cap
  4. 进行内存预留操作(主要是为了锁定内存,用于后续的内存分配,其中使用cas操作)
  5. 使用unsafe分配内存
  6. 设置分配的内存的起始地址address(该变量被Buffer class对象持有),如果不是整页地址,则进行滚动到页边界
  7. 设置该块堆外内存清理器Cleaner(用于后续的内存回收释放)

经过以上过程,完成了堆外内存的分配。

关于回收的详细过程,建议移步至上面提及的笨神的文章,这里只做简单的描述。

回收使用unsafe.freeMemory既然要回收堆外内存,那必须知道该块内存的的地址,与之唯一相关联的DirectByteBuffer对象,该对象中存有内存的地址address。接下来就要知道什么时候回收,这个就和jvm gc息息相关了,如何决策堆外内存回收呢?显然是该块内存不使用时,该块内存是否使用可以通过DirectByteBuffer对象是否要被gc回收。

就这样,知道回收哪里,什么时间点回收,就不难处理了。

引用笨神的描述:

DirectByteBuffer对象在创建的时候关联了一个PhantomReference,说到PhantomReference它其实主要是用来跟踪对象何时被回收的,它不能影响gc决策,但是gc过程中如果发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理,而DirectByteBuffer关联的PhantomReference是PhantomReference的一个子类,在最终的处理里会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块

上面的堆外内存创建过程中,描述到设置清理器Cleaner,该类就是PhantomReference的实现。

再可以看下ReferenceHandler的处理

private static class ReferenceHandler extends Thread {

    private static void ensureClassInitialized(Class<?> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
} static {
// pre-load and initialize InterruptedException and Cleaner classes
// so that we don't get into trouble later in the run loop if there's
// memory shortage while loading/initializing them lazily.
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
} ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
} public void run() {
while (true) {
tryHandlePending(true);
}
}
}

其中调用tryHandlePending

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;
} // 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;
}

最后调用c.clean(),即调用了DirectByteBuffer中的cleaner。

再来看下Cleaner.clean

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;
}
});
}
}

}

这里调用了thunk.run,再回头看下DirectByteBuffer的初始过程中,Cleaner的初始化调用了Cleaner的工厂方法create,对于传入的Runnable实现是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);
}
}

从DirectByteBuffer和Deallocator的构造方法中,不难看出,Deallocator.address就是堆外内存的地址,在run方法中调用了unsafe.freeMemory(address);回收释放堆外内存。

通过以上的两个过程大致介绍,也算是对堆外内存的分配与回收有个宏观的了解。由于个人技术能力有限,如果有错误的地方,请读者不吝给出宝贵意见。

Direct Buffer介绍的更多相关文章

  1. NIO中的heap Buffer和direct Buffer区别

    在Java的NIO中,我们一般采用ByteBuffer缓冲区来传输数据,一般情况下我们创建Buffer对象是通过ByteBuffer的两个静态方法: ByteBuffer.allocate(int c ...

  2. Java网络编程和NIO详解8:浅析mmap和Direct Buffer

    Java网络编程与NIO详解8:浅析mmap和Direct Buffer 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NI ...

  3. Tomcat 9内存溢出:"http-apr-8080-Acceptor-0" java.lang.OutOfMemoryError: Direct buffer memory

    Tomcat开启了APR模式,而APR模式会使用堆外内存,关于堆内存可从如下链接了解一下:http://blog.csdn.net/zhouhl_cn/article/details/6573213. ...

  4. Solr新建collection时报错 Caused by: Direct buffer memory

    错误如下 [root@192.168.1.235 conf]# curl "http://192.168.1.235:8983/solr/admin/collections ?action= ...

  5. Direct Buffer vs. Heap Buffer

    1. 劣势:创建和释放Direct Buffer的代价比Heap Buffer得要高. 2. 差别:Direct Buffer不是分配在堆上的,它不被GC直接管理(但Direct Buffer的JAV ...

  6. Java网络编程与NIO详解8:浅析mmap和Direct Buffer

    微信公众号[黄小斜]作者是蚂蚁金服 JAVA 工程师,目前在蚂蚁财富负责后端开发工作,专注于 JAVA 后端技术栈,同时也懂点投资理财,坚持学习和写作,用大厂程序员的视角解读技术与互联网,我的世界里不 ...

  7. JVM--你常见的jvm 异常有哪些? 代码演示:StackOverflowError , utOfMemoryError: Java heap space , OutOfMemoryError: GC overhead limit exceeded, Direct buffer memory, Unable_to_create_new_native_Thread, Metaspace

    直接上代码: public class Test001 { public static void main(String[] args) { //java.lang.StackOverflowErro ...

  8. 【神经网络与深度学习】Google Protocol Buffer介绍

    简介 什么是 Google Protocol Buffer? 假如您在网上搜索,应该会得到类似这样的文字介绍: Google Protocol Buffer( 简称 Protobuf) 是 Googl ...

  9. java Direct Buffer

    public static ByteBuffer allocate (int capacity)       //性能低于下面的Direct,因为是把内存建立在JVM堆上,容易被GC回收,可能需要多次 ...

随机推荐

  1. webrtc (6) 在Webrtc中集成VideoToolbox

    来源:http://blog.csdn.net/wangruihit/article/details/46550853 VideoToolbox是iOS平台在iOS8之后开放的一个Framework, ...

  2. Vue.js@2.6.10更新内置错误处机制,Fundebug同步支持相应错误监控

    摘要: Fundebug 的 JavaScript 错误监控插件同步支持 Vue.js 异步错误监控. Vue.js 从诞生至今已经 5 年,尤大在今年 2 月份发布了重大更新,即Vue 2.6.更新 ...

  3. Centos7 samba配置

    目录 免密码只读 加密码可读写 Samba配置了很多次,总是忘,现在写在博客里. 免密码只读 最主要的是免密配置,主要用到了两个配置,要写在[global]里: map to guest = Bad ...

  4. 使用aptitude安装软件

    linux的版本依赖问题很令人纠结,不过我们可以通过使用aptitude软件包管理器来解决这个依赖问题,aptitude是可以选择合适的版本与匹配软件安装.

  5. Jupyter notebook 中常用的快捷键

    1.注释和缩进 注释一行或多行: Ctrl + / 多行同时缩进:Tab 或者 Ctrl + ] 多行取消缩进: Shift + Tab 或者 ctrl + [ 2.编辑和运行 Enter : 转入编 ...

  6. Ubuntu配置samba服务器

    假设我的Ubuntu用户名:myname 1. 安装和卸载samba: sudo apt-get install samba samba-common sudo apt-get autoremove ...

  7. 良心送分题(牛客挑战赛35E+虚树+最短路)

    目录 题目链接 题意 思路 代码 题目链接 传送门 题意 给你一棵树,然后把这棵树复制\(k\)次,然后再添加\(m\)条边,然后给你起点和终点,问你起点到终点的最短路. 思路 由于将树复制\(k\) ...

  8. 07-C#笔记-运算符

    1. 支持++和-- 含义和C++中相同 2. 条件运算 同C++ 3. 位运算 ^ 异或 ~ 取反 4. 支持?:运算 5. 特殊 is 判断对象是否为某一类型. If( Ford is Car) ...

  9. python 文本全选

    这个是一个控制框有效果 # encoding: utf-8 from Tkinter import * def printentry(event): print("click on" ...

  10. 从一段文字中提取出uri信息

    package handle.groupby; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io ...