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
相关产品推荐
相关产品推荐

