0

0

JNI DirectByteBuffer 数据免复制上传 S3 的高效策略

DDD

DDD

发布时间:2025-09-21 20:36:00

|

948人浏览过

|

来源于php中文网

原创

JNI DirectByteBuffer 数据免复制上传 S3 的高效策略

本教程旨在解决Java应用中,通过JNI从共享内存获取的DirectByteBuffer数据上传至S3时,避免不必要的JVM堆内存复制问题。我们将介绍如何通过实现自定义的ByteSource和ByteBufferInputStream,直接将DirectByteBuffer中的数据流式传输至S3,从而提升性能并减少内存开销。此方法特别适用于处理大容量非堆内存数据,实现高效的数据传输。

1. JNI DirectByteBuffer 与 S3 上传的挑战

在高性能java应用中,尤其是在与c++等原生代码进行数据交互时,directbytebuffer(直接缓冲区)是管理非堆内存的常用机制。通过jni(java native interface),java程序可以获取并操作直接映射到共享内存的directbytebuffer,从而实现java与c++进程间的高效数据共享,避免数据在jvm堆与非堆之间的来回复制。

例如,一个典型的JNI函数可能如下所示,它将一个C++共享内存地址映射为一个Java DirectByteBuffer:

JNIEXPORT jobject JNICALL Java_service_SharedMemoryJNIService_getDirectByteBuffer
  (JNIEnv *env, jclass jobject, jlong buf_addr, jint buf_len){
        return env->NewDirectByteBuffer((void *)buf_addr, buf_len);
  }

当我们需要将这个DirectByteBuffer中的数据上传到Amazon S3等云存储服务时,传统的做法往往会引入不必要的内存复制。

2. 传统上传方案的局限性

许多云存储客户端库(例如jclouds的早期版本或某些简化API)在处理数据上传时,倾向于接收一个字节数组(byte[])作为数据源。这意味着,如果我们的数据存储在DirectByteBuffer中,就必须先将其内容复制到JVM堆上的一个byte[]数组中,然后再进行上传。

以下是一个典型的传统上传代码示例,它展示了这种内存复制:

import org.jclouds.blobstore.BlobStore;
import org.jclouds.blobstore.BlobStoreContext;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.io.payloads.ByteArrayPayload;

import java.nio.ByteBuffer;

public class S3Uploader {

    // 假设 getBlobStoreContext() 方法已存在并返回 BlobStoreContext 实例
    private BlobStoreContext getBlobStoreContext() {
        // ... 返回 BlobStoreContext 实例
        return null;
    }

    public String uploadByteBuffer(String container, String objectKey, ByteBuffer bb) {
        BlobStoreContext context = getBlobStoreContext();
        BlobStore blobStore = context.getBlobStore();

        // 问题所在:将 DirectByteBuffer 的内容复制到 JVM 堆内存中的 byte[]
        byte[] buf = new byte[bb.capacity()];
        bb.get(buf); // 这一步产生了不必要的内存复制

        ByteArrayPayload payload = new ByteArrayPayload(buf);
        Blob blob = blobStore.blobBuilder(objectKey)
                .payload(payload)
                .contentLength(bb.capacity())
                .build();
        blobStore.putBlob(container, blob);
        return objectKey;
    }
}

对于小文件而言,这种复制的开销可能不明显。然而,当处理大容量数据(如50MB甚至更大)时,将DirectByteBuffer中的数据完整复制到JVM堆内存中,会带来显著的性能下降和内存压力:

  • 内存消耗翻倍: 数据在非堆内存和JVM堆内存中各存在一份,增加了总内存占用
  • 垃圾回收负担: 堆内存中的byte[]会成为GC(Garbage Collection)的负担,尤其是在高并发或连续上传场景下。
  • CPU开销: 复制操作本身需要CPU时间,降低了数据处理效率。

3. 优化方案:基于 ByteSource 的免复制上传

为了避免上述问题,我们可以利用jclouds等库提供的更灵活的数据源抽象——com.google.common.io.ByteSource。ByteSource是一个Guava库提供的接口,它代表一个可以提供字节流的数据源。通过实现自定义的ByteSource,我们可以直接从DirectByteBuffer中读取数据,并将其封装成InputStream,从而实现数据的流式传输,避免一次性加载到堆内存。

CodeBuddy
CodeBuddy

腾讯云AI代码助手

下载

核心思想是创建一个ByteSource的实现类,其中openStream()方法返回一个能够直接从DirectByteBuffer读取数据的InputStream。

4. 实现 ByteBufferByteSource

我们需要创建两个类:一个继承自ByteSource的ByteBufferByteSource,以及一个作为其内部类的InputStream实现ByteBufferInputStream。

import com.google.common.base.Preconditions; // 用于参数检查,如果Guava未引入,可替换为普通null检查
import com.google.common.io.ByteSource;

import java.io.IOException;
import java.io.InputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;

/**
 * 一个 ByteSource 实现,用于直接从 ByteBuffer 读取数据。
 * 这避免了在上传 DirectByteBuffer 数据时将其复制到堆内存字节数组。
 */
public class ByteBufferByteSource extends ByteSource {
    private final ByteBuffer buffer; // 原始缓冲区,通常是一个 DirectByteBuffer

