本文转载自JDK源码阅读-DirectByteBuffer

导语

在文章JDK源码阅读-ByteBuffer中,我们学习了ByteBuffer的设计。但是他是一个抽象类,真正的实现分为两类:HeapByteBufferDirectByteBufferHeapByteBuffer是堆内ByteBuffer,使用byte[]存储数据,是对数组的封装,比较简单。DirectByteBuffer是堆外ByteBuffer,直接使用堆外内存空间存储数据,是NIO高性能的核心设计之一。本文来分析一下DirectByteBuffer的实现。

如何使用DirectByteBuffer

如果需要实例化一个DirectByteBuffer,可以使用java.nio.ByteBuffer#allocateDirect这个方法:

public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}

DirectByteBuffer实例化流程

我们来看一下DirectByteBuffer是如何构造,如何申请与释放内存的。先看看DirectByteBuffer的构造函数:

DirectByteBuffer(int cap) {                   // package-private
// 初始化Buffer的四个核心属性
super(-1, 0, cap, cap);
// 判断是否需要页面对齐,通过参数-XX:+PageAlignDirectMemory控制,默认为false
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;
}
// 初始化内存空间为0
unsafe.setMemory(base, size, (byte) 0);
// 设置内存起始地址
if (pa && (base % ps != 0)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 使用Cleaner机制注册内存回收处理函数
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

申请内存前会调用java.nio.Bits#reserveMemory判断是否有足够的空间可供申请:

// 该方法主要用于判断申请的堆外内存是否超过了用例指定的最大值
// 如果还有足够空间可以申请,则更新对应的变量
// 如果已经没有空间可以申请,则抛出OOME
// 参数解释:
// size:根据是否按页对齐,得到的真实需要申请的内存大小
// cap:用户指定需要的内存大小(<=size)
static void reserveMemory(long size, int cap) {
// 因为涉及到更新多个静态统计变量,这里需要Bits类锁
synchronized (Bits.class) {
// 获取最大可以申请的对外内存大小,默认值是64MB
// 可以通过参数-XX:MaxDirectMemorySize=<size>设置这个大小
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// -XX:MaxDirectMemorySize限制的是用户申请的大小,而不考虑对齐情况
// 所以使用两个变量来统计:
// reservedMemory:真实的目前保留的空间
// totalCapacity:目前用户申请的空间
if (cap <= maxMemory - totalCapacity) {
reservedMemory += size;
totalCapacity += cap;
count++;
return; // 如果空间足够,更新统计变量后直接返回
}
} // 如果已经没有足够空间,则尝试GC
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
// GC后再次判断,如果还是没有足够空间,则抛出OOME
if (totalCapacity + cap > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
totalCapacity += cap;
count++;
}
}

java.nio.Bits#reserveMemory方法中,如果空间不足,会调用System.gc()尝试释放内存,然后再进行判断,如果还是没有足够的空间,抛出OOME。

如果分配失败,则需要把预留的统计变量更新回去:

static synchronized void unreserveMemory(long size, int cap) {
if (reservedMemory > 0) {
reservedMemory -= size;
totalCapacity -= cap;
count--;
assert (reservedMemory > -1);
}
}

从上面几个函数中我们可以得到信息:

  1. 可以通过-XX:+PageAlignDirectMemor参数控制堆外内存分配是否需要按页对齐,默认不对齐。
  2. 每次申请和释放需要调用调用Bits的reserveMemoryunreserveMemory方法,这两个方法根据内部维护的统计变量判断当前是否还有足够的空间可供申请,如果有足够的空间,更新统计变量,如果没有足够的空间,调用System.gc()尝试进行垃圾回收,回收后再次进行判断,如果还是没有足够的空间,抛出OOME。
  3. Bits的reserveMemory方法判断是否有足够内存不是判断物理机是否有足够内存,而是判断JVM启动时,指定的堆外内存空间大小是否有剩余的空间。这个大小由参数-XX:MaxDirectMemorySize=设置。
  4. 确定有足够的空间后,使用sun.misc.Unsafe#allocateMemory申请内存
  5. 申请后的内存空间会被清零
  6. DirectByteBuffer使用Cleaner机制进行空间回收

可以看出除了判断是否有足够的空间的逻辑外,核心的逻辑是调用sun.misc.Unsafe#allocateMemory申请内存,我们看一下这个函数是如何申请对外内存的:

// 申请一块本地内存。内存空间是未初始化的,其内容是无法预期的。
// 使用freeMemory释放内存,使用reallocateMemory修改内存大小
public native long allocateMemory(long bytes);
// openjdk8/hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
UnsafeWrapper("Unsafe_AllocateMemory");
size_t sz = (size_t)size;
if (sz != (julong)size || size < 0) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
if (sz == 0) {
return 0;
}
sz = round_to(sz, HeapWordSize);
// 调用os::malloc申请内存,内部使用malloc函数申请内存
void* x = os::malloc(sz, mtInternal);
if (x == NULL) {
THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}
//Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
return addr_to_java(x);
UNSAFE_END

可以看出sun.misc.Unsafe#allocateMemory使用malloc这个C标准库的函数来申请内存。

DirectByteBuffer回收流程

在DirectByteBuffer的构造函数的最后,我们看到了这样的语句:

// 使用Cleaner机制注册内存回收处理函数
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

这是使用Cleaner机制进行内存回收。因为DirectByteBuffer申请的内存是在堆外,DirectByteBuffer本身支持保存了内存的起始地址而已,所以DirectByteBuffer的内存占用是由堆内的DirectByteBuffer对象与堆外的对应内存空间共同构成。堆内的占用只是很小的一部分,这种对象被称为冰山对象。

堆内的DirectByteBuffer对象本身会被垃圾回收正常的处理,但是对外的内存就不会被GC回收了,所以需要一个机制,在DirectByteBuffer回收时,同时回收其堆外申请的内存。

Java中可选的特性有finalize函数,但是finalize机制是Java官方不推荐的,官方推荐的做法是使用虚引用来处理对象被回收时的后续处理工作,可以参考JDK源码阅读-Reference。同时Java提供了Cleaner类来简化这个实现,Cleaner是PhantomReference的子类,可以在PhantomReference被加入ReferenceQueue时触发对应的Runnable回调。

DirectByteBuffer就是使用Cleaner机制来实现本身被GC时,回收堆外内存的能力。我们来看一下其回收处理函数是如何实现的:

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方法释放内存
unsafe.freeMemory(address);
address = 0;
// 更新统计变量
Bits.unreserveMemory(size, capacity);
} }

