1. 前言

最近研究ByteBuffer和DirectByteBuffer。堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。下面本博客就来详细介绍以下Java NIO 中的DirectByteBuffer。

2. Linux 内核态和用户态

  • 内核态:控制计算机的硬件资源,并提供上层应用程序运行的环境。比如socket I/0操作或者文件的读写操作等
  • 用户态:上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源
  • 系统调用:为了使上层应用能够访问到这些资源,内核为上层应用提供访问的接口

内核态由操作系统控制计算机的硬件资源,用户态的程序可以通过一些系统调用,通过上下文切换,由用户态切换到内核态,然后进行相应的操作系统底层系统调用。因此我们可以得知当我们通过JNI调用的native方法实际上就是从用户态切换到了内核态的一种方式。并且通过该系统调用使用操作系统所提供的功能。

2.DirectByteBuffer ———— 直接缓冲

堆内内存存在的问题:由于HeapByteBuffer是存储在JVM 堆中的,所以我们在使用ByteBufffer的时候,如果创建了一个很大的ByteBuffer的时候,那么过大的内存Buffer对于垃圾回收来说造成的影响会很大,JVM新生带会频繁进行Minor GC,进而对程序的性能造成影响。在某些I/O操作下,FilChannelImpl需要通过堆外内存进行数据传输,如果使用HeapByteBuffer的话,FilChannelImpl需要通过将HeapByteBuffer复制到堆外内存,然后进行数据传输。

DirectByteBuffer解决了上述产生的问题:

  • 垃圾回收停顿改善:由于DirectByteBuffer是堆外内存,不受垃圾回收机制控制,它直接受操作系统管理。所以减少了大内存Buffer在新生代中,造成JVM新生代进行频繁的垃圾回收。
  • 在某些场景下可以提升程序I/O操纵的性能。少去了将数据从堆内内存拷贝到堆外内存的步骤。

DirectByteBuffer是Java用于实现堆外内存的一个重要类,我们可以通过该类实现堆外内存的创建、使用和销毁。


DirectByteBuffer该类本身还是位于Java内存模型的堆中。堆内内存是JVM可以直接管控、操纵。 而DirectByteBuffer中的unsafe.allocateMemory(size);是个一个native方法,这个方法分配的是堆外内存,通过C的malloc来进行分配的。分配的内存是系统本地的内存,并不在Java的内存中,也不属于JVM管控范围,所以在DirectByteBuffer一定会存在某种方式来操纵堆外内存。 下面我们就来分析DirectByteBuffer的创建过程和回收过程。

2. 源码分析 DirectByteBuffer内存分配和回收

2.1 DirectByteBuffer内存分配

