文章目录
- Pre
- 概述
- 应用
- 访问效率: 堆内存 VS 直接内存
- 申请效率: 堆内存 VS 直接内存
- 数据存储结构: 堆内存 VS 直接内存
- 结论
- ByteBuffer.allocateDirect 源码分析
- unsafe.allocateMemory(size) ---> C++方法
- JVM参数 -XX:MaxDirectMemorySize
- 直接内存如何管理?
- 理论
- Code
- 总结
- 优点
- 缺点
Pre
Netty Review - ServerBootstrap源码解析
Netty Review - NioServerSocketChannel源码分析
Netty Review - 服务端channel注册流程源码解析
概述
在Java中,数据通常存储在堆内存中。Java里用DirectByteBuffer
可以分配一块直接内存(堆外内存),元空间对应的内存也叫作直接内存,它们对应的都是机器的物理内存。
但是,在某些情况下,直接操作系统的本地内存(off-heap memory)可能更有利,特别是对于需要进行大量I/O操作的应用程序,比如网络应用程序。Netty是一个用于构建高性能网络应用程序的框架,它提供了对直接内存的支持,以便更有效地处理数据传输。
直接内存的主要优势在于它的分配和释放不受Java堆内存管理的影响,因此可以避免堆内存的垃圾回收开销。由于直接内存是在操作系统层面分配和释放的,因此它不受Java虚拟机的堆内存大小限制,可以更灵活地管理大量的数据。
在Netty中,直接内存通常用于存储网络数据,例如接收到的字节数据或要发送的字节数据。通过使用直接内存,Netty能够更有效地进行数据传输,减少了数据在Java堆内存和操作系统内存之间的复制操作,提高了数据传输的效率和性能。
为了有效地管理直接内存的分配和释放,Netty使用了内存池(Memory Pool)的概念。通过内存池,Netty可以重用已分配的直接内存,避免频繁地进行内存分配和释放操作,减少了系统的内存管理开销,并提高了系统的稳定性和可靠性。
总而言之,Netty的直接内存支持使得开发人员能够构建高性能、高效率的网络应用程序,通过更有效地利用操作系统的本地内存,提高了数据传输的速度和性能,同时降低了系统的内存管理开销。
应用
访问效率: 堆内存 VS 直接内存
package com.artisan.directbuffer;
import java.nio.ByteBuffer;
/**
* 直接内存与堆内存的区别
* @author artisan
*/
public class DirectMemoryTest {
public static void heapAccess() {
long startTime = System.currentTimeMillis();
//分配堆内存
ByteBuffer buffer = ByteBuffer.allocate(1000);
for (int i = 0; i < 100000; i++) {
for (int j = 0; j < 200; j++) {
buffer.putInt(j);
}
buffer.flip();
for (int j = 0; j < 200; j++) {
buffer.getInt();
}
buffer.clear();
}
long endTime = System.currentTimeMillis();
System.out.println("堆内存访问:" + (endTime - startTime) + "ms");
}
public static void directAccess() {
long startTime = System.currentTimeMillis();
//分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1000);
for (int i = 0; i < 100000; i++) {
for (int j = 0; j < 200; j++) {
buffer.putInt(j);
}
buffer.flip();
for (int j = 0; j < 200; j++) {
buffer.getInt();
}
buffer.clear();
}
long endTime = System.currentTimeMillis();
System.out.println("直接内存访问:" + (endTime - startTime) + "ms");
}
public static void main(String args[]) {
for (int i = 0; i < 5; i++) {
heapAccess();
directAccess();
}
}
}
申请效率: 堆内存 VS 直接内存
package com.artisan.directbuffer;
import java.nio.ByteBuffer;
/**
* 直接内存与堆内存的区别
* @author artisan
*/
public class DirectMemoryTest {
public static void heapAllocate() {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
ByteBuffer.allocate(100);
}
long endTime = System.currentTimeMillis();
System.out.println("堆内存申请:" + (endTime - startTime) + "ms");
}
public static void directAllocate() {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
ByteBuffer.allocateDirect(100);
}
long endTime = System.currentTimeMillis();
System.out.println("直接内存申请:" + (endTime - startTime) + "ms");
}
public static void main(String args[]) {
for (int i = 0; i < 5; i++) {
heapAllocate();
directAllocate();
}
}
}
数据存储结构: 堆内存 VS 直接内存
结论
优点:
- 不占用堆内存空间,减少了发生GC的可能
- java虚拟机实现上,本地IO会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)
缺点:
- 初始分配较慢
- 没有JVM直接帮助管理内存,容易发生内存溢出。为了避免一直没有FULL GC,最终导致直接内存把物理内存耗完。我们可以指定直接内存的最大值,通过-XX:MaxDirectMemorySize来指定,当达到阈值的时候,调用system.gc来进行一次FULL GC,间接把那些没有被使用的直接内存回收掉
从程序运行结果看出直接内存申请较慢,但访问效率高。在java虚拟机实现上,本地IO一般会直接操作直接内存(直接内存=>系统调用=>硬盘/网卡),而非直接内存则需要二次拷贝(堆内存=>直接内存=>系统调用=>硬盘/网卡)。
直接内存在申请时可能会比在堆内存中分配的速度慢一些,这是因为在堆内存中分配只涉及Java堆内存管理系统的操作,而在直接内存中分配则涉及到操作系统的系统调用,因此可能会有更多的开销。但是,一旦分配完成,直接内存的访问效率通常会比堆内存高,因为它可以直接被操作系统访问,而不需要经过Java堆内存管理系统的复制操作。
在Java虚拟机的实现中,对于本地IO操作,如果使用直接内存,则可以直接操作直接内存,然后通过系统调用将数据传输到硬盘或网卡。这样可以避免额外的内存复制操作,提高了IO操作的效率。而对于堆内存中的数据,需要先将数据复制到直接内存中,然后再进行系统调用传输到硬盘或网卡,这就需要进行额外的数据拷贝,导致了额外的开销和性能损失。
因此,对于需要进行大量IO操作的应用程序,使用直接内存通常能够获得更好的性能表现。然而,直接内存的使用也需要注意管理和释放,以避免内存泄漏和其他潜在的问题。
ByteBuffer.allocateDirect 源码分析
public static ByteBuffer allocateDirect(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new DirectByteBuffer(capacity);
}
这是DirectByteBuffer类的构造函数实现,用于创建直接字节缓冲区对象。
DirectByteBuffer(int cap) { // 包私有
super(-1, 0, cap, cap); // 调用父类构造函数,设置position和limit为0,capacity为cap
// 判断是否需要在直接内存页对齐
boolean pa = VM.isDirectMemoryPageAligned();
// 获取操作系统页面大小
int ps = Bits.pageSize();
// 计算需要分配的内存大小,如果需要页对齐,则在容量上加上一个页面大小
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//判断是否有足够的直接内存空间分配,可通过‐XX:MaxDirectMemorySize=<size>参数指定直接内存最大可分配空间,如果不指定默认为最大堆内存大小,
//在分配直接内存时如果发现空间不够会显示调用System.gc()触发一次full gc回收掉一部分无用的直接内存的引用对象,同时直接内存也会被释放掉. 如果释放完分配空间还是不够会抛出异常java.lang.OutOfMemoryError
// 预留直接内存,如果分配失败,抛出OutOfMemoryError
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配内存,并返回分配的内存地址
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机制注册内存回收处理函数,当直接内存引用对象被GC清理掉时,会提前调用这里注册的释放直接内存的Deallocator线程对象的run方法
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
该构造函数执行以下操作:
- 调用父类构造函数,初始化ByteBuffer的position和limit为0,capacity为cap。
- 判断是否需要进行直接内存页对齐。
- 获取操作系统的页面大小。
- 计算需要分配的内存大小,如果需要页对齐,则在容量上加上一个页面大小。
- 预留直接内存。
- 分配内存,并返回分配的内存地址。
- 初始化分配的内存为0。
- 如果需要进行页对齐,并且分配的内存地址不在页面边界上,将地址向上舍入到页面边界。
- 创建Cleaner对象,用于释放内存。
- 最后,att字段设置为null,该字段用于存储可选的附件对象。
unsafe.allocateMemory(size) —> C++方法
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
// 使用UnsafeWrapper宏定义的函数进行包装
UnsafeWrapper("Unsafe_AllocateMemory");
// 将传入的jlong类型的size转换为size_t类型的sz,这是为了在C++中使用
size_t sz = (size_t)size;
// 检查size是否为负数或超出了jlong类型的范围,如果是,则抛出IllegalArgumentException异常
if (sz != (julong)size || size < 0) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
// 如果size为0,则直接返回0,不进行内存分配
if (sz == 0) {
return 0;
}
// 将sz向上舍入到HeapWordSize的倍数,确保内存对齐
sz = round_to(sz, HeapWordSize);
// 调用os::malloc函数分配内存。os::malloc是JVM调用操作系统的内存分配函数,通常是malloc
void* x = os::malloc(sz, mtInternal);
// 如果内存分配失败(返回NULL),则抛出OutOfMemoryError异常
if (x == NULL) {
THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}
// 将分配的内存地址转换为Java对象引用并返回
return addr_to_java(x);
UNSAFE_END
这段代码是在C++中实现的Unsafe_AllocateMemory函数, 通过Unsafe类的allocateMemory方法在本地堆上分配内存的底层实现。
JVM参数 -XX:MaxDirectMemorySize
在Java应用的默认情况下,可以使用的最大直接内存取决于系统的限制和JVM参数的设置。一般来说,默认情况下,JVM并不限制直接内存的使用,但是操作系统可能会对进程的虚拟内存大小进行限制。
在某些操作系统上,进程可以使用的虚拟内存大小受到32位或64位系统的限制,以及特定操作系统的配置和限制。另外,有些操作系统还可能会限制单个进程可分配的直接内存的大小。
通常情况下,如果没有显式设置直接内存的大小(例如通过-XX:MaxDirectMemorySize
参数),Java应用程序可以使用的最大直接内存大小与堆内存大小没有直接关系。默认情况下,Java应用程序可以根据操作系统和硬件配置使用尽可能多的直接内存,但是受到操作系统的限制。
也有一种说法,默认能够使用的最大的直接内存 = 最大堆内存。 待考证。
直接内存如何管理?
理论
ByteBuffer.allocateDirect()
方法用于申请直接内存,这种内存是由操作系统分配的,而不是由JVM的堆内存管理器分配的。因此,直接内存的管理和回收与堆内存不同。
直接内存的管理和回收通常由操作系统来完成。当调用 allocateDirect()
方法时,会向操作系统请求一块内存区域。这个区域的分配和释放由操作系统的内存管理机制来管理,而不是由JVM的垃圾回收器来管理。
直接内存的释放不是由 Java 的垃圾回收器来处理的,而是由操作系统的内存管理机制来处理。当不再需要直接内存时,ByteBuffer
对象可以被垃圾回收器回收,但是直接内存本身并不会被立即释放。相反,直接内存的释放可能会延迟到 JVM 关闭时,或者在应用程序调用 System.gc()
进行垃圾回收时。
另外,可以使用 ByteBuffer
的 clear()
方法或者手动调用 System.gc()
来提示 JVM 尽快释放直接内存。但是这并不能保证直接内存会立即被释放,因为直接内存的释放时间是由操作系统来决定的。
Code
一个简单的案例和代码实现,演示了如何手动管理直接内存的分配和释放
package com.artisan.directbuffer;
import java.nio.ByteBuffer;
/**
* @author 小工匠
* @version 1.0
* @mark: show me the code , change the world
*/
public class DirectMemoryManager {
private ByteBuffer buffer;
public DirectMemoryManager(int capacity) {
// 申请直接内存
buffer = ByteBuffer.allocateDirect(capacity);
}
// 使用直接内存进行读写操作
public void writeToBuffer(byte[] data) {
buffer.put(data);
}
public byte[] readFromBuffer() {
// 将缓冲区的位置设为 0,限制设为当前位置
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
return data;
}
// 释放直接内存
public void releaseMemory() {
// 释放 buffer 对象
buffer.clear();
// 将 buffer 设置为 null,便于垃圾回收
buffer = null;
// 强制调用垃圾回收器进行内存回收
System.gc();
}
public static void main(String[] args) {
// 创建直接内存管理器并分配直接内存
DirectMemoryManager memoryManager = new DirectMemoryManager(1024);
// 使用直接内存进行读写操作
byte[] data = "Hello, Direct Memory!".getBytes();
memoryManager.writeToBuffer(data);
byte[] readData = memoryManager.readFromBuffer();
System.out.println("Read from buffer: " + new String(readData));
// 释放直接内存
memoryManager.releaseMemory();
}
}
在这个例子中,DirectMemoryManager
类负责管理直接内存。它通过调用 ByteBuffer.allocateDirect(capacity)
方法来申请直接内存,并通过 writeToBuffer()
和 readFromBuffer()
方法进行读写操作。
最后,通过调用 releaseMemory()
方法释放直接内存。
在释放直接内存时,首先调用 buffer.clear()
方法清空缓冲区,然后将 buffer
对象置为 null
,最后强制调用 System.gc()
方法触发垃圾回收。
请注意,这并不能保证直接内存会立即被释放,因为直接内存的释放时间是由操作系统来决定的。
总结
优点
-
减少了垃圾回收的影响: 直接内存不受 Java 堆内存大小的限制,可以避免频繁的垃圾回收,提高应用的性能和稳定性。
-
减少了数据拷贝: 直接内存与操作系统进行了直接交互,减少了数据在 Java 堆内存与操作系统内存之间的拷贝次数,提高了 I/O 操作的效率。
缺点
-
初始分配较慢: 直接内存的分配过程通常比堆内存的分配要慢,因为它需要调用操作系统的原生方法来申请内存空间。
-
内存管理复杂: 直接内存的管理需要手动进行,没有 JVM 自动进行内存回收的机制,容易导致内存泄漏或者内存溢出的问题。
-
容易导致内存溢出: 如果不合理地使用直接内存,可能会导致操作系统的物理内存被耗尽,从而引发应用程序的崩溃。
-
难以调试: 直接内存的内存溢出问题比较难以调试和定位,需要借助一些专业的内存分析工具来进行排查。
总的来说,直接内存适用于需要频繁进行 I/O 操作或者数据量较大的场景,但需要开发人员在使用时注意合理管理和控制直接内存的大小,避免出现性能问题或者内存溢出的情况。