Java直接内存

本文介绍了如何在Java中使用直接内存(ByteBuffer的allocateDirect())提高大文件读取性能,并强调了直接缓冲区的堆外内存管理,包括使用cleaner()方法显式释放内存以避免内存耗尽问题。
摘要由CSDN通过智能技术生成

直接内存如何使用

直接上代码,代码中有注释【对直接内存的分配以及释放】进行说明。

package cn.ordinary.util.io.file;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.*;
import java.util.ArrayList;
import java.util.List;

/**
 * @Date 2024/3/14
 * @Author sh
 **/
public class FileUtils {

    /**
     * 读取文件内容。
     *
     * @param filePath 文件路径 + 文件名
     * @param charset  文件编码,默认使用 StandardCharsets.UTF_8
     * @throws FileNotFoundException 如果文件不存在
     * @throws IOException           如果读取文件发生错误
     */
    public static StringBuilder read(String filePath, Charset charset)
            throws Exception {
        try (RandomAccessFile file = new RandomAccessFile(filePath, "r");
             FileChannel fileChannel = file.getChannel()) {

            /**
             * 1). 直接内存使用
             *  ① allocate()方法:
             *      这个方法会分配一个新的 非直接缓冲区。
             *      非直接缓冲区的内容位于 Java堆内存中,这意味着它受到垃圾回收器的影响。
             *  ② allocateDirect()方法:
             *      这个方法会分配一个新的 直接缓冲区。
             *      直接缓冲区的内容位于 操作系统的本地内存中(具体的来说,应该是在 java8内存结构中本地内存中的直接内存),
             *    而不是 Java堆内存中,所以它不受垃圾回收的影响。
             *      直接缓冲区可以提供更高的 I/O性能,因为对于本地 I/O操作而言,它不需要额外的内存复制操作(Java堆 和 本地内存 之间的数据复制)。
             * 2). 缓冲区预分配
             *  ① ByteBuffer 的容量最好不要是固定值。因为,在处理大文件的时候,可能需要多个这样的缓冲区来装载整个文件的内容。
             *  ② 使用 StringBuilder 累加文件内容是很高效的,比 String 拼接在循环冲更加高效。
             *      但是,在处理大文件的时候,capacity 的大小可能需要动态调整,或者根据文件的实际大小预先分配足够的空间,避免多次扩容带来的开销。
             *  综述,对于大文件,提前知道文件大小并且预分配 ByteBuffer和StringBuilder 的大小能显著提高性能,减少内存重新分配的次数。
             */
            // 1- 创建一个 ByteBuffer(字节缓冲区),用于存放读取到的数据
            // 获取文件大小
            long fileSize = file.length();
            // 根据文件大小动态分配 ByteBuffer 的容量
            int capacity = ((int) Math.min(fileSize, Integer.MAX_VALUE));
            // 方式一:使用【非直接】字节缓冲区
            //ByteBuffer buffer = ByteBuffer.allocate(capacity);
            // 方式二:使用【直接】字节缓冲区
            ByteBuffer buffer = ByteBuffer.allocateDirect(capacity);

            // 2- 创建一个 utf8 的 CharsetDecoder
            if (null == charset) {
                charset = StandardCharsets.UTF_8;
            }
            CharsetDecoder decoder = charset.newDecoder();

            // 3- 读取数据到 buffer
            // 将读取到的内容存放到 StringBuilder
            StringBuilder content = new StringBuilder((int) fileSize);
            while (fileChannel.read(buffer) != -1) {
                // 切换到读模式,准备读取数据
                buffer.flip();
                // 使用 指定字符集 解码 ByteBuffer 到 CharBuffer,再转换为 String
                content.append(decoder.decode(buffer));
                // 清空缓冲区,准备下一次读取
                buffer.clear();
            }

            /**
             * 显示的释放 直接缓冲区占用的堆外内存。
             *
             * 通过ByteBuffer.allocateDirect()分配的直接缓冲区(Direct Buffer)使用的是JVM堆外内存。
             * 与传统的堆内存不同,直接缓冲区不受Java垃圾回收器(GC)的直接管理,但是它们仍然会被间接地管理。
             *
             * 直接缓冲区 与 一个Java对象关联(即ByteBuffer对象),该对象处于JVM的管理之下。
             * 当这个Java对象变得不可达时(即没有任何引用指向它),垃圾回收器可以对其进行垃圾回收。但是,这种方式并不是立即释放内存,而是要等到垃圾收集器运行时才会进行。
             * 在垃圾回收过程中,可以通过对象的清理(finalization)机制或者Java 9引入的Cleaner类来释放直接缓冲区占用的堆外内存。
             *
             * 依赖于垃圾回收来回收直接缓冲区的内存可能会存在延迟,因为垃圾回收器无法直接感知堆外内存的压力。
             * 因此,如果快速和频繁地分配大量的直接缓冲区,可能会导致内存耗尽,特别是在有限的堆外内存资源的情况下。
             * 为了更好地管理直接缓冲区的堆外内存,可以使用 Cleaner 类显示的释放直接缓冲区占用的堆外内存。
             */
            cleanDirectMemory(buffer);

            // 返回读取到的文件内容
            return content;
        } catch (FileNotFoundException e) {
            // 抛出文件不存在的异常,便于调用者识别和处理文件不存在的情况。
            throw new FileNotFoundException("文件不存在: " + filePath);
        } catch (IOException e) {
            // 抛出IO异常,便于调用者识别和处理IO异常。
            throw new IOException("读取文件失败: " + filePath, e);
        }
    }

    /**
     * 显式释放直接缓冲区占用的内存
     *
     * @param buffer 字节缓冲区(这里指的是 直接缓冲区,因为 非直接缓冲区 在 Java堆内存上,GC可以进行自动内存管理)
     */
    private static void cleanDirectMemory(ByteBuffer buffer) throws Exception {
        if (null == buffer) {
            return;
        }

        // 下面是 jdk8及之前的写法。从 Java9开始,推荐的释放直接缓冲区内存的方式是使用 sun.misc.Unsafe 类或者使用 java.lang.ref.Cleaner 类。
        try {
            // 调用 ByteBuffer 的 cleaner()方法 获取 Cleaner
            Method cleanerMethod = buffer.getClass().getMethod("cleaner");
            cleanerMethod.setAccessible(true);
            Object cleaner = cleanerMethod.invoke(buffer);

            // 调用 Cleaner 的 clean()方法 来显示的释放 直接缓冲区占用的 堆外内存(这里指的是 本地内存中的直接内存)。
            Method cleanMethod = cleaner.getClass().getMethod("clean");
            cleanMethod.setAccessible(true);
            cleanMethod.invoke(cleaner);
        } catch (Exception e) {
            throw new Exception("释放接缓冲区占用的堆外内存失败", e);
        }
    }


}

  • 11
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值