在DirectByteBuffer的父类Buffer中有个address属性,address只会被Directbuffer给使用到。之所以将address属性升级放在Buffer中,是为了在JNI调用GetDirectBufferAddress时提升它调用的速率。

 DirectByteBuffer(int cap) {                   // package-privatesuper(-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 boundaryaddress = base + ps - (base & (ps - 1));} else {address = base;}cleaner = Cleaner.create(this, new Deallocator(base, size, cap));att = null;}

unsafe.allocateMemory(size);分配完堆外内存后就会返回分配的堆外内存基地址,并将这个地址赋值给了address属性。这样我们后面通过JNI对这个堆外内存操作时都是通过这个address来实现的了。

2.2 DirectByteBuffer回收

前面我们说到了DirectByteBuffer是堆外内存,它不由JVM 垃圾回收机制控制。所以JVM 垃圾回收不了DirectByteBuffer分配的内存,那么DirectByteBuffer是如何进行回收的呢?
DirectBuffer内存回收主要有两种方式,一种是通过System.gc来回收,另一种是通过构造函数里创建的Cleaner对象来回收。

System.gc回收

在DirectBuffer的构造函数中,用到了Bit.reserveMemory这个方法,该方法如下

static void reserveMemory(long size, int cap) {······if (tryReserveMemory(size, cap)) {return;}······while (jlra.tryHandlePendingReference()) {if (tryReserveMemory(size, cap)) {return;}}System.gc();// 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 luckthrow new OutOfMemoryError("Direct buffer memory");} finally {if (interrupted) {// don't swallow interruptsThread.currentThread().interrupt();}}}

reserveMemory方法首先尝试分配内存,如果分配成功的话,那么就直接退出。如果分配失败那么就通过调用tryHandlePendingReference来尝试清理堆外内存(最终调用的是Cleaner的clean方法,其实就是unsafe.freeMemory然后释放内存),清理完内存之后再尝试分配内存。如果还是失败,调用System.gc()来触发一次FullGC进行回收(前提是没有加-XX:-+DisableExplicitGC参数)。

Cleaner对象回收

另个触发堆外内存回收的时机是通过Cleaner对象的clean方法进行回收。在每次新建一个DirectBuffer对象的时候,会同时创建一个Cleaner对象,同一个进程创建的所有的DirectBuffer对象跟Cleaner对象的个数是一样的,并且所有的Cleaner对象会组成一个链表,前后相连。

3. 源码分析 FilChannelImpl的read调用

下面我们来看一下为什么在某些I/O操作下,使用DirectBuffer对比HeapByteBuffer的性能会更好。在FileChannelImpl的read方法中进行read操作的时候,会调用IOUtil.read(this.fd, var1, -1L, this.nd) 的read 方法。

public class FileChannelImpl{public int read(ByteBuffer var1) throws IOException {this.ensureOpen();if (!this.readable) {throw new NonReadableChannelException();} else {Object var2 = this.positionLock;synchronized(this.positionLock) {int var3 = 0;int var4 = -1;byte var5;try {this.begin();var4 = this.threads.add();if (this.isOpen()) {do {var3 = IOUtil.read(this.fd, var1, -1L, this.nd);} while(var3 == -3 && this.isOpen());int var12 = IOStatus.normalize(var3);return var12;}var5 = 0;} finally {this.threads.remove(var4);this.end(var3 > 0);assert IOStatus.check(var3);}return var5;}}}
}

接下来我们继续跟踪源码,找到为什么I/O操作下,使用DirectBuffer对比HeapByteBuffer的性能会更好。

public class IOUtil{static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {if (var1.isReadOnly()) {throw new IllegalArgumentException("Read-only buffer");} else if (var1 instanceof DirectBuffer) {return readIntoNativeBuffer(var0, var1, var2, var4);} else {ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());int var7;try {int var6 = readIntoNativeBuffer(var0, var5, var2, var4);var5.flip();if (var6 > 0) {var1.put(var5);}var7 = var6;} finally {Util.offerFirstTemporaryDirectBuffer(var5);}return var7;}}
}

从这段代码中,我们可以看出来如果FileDescriptor操作的ByteBuffer是堆外内存的话,我们直接调用readIntoNativeBuffer将文件从内核缓冲区直接读取到DirectBuffer中。但是如果我们操作的是HeapBytebuffer的话,我们首先构造一个新的DirectBuffer,然后调用readIntoNativeBuffer将文件从内核缓冲区直接读取到DirectBuffer中,最后将这个DirectBuffer复制到HeapByteBuffer之中。所以说,如果我们直接使用DirectBuffer,在进行数据读取的话,就不用将构造出来新的DirectBuffer复制到HeapByteBuffer之中。所以对于大文件来说,我们使用DirectBuffer性能更好。

Q:小伙伴可能回文,那为什么操作系统不直接访问Java堆内的内存区域了?

A:这是因为JNI方法访问的内存区域是一个已经确定了的内存区域地址,那么该内存地址指向的是Java堆内内存的话,那么如果在操作系统正在访问这个内存地址的时候,Java在这个时候进行了GC操作,而GC操作会涉及到数据的移动操作[GC经常会进行先标志-整理算法的操作。即,将可回收的空间做标志,然后清空标志位置的内存,然后会进行一个整理,整理就会涉及到对象的移动,移动的目的是为了腾出一块更加完整、连续的内存空间,以容纳更大的新对象],数据的移动会使JNI调用的数据错乱。所以JNI调用的内存是不能进行GC操作的。

Q:如上面所说,JNI调用的内存是不能进行GC操作的,那该如何解决了?

A:①堆内内存与堆外内存之间数据拷贝的方式(并且在将堆内内存拷贝到堆外内存的过程JVM会保证不会进行GC操作):比如我们要完成一个从文件中读数据到堆内内存的操作,即FileChannelImpl.read(HeapByteBuffer)。这里实际上File I/O会将数据读到堆外内存中,然后堆外内存再讲数据拷贝到堆内内存,这样我们就读到了文件中的内存。

NIO详解(六):Java堆外内存相关推荐

  1. Java堆外内存泄露分析

    查看堆内存占用正常,jvm垃圾回收也没有异常.而top出来显示java占用内存是几个G,那么可能想到了是堆外内存泄漏. 需要安装google-perftools工具进行分析 1.先安装g++ 不然编译 ...

  2. java堆外内存泄漏分析排查

    JAVA堆外内存分析 文章目录 JAVA堆外内存分析 1.前言 2.准备 3.具体分析 3.1堆外溢出风险判断 3.1.1确认java进程号 3.1.2查看此java进程的jvm参数 3.1.3查看j ...

  3. Cassandra Java堆外内存排查经历全记录

    背景 最近准备上线cassandra这个产品,同事在做一些小规格ECS(8G)的压测.压测时候比较容易触发OOM Killer,把cassandra进程干掉.问题是8G这个规格我配置的heap(Xmx ...

  4. 记一次Cassandra Java堆外内存排查经历

    背景 最近准备上线cassandra这个产品,同事在做一些小规格ECS(8G)的压测.压测时候比较容易触发OOM Killer,把cassandra进程干掉.问题是8G这个规格我配置的heap(Xmx ...

  5. java 堆外内存 查看_超干货!Cassandra Java堆外内存排查经历全记录

    背景 最近准备上线cassandra这个产品,同事在做一些小规格ECS(8G)的压测.压测时候比较容易触发OOM Killer,把cassandra进程干掉.问题是8G这个规格我配置的heap(Xmx ...

  6. java 堆外内存_详解Java堆外内存

    临近春节,最近有点时间,准备顺着上篇专栏的思路写下去,建议先阅读: juejin.im/post/684490- 武汉那几个吃野味的傻[],请藏好你们的妈 正文开始 在运行Java程序时,java虚拟 ...

  7. java堆外内存详解(又名直接内存)和ByteBuffer

    堆内内存 java的内存分为堆内内存和堆外内存,在了解堆外内存之前,先看看堆内内存是啥,堆内内存是受jvm管控的,也就是说,堆内内存由jvm负责创建和回收:创建和回收都是自动进行的,不需要人为干预: ...

  8. Java堆外内存的使用

    堆外内存的回收见HeapByteBuffer和DirectByteBuffer以及回收DirectByteBuffer 基本类型长度 在Java中有很多的基本类型,比如: byte,一个字节是8位bi ...

  9. java堆外内存6_Java堆外内存排查小结

    简介 JVM堆外内存难排查但经常会出现问题,这可能是目前最全的JVM堆外内存排查思路.之前的文章排版太乱,现在整理重发一下,内容是一样的. 通过本文,你应该了解: pmap 命令 gdb 命令 per ...

最新文章

  1. 分析UIWindow
  2. [Exchange]使用EWS托管API2.0同步邮箱
  3. 分享一个轻型ORM--Dapper选用理由
  4. int **a[3][4] 和 sizeof(a) 和 int(**)a[3][4]
  5. Linux 支持显卡sli么,AMD Vega20专业卡将支持XGMI总线交火
  6. 网络编程--Address already in use 问题
  7. 【虚拟化实战】存储设计之七Block Size
  8. 客户端从config上获取配置
  9. 实战 Lucene,第 1 部分: 初识 Lucene
  10. Linux pause函数 详解
  11. Unity中的场景切换
  12. 【Vue】—子级向父级传递数据
  13. #题目:GCD XOR UVA - 12716
  14. iOS 6 SDK: 在应用内展示App Store
  15. IP地址库ipip.net
  16. TouchDesigner学习 全屏输出
  17. MYSQL实现排序分组取第一条sql
  18. windows提示“为了对电脑进行保护,已经阻止此应用”的解决方案 mmc.exe
  19. 区块链与区块链平台的工作流程
  20. 道与术丨华为云数据库战略启示录

热门文章

  1. python sdklive2d_【Android】用Cubism 2制作自己的Live2D——android sdk样本的下载与Android studio编译!...
  2. .2018年java还能学吗,2018年,Java程序员应该学习的 9 个建议
  3. 2018年2月16日训练日记
  4. 无法找到该页您正在搜索的页面可能已经删除、更名或暂时不可用。HTTP 错误 404 - 文件或目录未找到。
  5. 动画设计就业市场调查报告
  6. 扬帆志远—tiktok跨境电商运营流程
  7. 最小生成基环森林--bzoj4883: [Lydsy1705月赛]棋盘上的守卫
  8. 极客战记计算机科学2村庄守卫,极客战记任务攻略 任务攻略流程汇总详解[多图]...
  9. java 窗口模态_JAVA- GUI基础(模态窗口)
  10. 如何用python创建文件_python在指定目录创建文件