    /**
     * 构造一个 ByteBufferByteSource 实例。
     * 提供的 ByteBuffer 应该处于可读状态(例如,数据写入后可能已执行 flip() 操作)。
     *
     * @param buffer 要读取的 ByteBuffer。不能为 null。
     */
    public ByteBufferByteSource(ByteBuffer buffer) {
        this.buffer = Preconditions.checkNotNull(buffer, "ByteBuffer 不能为空");
    }

    @Override
    public InputStream openStream() {
        // 创建缓冲区的副本,以确保对原始缓冲区的后续操作或对 openStream() 的多次调用
        // 不会干扰此流的当前位置。副本与原始缓冲区共享底层数据,但拥有独立的 position、limit 和 mark。
        return new ByteBufferInputStream(buffer.duplicate());
    }

    /**
     * 一个 InputStream 实现,用于直接从 ByteBuffer 读取数据。
     */
    private static final class ByteBufferInputStream extends InputStream {
        private final ByteBuffer buffer;
        private boolean closed = false;

        ByteBufferInputStream(ByteBuffer buffer) {
            this.buffer = buffer;
        }

        @Override
        public synchronized int read() throws IOException {
            if (closed) {
                throw new IOException("流已关闭");
            }
            if (!buffer.hasRemaining()) {
                return -1; // 流结束
            }
            try {
                // 读取单个字节并将其作为整数返回 (0-255)。
                // & 0xFF 确保字节被视为无符号整数。
                return buffer.get() & 0xFF;
            } catch (BufferUnderflowException bue) {
                // 理论上在检查 hasRemaining() 后不应发生,但作为安全措施。
                return -1;
            }
        }

        @Override
        public synchronized int read(byte[] b, int off, int len) throws IOException {
            if (closed) {
                throw new IOException("流已关闭");
            }
            if (b == null) {
                throw new NullPointerException("缓冲区 'b' 不能为 null");
            }
            if (off < 0 || len < 0 || len > b.length - off) {
                throw new IndexOutOfBoundsException(
                        String.format("无效的读取参数: b.length=%d, off=%d, len=%d", b.length, off, len));
            }
            if (len == 0) {
                return 0; // 没有字节需要读取
            }

            int bytesToRead = Math.min(len, buffer.remaining());
            if (bytesToRead == 0) {
                return -1; // 流结束
            }

            // 直接将数据读取到提供的字节数组中。
            buffer.get(b, off, bytesToRead);
            return bytesToRead;
        }

        @Override
        public void close() throws IOException {
            super.close();
            closed = true;
            // 底层 ByteBuffer 不在此处关闭,因为其生命周期由外部管理。
            // 它是共享内存的一个视图,因此关闭它是不合适的。
        }
    }
}

代码说明:

  • ByteBufferByteSource 构造函数接收一个 ByteBuffer 实例。openStream() 方法返回一个 ByteBufferInputStream 实例,它内部持有一个 ByteBuffer 的副本。使用副本是为了保护原始 ByteBuffer 的状态,允许在不同流操作中独立管理读取位置。
  • ByteBufferInputStream 实现了 InputStream 接口。
  • read() 方法每次读取一个字节,并返回其无符号整数值。
  • read(byte[] b, int off, int len) 方法是效率的关键。它允许批量读取数据到目标字节数组,避免了单字节读取的开销,显著提升了IO性能。
  • close() 方法仅标记流为已关闭,但不会关闭底层的 ByteBuffer,因为 DirectByteBuffer 通常管理的是共享内存,其生命周期独立于Java流。

5. 如何使用自定义 ByteSource 上传 S3

有了 ByteBufferByteSource 类,我们就可以修改上传逻辑,直接使用它来创建 Blob,从而避免中间的堆内存复制。

import org.jclouds.blobstore.BlobStore;
import org.jclouds.blobstore.BlobStoreContext;
import org.jclouds.blobstore.domain.Blob;

import java.nio.ByteBuffer;

public class OptimizedS3Uploader {

    // 假设 getBlobStoreContext() 方法已存在并返回 BlobStoreContext 实例
    private BlobStoreContext getBlobStoreContext() {
        // ... 返回 BlobStoreContext 实例
        return null;
    }

    /**
     * 将 DirectByteBuffer 中的数据直接上传到 S3,避免中间的堆内存复制。
     *
     * @param container      S3 存储桶名称
     * @param objectKey      S3 对象键
     * @param directByteBuffer 包含待上传数据的 DirectByteBuffer
     * @return 上传的 S3 对象键
     */
    public String uploadDirectByteBufferToS3(String container, String objectKey, ByteBuffer directByteBuffer) {
        BlobStoreContext context = getBlobStoreContext();
        BlobStore blobStore = context.getBlobStore();

        // 从 DirectByteBuffer 创建自定义的 ByteSource
        ByteBufferByteSource byteSource = new ByteBufferByteSource(directByteBuffer);

        // 使用 ByteSource 作为 payload,避免数据复制
        Blob blob

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
guava包作用
guava包作用

guava是一个java库,增强了java标准库,提供更有效率和易于使用的集合、实用程序、缓存和并发工具。想了解更多guava的相关内容,可以阅读本专题下面的文章。

261

2024.05.29

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

463

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

544

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

93

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

200

2025.08.29

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

1133

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

213

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

1801

2025.12.29

java入门学习合集
java入门学习合集

本专题整合了java入门学习指南、初学者项目实战、入门到精通等等内容,阅读专题下面的文章了解更多详细学习方法。

1

2026.01.29

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 7.9万人学习

Java 教程
Java 教程

共578课时 | 53.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号