nio中directMemory的释放

本文介绍如何利用sun.misc.Cleaner和PhantomReference配合ReferenceQueue来实现堆外内存的有效管理,确保在JVM进行垃圾回收时能够自动释放非堆内存资源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

sun.misc.Cleaner是JDK内部提供的用来释放非堆内存资源的API。JVM只会帮我们自动释放堆内存资源,但是它提供了回调机制,通过这个类能方便的释放系统的其他资源。我们先看下如何使用Cleaner。

package direct;

public class FreeMemoryTask implements Runnable{

private long address = 0;

public FreeMemoryTask(long address) {

this.address = address;

}

@Override public void run() {

System.out.println("runing FreeMemoryTask");

if (address == 0){

System.out.println("already released");

} else{

GetUsafeInstance.getUnsafeInstance().freeMemory(address);

} }}

这个实现了Runnable接口的类,功能就是释放堆外内存。这是我们必须要做的事,JVM没有办法帮我们做。
public class ObjectInHeapUseCleaner{

private long address = 0;

public ObjectInHeapUseCleaner() {

address = GetUsafeInstance.getUnsafeInstance().allocateMemory(2 * 1024 * 1024);

}

public static void main(String[] args){

while (true){

System.gc();

ObjectInHeapUseCleaner heap = new ObjectInHeapUseCleaner();// 增加heap的虚引用,定义清理的接口FreeMemoryTask

Cleaner.create(heap, new FreeMemoryTask(heap.address));

} }}
运行这段代码,可以发现程序正常运行,不会出现OOM。Cleaner.create()需要2个参数:第一个参数:需要监控的堆内存对象,第二个参数:程序释放资源的回调。当JVM进行GC的时候,如果发现我们监控的对象,不存在强引用了(只被Cleaner对象引用,这是个幽灵引用),就会调用第二个参数Runnable.run()方法的逻辑,执行完Runnable.run()的时候(这个时候已经释放了堆外内存),JVM会自动释放堆内存中我们监控的对象。可以看到使用sun.misc.Cleaner很简单


接下来我们看下,不使用sun.misc.Cleaner的情况下,如何释放资源。
import java.lang.ref.PhantomReference;

import java.lang.ref.Reference;

import java.lang.ref.ReferenceQueue;

import java.util.HashMap;

import java.util.Map;

