You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java中无GC开销将String以UTF-16高效复制到预分配ByteBuffer

解答:String转UTF-16 ByteBuffer的JIT优化与高效实现

JIT优化情况

HotSpot的C2编译器(Server模式下的JIT)在满足特定条件时,会对你的循环代码做大幅优化:

  • 当循环次数足够多(达到JIT编译阈值)
  • 目标ByteBuffer是HeapByteBuffer(直接操作Java数组,优化空间更大)
  • 代码没有异常分支、反射调用这类阻碍优化的逻辑

具体优化点包括:

  • 循环展开:把多次putChar操作合并,减少循环控制的额外开销
  • SIMD指令生成:针对批量char复制,自动生成AVX/SSE这类SIMD指令,一次性处理多个char(比如一次复制8个char,对应16字节),效率提升明显
  • 边界检查消除:JIT能推断出循环索引始终在合法范围内,dest的剩余空间足够,从而去掉每次charAt和putChar的边界检查

如果是DirectByteBuffer,优化程度会稍弱(因为涉及直接内存访问),但依然会做循环展开和部分向量优化。

更高效的实现方式

1. 用CharBuffer批量写入(简洁高效)

直接借助ByteBuffer的asCharBuffer()方法,调用标准库的put(String),代码更简洁,且JIT对标准库方法的优化更成熟:

private static void strcpy(ByteBuffer dest, String src) {
    dest.asCharBuffer().put(src);
}

这个方法底层也是循环写入,但标准库代码经过长期打磨,JIT能更高效地生成最优汇编。

2. 基于Unsafe的直接内存复制(极致性能,零GC)

通过Unsafe直接操作String的底层数组和ByteBuffer的内存,避免逐个char的方法调用开销,完全无GC:

Java 8及以前(String底层为char[])

private static final Unsafe UNSAFE = getUnsafe();
private static final long STRING_VALUE_OFFSET = getStringValueOffset();

private static void strcpyUnsafe(ByteBuffer dest, String src) {
    char[] srcChars = (char[]) UNSAFE.getObject(src, STRING_VALUE_OFFSET);
    int len = srcChars.length;
    int destPos = dest.position();

    if (dest instanceof DirectByteBuffer) {
        long destAddr = ((DirectByteBuffer) dest).address() + destPos;
        UNSAFE.copyMemory(srcChars, Unsafe.ARRAY_CHAR_BASE_OFFSET,
                          null, destAddr, len << 1); // 每个char占2字节
    } else {
        byte[] destBytes = ((HeapByteBuffer) dest).hb;
        int destArrayPos = destPos + ((HeapByteBuffer) dest).offset;
        // 根据ByteBuffer的字节顺序处理
        if (dest.order() == ByteOrder.BIG_ENDIAN) {
            for (int i = 0; i < len; i++) {
                char c = srcChars[i];
                destBytes[destArrayPos++] = (byte) (c >> 8);
                destBytes[destArrayPos++] = (byte) c;
            }
        } else {
            for (int i = 0; i < len; i++) {
                char c = srcChars[i];
                destBytes[destArrayPos++] = (byte) c;
                destBytes[destArrayPos++] = (byte) (c >> 8);
            }
        }
    }
    dest.position(destPos + len << 1);
}

// 辅助方法:获取Unsafe实例
private static Unsafe getUnsafe() {
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

// 辅助方法:获取String.value字段的偏移量
private static long getStringValueOffset() {
    try {
        Field field = String.class.getDeclaredField("value");
        return UNSAFE.objectFieldOffset(field);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Java 9+(String底层为byte[],需处理编码)

Java 9后String用byte数组存储,编码为LATIN1或UTF16,需要先判断编码再复制:

private static final long STRING_CODER_OFFSET = getStringCoderOffset();
private static final byte LATIN1 = 0;
private static final byte UTF16 = 1;

private static void strcpyUnsafeJava9(ByteBuffer dest, String src) {
    byte coder = UNSAFE.getByte(src, STRING_CODER_OFFSET);
    byte[] srcBytes = (byte[]) UNSAFE.getObject(src, STRING_VALUE_OFFSET);
    int len = src.length();
    int destPos = dest.position();

    if (dest instanceof DirectByteBuffer) {
        long destAddr = ((DirectByteBuffer) dest).address() + destPos;
        if (coder == UTF16) {
            // 直接复制UTF16字节数组
            UNSAFE.copyMemory(srcBytes, Unsafe.ARRAY_BYTE_BASE_OFFSET,
                              null, destAddr, len << 1);
        } else {
            // LATIN1转UTF16,每个字节扩展为两字节
            if (dest.order() == ByteOrder.BIG_ENDIAN) {
                for (int i = 0; i < len; i++) {
                    UNSAFE.putByte(destAddr++, (byte) 0);
                    UNSAFE.putByte(destAddr++, srcBytes[i]);
                }
            } else {
                for (int i = 0; i < len; i++) {
                    UNSAFE.putByte(destAddr++, srcBytes[i]);
                    UNSAFE.putByte(destAddr++, (byte) 0);
                }
            }
        }
    } else {
        byte[] destBytes = ((HeapByteBuffer) dest).hb;
        int destArrayPos = destPos + ((HeapByteBuffer) dest).offset;
        if (coder == UTF16) {
            System.arraycopy(srcBytes, 0, destBytes, destArrayPos, len << 1);
        } else {
            if (dest.order() == ByteOrder.BIG_ENDIAN) {
                for (int i = 0; i < len; i++) {
                    destBytes[destArrayPos++] = (byte) 0;
                    destBytes[destArrayPos++] = srcBytes[i];
                }
            } else {
                for (int i = 0; i < len; i++) {
                    destBytes[destArrayPos++] = srcBytes[i];
                    destBytes[destArrayPos++] = (byte) 0;
                }
            }
        }
    }
    dest.position(destPos + len << 1);
}

// 辅助方法:获取String.coder字段的偏移量
private static long getStringCoderOffset() {
    try {
        Field field = String.class.getDeclaredField("coder");
        return UNSAFE.objectFieldOffset(field);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

⚠️ 注意:Unsafe绕过了Java安全机制,不同JVM版本的字段偏移可能变化,需要做好版本兼容。

3. 字节顺序的重要性

无论用哪种实现,都要确保ByteBuffer的字节顺序(order())符合需求——UTF-16分大端(BE)和小端(LE),默认是大端。如果需要和系统原生字节顺序一致,可调用dest.order(ByteOrder.nativeOrder())。

内容的提问来源于stack exchange,提问作者pippo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 19:07:02