sun.misc.Unsafe#freeMemory方法使用C标准库的free函数释放内存空间。同时更新Bits类中的统计变量。

DirectByteBuffer读写逻辑

public ByteBuffer put(int i, byte x) {
unsafe.putByte(ix(checkIndex(i)), ((x)));
return this;
} public byte get(int i) {
return ((unsafe.getByte(ix(checkIndex(i)))));
} private long ix(int i) {
return address + (i << 0);
}

DirectByteBuffer使用sun.misc.Unsafe#getByte(long)sun.misc.Unsafe#putByte(long, byte)这两个方法来读写堆外内存空间的指定位置的字节数据。不过这两个方法本地实现比较复杂,这里就不分析了。

默认可以申请的堆外内存大小

上文提到了DirectByteBuffer申请内存前会判断是否有足够的空间可供申请,这个是在一个指定的堆外大小限制的前提下。用户可以通过-XX:MaxDirectMemorySize=这个参数来控制可以申请多大的DirectByteBuffer内存。但是默认情况下这个大小是多少呢?

DirectByteBuffer通过sun.misc.VM#maxDirectMemory来获取这个值,可以看一下对应的代码:

// A user-settable upper limit on the maximum amount of allocatable direct
// buffer memory. This value may be changed during VM initialization if
// "java" is launched with "-XX:MaxDirectMemorySize=<size>".
//
// The initial value of this field is arbitrary; during JRE initialization
// it will be reset to the value specified on the command line, if any,
// otherwise to Runtime.getRuntime().maxMemory().
//
private static long directMemory = 64 * 1024 * 1024; // Returns the maximum amount of allocatable direct buffer memory.
// The directMemory variable is initialized during system initialization
// in the saveAndRemoveProperties method.
//
public static long maxDirectMemory() {
return directMemory;
}

这里directMemory默认赋值为64MB,那对外内存的默认大小是64MB吗?不是,仔细看注释,注释中说,这个值会在JRE启动过程中被重新设置为用户指定的值,如果用户没有指定,则会设置为Runtime.getRuntime().maxMemory()

这个过程发生在sun.misc.VM#saveAndRemoveProperties函数中,这个函数会被java.lang.System#initializeSystemClass调用:

public static void saveAndRemoveProperties(Properties props) {
if (booted)
throw new IllegalStateException("System initialization has completed"); savedProps.putAll(props); // Set the maximum amount of direct memory. This value is controlled
// by the vm option -XX:MaxDirectMemorySize=<size>.
// The maximum amount of allocatable direct buffer memory (in bytes)
// from the system property sun.nio.MaxDirectMemorySize set by the VM.
// The system property will be removed.
String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
if (s != null) {
if (s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
} else {
long l = Long.parseLong(s);
if (l > -1)
directMemory = l;
}
} //...
}

所以默认情况下,可以申请的DirectByteBuffer大小为Runtime.getRuntime().maxMemory(),而这个值等于可用的最大Java堆大小,也就是我们-Xmx参数指定的值。

所以最终结论是:默认情况下,可以申请的最大DirectByteBuffer空间为Java最大堆大小的值。

和DirectByteBuffer有关的JVM选项