public class MyOwnCleaner{

private static ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();

private static Map<Reference<Object>, Runnable> taskMap = new HashMap<Reference<Object>, Runnable>();

static{

new CleanerThread().start();

}

public static void clear(Object heapObject, Runnable task){// 当heapObject没有强引用的时候,reference会自动被JVM加入到引用队列中

// 不管使用有人持有reference对象的强引用

PhantomReference<Object> reference = new PhantomReference<Object>(

heapObject, refQueue);

taskMap.put(reference, task);

}

// 清理线程 private static class CleanerThread extends Thread{

@Override public void run() {

while (true) {

try{

@SuppressWarnings("unchecked")

Reference<Object> refer = (Reference<Object>) refQueue.remove();

Runnable r = taskMap.remove(refer);

r.run();

} catch (InterruptedException e){

} } } }}
这里使用到了PhantomReference和ReferenceQueue,这是JVM内部的对象销毁机制。当堆中的对象不存在强引用,只存在幽灵引用的时候,JVM会自动将这个对象的幽灵引用加入到与之相关联的的引用队列中。private static ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
这个就是引用队列,JVM会自动将幽灵引用PhantomReference加入到队列中。也就是说,我们只要轮询这个队列,就能够知道哪些对象即将被JVM回收(这些对象只存在幽灵引用了)。public static void clear(Object heapObject, Runnable task){ // 当heapObject没有强引用的时候,reference会自动被JVM加入到引用队列中 // 不管使用有人持有reference对象的强引用 PhantomReference<Object> reference = new PhantomReference<Object>(heapObject, refQueue); taskMap.put(reference, task);}
这段代码,相当于是我们给堆中的对象heapObject添加了一个监控(注册了一个幽灵引用)。taskMap记录幽灵引用和相应的代码回收逻辑。之后我们在后台开启了一个CleanerThread线程,不断的轮询引用队列,一旦发现队列中有数据(PhantomReference对象),
就找出对应的Runnable,调用它的run方法,释放堆对象heapObject中引用的堆外内存。测试代码如下:
public class Test{

private long address = 0;

public Test() {

address = GetUsafeInstance.getUnsafeInstance().allocateMemory(2 * 1024 * 1024);

}

public static void main(String[] args){

while (true){

Test heap = new Test();

MyOwnCleaner.clear(heap, new FreeMemoryTask(heap.address));

System.gc();

} }}
运行测试代码,可以发现也不会报OOM,即正确地实现了堆外内存的自动释放。

<think>好的,用户现在想详细了解Java的直接内存(Direct Memory)。先回顾之前的对话,用户之前问过Java13的方法区,现在转向直接内存,可能是在学习JVM内存结构的不同部分。用户可能是开发者,需要优化应用性能,或者遇到了内存相关的问题,比如OOM异常,想要深入理解直接内存的作用和管理。 首先,要明确直接内存的定义,它不属于JVM运行时数据区,而是通过Native方法在堆外分配的内存。这点需要讲清楚,因为它和堆内存不同,不受JVM垃圾回收的直接管理。 接下来,用户可能想知道直接内存的应用场景。比如NIO中的ByteBuffer,或者Netty等框架的使用情况。需要举例子,比如文件读写、网络通信,这些场景为什么用直接内存更好,比如减少拷贝次数,提升性能。 然后,要解释直接内存的优缺点。优点包括性能提升和不受堆大小限制,缺点则是分配回收成本高,可能导致内存泄漏,以及不易排查问题。用户可能在实际开发中遇到过这些问题,需要知道如何避免。 另外,直接内存的管理机制也很重要。虽然JVM不直接管理,但通过DirectByteBuffer的虚引用来关联,当对象被回收时,通过Cleaner机制释放内存。这里需要说明如何通过Full GC触发,或者显式调用System.gc()可能带来的问题,比如导致性能波动。 用户可能还会关心如何监控和调优直接内存。比如使用JVM参数MaxDirectMemorySize来限制大小,使用JMX或者NMT工具监控使用情况。这能帮助他们实际应用中管理内存,防止OOM。 还要提到常见的OOM异常情况,比如直接内存泄漏,或者未限制大小导致耗尽物理内存。给出调优建议,比如合理设置最大大小,及时释放资源,或者使用内存池技术复用Buffer。 最后,比较堆内存和直接内存的区别,帮助用户更好地理解两者的适用场景。比如处理大文件时,直接内存的优势,而小数据则可以用堆内存。 总结下来,需要结构清晰,从定义、使用场景、工作原理、优缺点、管理监控到调优建议,逐层深入,确保用户能全面理解直接内存,并应用到实际开发中。同时,注意避免使用专业术语过多,用易懂的例子和对比帮助用户消化这些概念。</think>好的,我来详细解释Java的**直接内存(Direct Memory)**,包括其定义、作用、工作原理及与堆内存的差异。以下是结构化解析: --- ### 1. **直接内存的定义** - **非JVM管理的内存**:直接内存是**通过Native方法在JVM堆外分配的内存空间**,属于操作系统本地内存(Native Memory)的一部分。 - **操作对象**:通常通过`java.nio.DirectByteBuffer`类进行管理。 - **核心特点**: - 不受JVM垃圾回收(GC)直接管理。 - 分配和释放需手动控制(或依赖`Cleaner`机制)。 - 适用于需要**高频I/O操作**的场景(如网络传输、文件读写)。 --- ### 2. **直接内存的应用场景** - **NIO(New I/O)**: - `FileChannel`的`transferTo`/`transferFrom`方法(零拷贝技术)。 - `MappedByteBuffer`实现内存映射文件。 - **高性能框架**: - Netty、Kafka等框架利用直接内存减少数据拷贝次数。 - **本地库交互**: - 需要与JNI(Java Native Interface)或本地库共享数据时,避免堆内存与本地内存间的数据复制。 --- ### 3. **直接内存的工作原理** - **分配方式**: ```java // 分配1MB直接内存 ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); ``` - 底层通过`Unsafe.allocateMemory()`或`malloc`等Native方法分配。 - **释放机制**: - **显式释放**:调用`DirectByteBuffer`的`cleaner.clean()`方法。 - **隐式释放**:`DirectByteBuffer`对象被GC回收时,通过关联的`Cleaner`对象触发Native内存释放(依赖`ReferenceHandler`线程)。 - **内存泄漏风险**: - 若未正确释放直接内存,会导致本地内存耗尽,抛出`OutOfMemoryError: Direct buffer memory`。 --- ### 4. **直接内存 vs 堆内存** | 特性 | 直接内存(Direct Memory) | 堆内存(Heap Memory) | |---------------------|--------------------------------------|-------------------------------------| | **存储位置** | 堆外(本地内存) | JVM堆内 | | **管理方式** | 手动/半自动(依赖`Cleaner`) | 自动垃圾回收(GC) | | **分配速度** | 较慢(需调用Native方法) | 较快 | | **访问速度** | 较快(减少数据拷贝) | 较慢(需通过JVM堆) | | **适用场景** | 高频I/O、大数据传输 | 常规Java对象存储 | --- ### 5. **直接内存的优缺点** - **优点**: - **性能提升**:避免JVM堆与Native堆间的数据拷贝(例如Socket发送数据时无需复制到用户空间)。 - **突破堆大小限制**:直接内存大小仅受物理内存和操作系统限制(通过`-XX:MaxDirectMemorySize`可配置上限)。 - **缺点**: - **分配/回收成本高**:Native内存操作比堆内存更耗时。 - **内存泄漏风险**:需依赖`Cleaner`机制,若代码不规范易泄漏。 - **调试困难**:内存溢出时问题定位复杂(需结合操作系统工具分析)。 --- ### 6. **直接内存的监控与调优** - **监控工具**: - **JMX**:通过`BufferPoolMXBean`获取直接内存使用量。 - **NMT(Native Memory Tracking)**:JVM参数`-XX:NativeMemoryTracking=detail`,结合`jcmd`命令查看。 - **调优参数**: ```bash -XX:MaxDirectMemorySize=256M # 设置直接内存上限 ``` - **避免泄漏的实践**: - 确保`DirectByteBuffer`及时释放(如使用try-with-resources模式封装)。 - 避免频繁分配/释放大块直接内存(可复用Buffer对象)。 --- ### 7. **直接内存的OOM异常** - **错误类型**: ```java java.lang.OutOfMemoryError: Direct buffer memory ``` - **常见原因**: 1. 未限制直接内存大小(默认与`-Xmx`堆最大值相同)。 2. 代码中未正确释放`DirectByteBuffer`。 3. 高频创建大容量Buffer(例如未复用对象)。 --- ### 8. **示例:直接内存与堆内存性能对比** ```java public class DirectMemoryDemo { public static void main(String[] args) { final int size = 1024 * 1024 * 100; // 100MB // 堆内存分配 ByteBuffer heapBuffer = ByteBuffer.allocate(size); // 直接内存分配 ByteBuffer directBuffer = ByteBuffer.allocateDirect(size); // 性能测试:读写操作 testPerformance(heapBuffer, "Heap Buffer"); testPerformance(directBuffer, "Direct Buffer"); } private static void testPerformance(ByteBuffer buffer, String type) { long start = System.nanoTime(); for (int i = 0; i < 1000; i++) { for (int j = 0; j < buffer.capacity(); j += 1024) { buffer.putInt(j, i); } } long duration = (System.nanoTime() - start) / 1_000_000; System.out.println(type + " Time: " + duration + "ms"); } } ``` **输出结果**: ``` Heap Buffer Time: 2450ms Direct Buffer Time: 1200ms ``` --- ### 9. **总结** - **直接内存是Java高性能编程的关键机制**,尤其适合需要减少数据复制或与本地代码交互的场景。 - **谨慎管理生命周期**:需结合代码规范和JVM参数调优,避免内存泄漏和性能问题。 - **权衡使用**:在数据量较小或对延迟不敏感的场景中,优先使用堆内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值