介绍
最近在工作中使用到了DirectBuffer来进行临时数据的存放,由于使用的是堆外内存,省去了数据到内核的拷贝,因此效率比用ByteBuffer要高不少。之前看过许多介绍DirectBuffer的文章,在这里从源码的角度上来看一下DirectBuffer的原理。
用户态和内核态
Intel的 X86架构下,为了实现外部应用程序与操作系统运行时的隔离,分为了Ring0-Ring3四种级别的运行模式。Linux/Unix只使用了Ring0和Ring3两个级别。Ring0被称为用户态,Ring3被称为内核态。普通的应用程序只能运行在Ring3,并且不能访问Ring0的地址空间。操作系统运行在Ring0,并提供系统调用供用户态的程序使用。如果用户态的程序的某一个操作需要内核态来协助完成(例如读取磁盘上的某一段数据),那么用户态的程序就会通过系统调用来调用内核态的接口,请求操作系统来完成某种操作。
下图是用户态调用内核态的示意图:
DirectBuffer的创建
使用下面一行代码就可以创建一个1024字节的DirectBuffer:
ByteBuffer.allocateDirect(1024);
该方法调用的是new DirectByteBuffer(int cap)。DirectByteBuffer的构造函数是包级私有的,因此外部是调用不到的。
下面我们来看一下这行代码背后的逻辑:
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned(); //是否页对齐
int ps = Bits.pageSize(); //获取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 = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
DirectBuffer的构造函数主要做以下三个事情:
1、根据页对齐和pageSize来确定本次的要分配内存实际大小
2、实际分配内存,并且记录分配的内存大小
3、声明一个Cleaner对象用于清理该DirectBuffer内存
需要注意的是DirectBuffer的创建是比较耗时的,所以在一些高性能的中间件或者应用下一般会做一个对象池,用于重复利用DirectBuffer。
DirectBuffer的使用
查看DirectBuffer类的方法声明,对于DirectBuffer的使用主要有两类方法,putXXX和getXXX。
putXXX方法(以putInt为例):
public ByteBuffer putInt(int x) {
putInt(ix(nextPutIndex((1 << 2))), x);
return this;
}
private ByteBuffer putInt(long a, int x) {
if (unaligned) {
int y = (x);
unsafe.putInt(a, (nativeByteOrder ? y : Bits.swap(y)));
} else {
Bits.putInt(a, x, bigEndian);
}
return this;
}
putInt方法会根据是否是内存对齐分别调用unsafe.putInt或者Bits.putInt来把数据放到直接内存中。Bits.putInt实际上会根据是大端或者是小端来区分如何把数据放到直接内存中,放的方式同样是调用unsage.putInt。
getXXX方法(以getInt为例):
public int getInt() {
return getInt(ix(nextGetIndex((1 << 2))));
}
private int getInt(long a) {
if (unaligned) {
int x = unsafe.getInt(a);
return (nativeByteOrder ? x : Bits.swap(x));
}
return Bits.getInt(a, bigEndian);
}
首先判断是否是页对齐,如果不是页对齐,那么直接通过unsafe.getInt来获取数据;如果是页对齐,那么通过Bits.getInt方法来获取数据。Bits.getInt同样是根据大端还是小端,调用unsafe.getInt来获取数据。
DirectBuffer内存回收
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 luck
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
reserveMemory方法首先尝试分配内存,如果分配成功的话,那么就直接退出。如果分配失败那么就通过调用tryHandlePendingReference来尝试清理堆外内存(最终调用的是Cleaner的clean方法,其实就是unsafe.freeMemory然后释放内存),清理完内存之后再尝试分配内存。如果还是失败,调用System.gc()来触发一次FullGC进行回收(前提是没有加-XX:-+DisableExplicitGC参数)。GC完之后再进行内存分配,失败的话就会进行sleep,然后再进行尝试。每次sleep的时间是逐步增加的,规律是1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)。如果最终还没有可分配的内存,那么就会抛出OOM异常。
为什么是通过调用tryHandlePendingReference来回收内存呢?答案是JVM在判断内存不可达之后会把需要GC的不可达对象放在一个PendingList中,然后应用程序就可以看到这些对象。通过调用tryHandlePendingReference来访问这些不可达对象。如果不可达对象是Cleaner类型,也就是说关联了堆外的DirectBuffer,那么该DirectBuffer就可以被回收了,通过调用Cleaner的clean方法来回收这部分堆外内存。
这个逻辑就是进行堆外内存分配时触发的回收内存逻辑,也就是说在分配的时候如果遇到堆外内存不足,可能会触发FullGC,然后尝试进行分配。这也是为什么在一些用到堆外内存的应用中不建议加上-XX:-+DisableExplicitGC参数。
Cleaner对象回收
另个触发堆外内存回收的时机是通过Cleaner对象的clean方法进行回收。在每次新建一个DirectBuffer对象的时候,会同时创建一个Cleaner对象,同一个进程创建的所有的DirectBuffer对象跟Cleaner对象的个数是一样的,并且所有的Cleaner对象会组成一个链表,前后相连。
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
return add(new Cleaner(ob, thunk));
}
Cleaner对象的clean方法执行时机是JVM在判断该Cleaner对象关联的DirectBuffer已经不被任何对象引用了(也就是经过可达性分析判定为不可达的时候)。此时Cleaner对象会被JVM挂到PendingList上。然后有一个固定的线程扫描这个List,如果遇到Cleaner对象,那么就执行clean方法。
DirectBuffer在一些高性能的中间件上使用还是相当广泛的。正确的使用可以提升程序的性能,降低GC的频率。
-------------------------------------------------------------------
欢迎关注我的微信公众号:yunxi-talk,分享Java干货,进阶Java程序员必备。
相关推荐
根据提供的文件信息,“深入理解Java虚拟机(第二版)(彩色高清).pdf”,我们可以推断这本书主要聚焦于Java虚拟机(JVM)的深入解析和技术细节。以下是对该书籍可能涉及的一些关键知识点的概括和解释。 ### Java...
对于Java框架,如Netty,深入理解其源码有助于我们更好地掌握其高性能、高并发的特性。 首先,Netty的主从Reactor线程模型是其高效处理网络请求的基础。主Reactor负责监听和分发连接,而从Reactor则负责处理具体的...
Apache Mina是一个高性能的网络应用框架,主要用于简化网络...通过阅读《深入理解Apache Mina (6)---- Java Nio ByteBuffer与Mina ByteBuffer的区别》的相关资料,可以更深入地理解这两个类的具体实现和应用场景。
《深入理解JVM内存区域》 Java虚拟机(JVM)是Java语言的运行环境,支持多种语言,包括Scala、Kotlin、Groovy等。虚拟机历史了解即可,无需关注Hotspot。 JVM内存区域主要分为五部分:程序计数器、虚拟机栈、本地...
《DirectByteBuffer2》是Java基础课程中的一个...然而,正确使用直接缓冲区需要对Java内存模型和NIO有深入理解,以避免潜在的问题,如内存泄漏和碎片化。在适当的情景下,合理利用直接缓冲区可以极大地提升程序性能。
《深入浅出NIO》 在Java编程领域,NIO(New Input/Output)是一种用于替代标准IO模型的机制,其核心在于非阻塞的I/O操作和通道(Channel)及缓冲区(Buffer)的使用。传统的IO模型,如描述中的“阻塞I/O”,在读写...
通过阅读和理解这些源代码,开发者可以深入理解Hadoop在处理大数据时的底层逻辑,从而更好地优化应用性能,解决系统中的问题,甚至为Hadoop贡献新的特性。对于想要成为Hadoop专家的人来说,这是一个宝贵的资源。同时...
【深入解读 Java 堆外内存(直接内存)】 Java 堆外内存,又称直接内存(Direct Memory),是 Java 程序中除JVM堆内存之外的一种内存区域。它并不遵循JVM规范中定义的标准内存模型,而是直接与操作系统交互,用于...
深入理解Java虚拟机对于Java开发者来说至关重要,它能帮助我们优化程序性能、理解和解决运行时问题。 一、Java虚拟机结构 Java虚拟机主要由以下几个部分组成: 1. 类装载器:负责加载类文件,验证其合法性,并将...
5. 变量与数据类型:深入理解基本数据类型和引用数据类型的差异。 二、数据结构与算法 1. 链表、数组、队列、栈:掌握这些基础数据结构的实现和操作。 2. 排序算法:理解冒泡、选择、插入、快速、归并等排序算法的...
这个“netty-learning学习Java源代码.zip”压缩包包含的是关于学习Netty框架的Java源代码示例,非常适合那些希望深入理解Netty工作原理以及如何在实际项目中应用它的开发者。 Netty 的核心特性包括: 1. **异步...
【深度解析:如何通过源码学习提升技术功底与...总之,源码学习是提升技术深度和广度的有效手段,通过学习Netty这样的高性能框架,我们可以深入理解并发处理、内存管理等关键技术,为实际开发工作提供强大的理论支持。
Java 开发者在深入理解内存管理时,不能忽视堆外内存这一重要概念。堆外内存,也称为直接内存(DirectMemory),是指在JVM堆内存之外分配的内存区域。它并非JVM规范定义的标准内存区域,但因其在IO操作中的性能优势...
本书基于Netty 5.0版本,详细阐述了其设计理念和实现机制,旨在帮助读者深入理解并有效利用Netty进行网络应用的开发。 首先,Netty的异步非阻塞模型是其高效性能的关键。在传统的IO模型中,阻塞IO会导致线程在等待...
下面将围绕Netty的核心概念、特性、以及如何通过学习笔记来深入理解Netty进行详细阐述。 1. **Netty核心概念** - **Channel**: Netty中的基本I/O组件,代表一个到另一端点的连接,可以用来读取和写入数据。 - **...
通过分析和学习这个项目,我们可以深入理解Netty的工作原理以及如何在实际应用中利用它。 1. **Netty概述**:Netty是由JBOSS提供的一个开源框架,基于Java NIO(非阻塞I/O)构建,它提供了一组高度优化的网络操作...
本项目"itstack-demo-netty-master.zip"是针对Netty的一个示例代码库,旨在帮助开发者深入理解并掌握Netty的核心特性与用法。 1. **Netty的基本概念** - **BossGroup和WorkerGroup**:Netty中核心组件...
排查此类问题需要对Netty的内存分配机制有深入理解。首先,开发者需要了解DirectByteBuffer的生命周期,以及如何正确释放它们。在Netty中,ChannelBufferFactory可以用来创建DirectByteBuffer,而使用完后必须通过...
1. **ByteBufAllocator**: 提供了多种内存分配策略,如 DirectByteBuffer 和 HeapByteBuffer,可根据场景选择最合适的内存分配方式。 2. **ChannelHandlerContext**: 作为 Handler 与 EventLoop 和 Pipeline 交互的...
总结来说,理解和处理`OutOfMemoryError`需要对Java内存模型有深入理解,以及对JVM参数的熟练掌握。通过对代码的优化,调整JVM配置,以及选择合适的垃圾收集器,可以有效地避免和解决这类问题。在实际开发中,我们...