根据上文的分析,有两个JVM参数与DirectByteBuffer直接相关:

  • -XX:+PageAlignDirectMemory:指定申请的内存是否需要按页对齐,默认不对其
  • -XX:MaxDirectMemorySize=,可以申请的最大DirectByteBuffer大小,默认与-Xmx相等

JDK源码阅读-DirectByteBuffer的更多相关文章

  1. JDK源码阅读-ByteBuffer

    本文转载自JDK源码阅读-ByteBuffer 导语 Buffer是Java NIO中对于缓冲区的封装.在Java BIO中,所有的读写API,都是直接使用byte数组作为缓冲区的,简单直接.但是在J ...

  2. JDK源码阅读(三):ArraryList源码解析

    今天来看一下ArrayList的源码 目录 介绍 继承结构 属性 构造方法 add方法 remove方法 修改方法 获取元素 size()方法 isEmpty方法 clear方法 循环数组 1.介绍 ...

  3. JDK源码阅读(一):Object源码分析

    最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...

  4. 利用IDEA搭建JDK源码阅读环境

    利用IDEA搭建JDK源码阅读环境 首先新建一个java基础项目 基础目录 source 源码 test 测试源码和入口 准备JDK源码 下图框起来的路径就是jdk的储存位置 打开jdk目录,找到sr ...

  5. JDK源码阅读-FileOutputStream

    本文转载自JDK源码阅读-FileOutputStream 导语 FileOutputStream用户打开文件并获取输出流. 打开文件 public FileOutputStream(File fil ...

  6. JDK源码阅读-FileInputStream

    本文转载自JDK源码阅读-FileInputStream 导语 FileIntputStream用于打开一个文件并获取输入流. 打开文件 我们来看看FileIntputStream打开文件时,做了什么 ...

  7. JDK源码阅读-RandomAccessFile

    本文转载自JDK源码阅读-RandomAccessFile 导语 FileInputStream只能用于读取文件,FileOutputStream只能用于写入文件,而对于同时读取文件,并且需要随意移动 ...

  8. JDK源码阅读-FileDescriptor

    本文转载自JDK源码阅读-FileDescriptor 导语 操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数.Java虽然在设计上使用了抽象程度更高的流来作为文 ...

  9. JDK源码阅读-Reference

    本文转载自JDK源码阅读-Reference 导语 Java最初只有普通的强引用,只有对象存在引用,则对象就不会被回收,即使内存不足,也是如此,JVM会爆出OOME,也不会去回收存在引用的对象. 如果 ...

随机推荐

  1. JDBC连接数据库,数据库访问层

    为什么需要JDBC JDBC API DriverManager JDBC驱动 JDBC的功能 JDBC步骤 数据访问层DAO DAO模式的组成 DAO模式的实际应用 为什么需要JDBC? JDBC是 ...

  2. Nginx 指定域名(或子域名)和网站绑定

    问题起因 博主最近在 CentOS 上面部署另外一个网站,但并不想通过端口号来访问,因为端口号对于 SEO 优化不利,且用户访问较繁琐(使用域名不就是为了方便用户访问吗?再引入端口号岂不是和使用域名的 ...

  3. 如何学习Java?从标识符开始

    标识符 1.定义 Java对各种变量.方法和类等要素命名时使用的字符序列称为标识符(包含但不限于:类名.变量名.方法名.接口名.包名--) 2.命名规则 1.由26个英文字母大小写,0-9,_或$组成 ...

  4. Pytest(9)skip跳过用例

    前言 pytest.mark.skip可以标记无法在某些平台上运行的测试功能,或者您希望失败的测试功能 Skip和xfail: 处理那些不会成功的测试用例 你可以对那些在某些特定平台上不能运行的测试用 ...

  5. Codeforces Round #681 (Div. 2, based on VK Cup 2019-2020 - Final)【ABCDF】

    比赛链接:https://codeforces.com/contest/1443 A. Kids Seating 题意 构造一个大小为 \(n\) 的数组使得任意两个数既不互质也不相互整除,要求所有数 ...

  6. hdu4348 To the moon (主席树 || 离线线段树)

    Problem Description Background To The Moon is a independent game released in November 2011, it is a ...

  7. hdu3565 Bi-peak Number (有上界和下界的数位dp)

    Problem Description A peak number is defined as continuous digits {D0, D1 - Dn-1} (D0 > 0 and n & ...

  8. Codeforces Round #498 (Div. 3) E. Military Problem (DFS)

    题意:建一颗以\(1\)为根结点的树,询问\(q\)次,每次询问一个结点,问该结点的第\(k\)个子结点,如果不存在则输出\(-1\). 题解:该题数据范围较大,需要采用dfs预处理的方法,我们从结点 ...

  9. JavaScript_继承

  10. Operating System:信号量

    pv原语操作(1)操作系统PV意思:PV操作与信号量的处理相关,P表示通过的意度思,V表示释放的意思.(2)p操作和v操作是不可中断问的程序段,称为原语.如果将信号量看作共享变量,则pv操作为其临界区 ...