本文转载自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. java 石头剪子布游戏

    源代码 StoneGame.java 1 import java.io.BufferedReader; 2 import java.io.IOException; 3 import java.io.I ...

  2. Flutter--Dart基础语法(四)异步

    前言 Flutter 是 Google 开源的 UI 工具包,帮助开发者通过一套代码库高效构建多平台精美应用,Flutter 开源.免费,拥有宽松的开源协议,支持移动.Web.桌面和嵌入式平台. Fl ...

  3. 黑客练手入门| pwnable.kr—幼儿瓶—01:fd

    目录 前言 pwnable.kr介绍 该怎么玩 幼儿瓶第一道题:fd 0x00 问题描述 0x01 源码分析 0x02 解题方法 0x03 知识点总结 前言 担心有人不知道pwnable.kr是什么, ...

  4. WebApi Swagger 接口多版本控制 适用于APP接口管理

    最近研究了下swagger多版本的维护,网上的文章千篇一律,无法满足我的需求,分享下我的使用场景以及实现 演示环境:Visual Studio 2019.Asp.NET WebAPI.NET Fram ...

  5. Yarn参数优化(Fair Scheduler版本)

    YARN 自从hadoop2.0之后, 我们可以使用apache yarn 来对集群资源进行管理.yarn把可以把资源(内存,CPU)以Container的方式进行划分隔离.YARN会管理集群中所有机 ...

  6. 2020牛客暑期多校训练营(第二场)Fake Maxpooling

    传送门:Fake Maxpooling 题意:给出矩阵的行数n和列数m,矩阵 Aij = lcm( i , j )  ,求每个大小为k*k的子矩阵的最大值的和. 题解:如果暴力求解肯定会t,所以要智取 ...

  7. 牛客小白月赛30 B.最好的宝石 (线段树)

    题意:RT. 题解:很明显的线段树维护区间最大值操作,但是我们同时还要维护最大值的个数,我们在build或者modify操作完子树然后push_up的时候,我们先从两个儿子取max更新父节点的最大值, ...

  8. VS Code 搭建合适的 markdown 文档编写环境

    写在开头,之前我是使用Gitee与Github作为图床和Picgo搭配Typora使用的 ,但因为最近觉得这样还是稍微比较繁琐,然后因为VS Code是我的主要文本编辑器.Cpp,Python等均是在 ...

  9. Dapr 已在塔架就位 将发射新一代微服务

    微服务是云原生架构的核心,通常使用Kubernetes 来按需管理服务扩展. 微软一直走在 Cloud Native Computing Foundation的 最前沿,并通过使用Kubernetes ...

  10. [视频] Docker 安装 nginx + rtmp

    目录 拉取镜像 创建并运行容器,映射出两个端口1935.80 将视频文件推流至rtmp服务器 使用ffplay播放rtmp流 拉取镜像 docker pull alfg/nginx-rtmp 创建并运 ...