首页 > 解决方案 > 使用 Java unsafe 将 char 数组指向内存位置

问题描述

对 Java 应用程序的一些分析表明,它花费大量时间将 UTF-8 字节数组解码为 String 对象。UTF-8 字节流来自 LMDB 数据库,数据库中的值是 Protobuf 消息,这就是它如此多地解码 UTF-8 的原因。由这个引起的另一个问题是,由于从内存映射解码到 JVM 中的字符串对象,字符串占用了大量内存。

我想重构这个应用程序,这样它就不会在每次从数据库中读取消息时分配一个新的字符串。我希望 String 对象中的底层 char 数组简单地指向内存位置。

package testreflect;

import java.lang.reflect.Field;

import sun.misc.Unsafe;

public class App {
    public static void main(String[] args) throws Exception {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe UNSAFE = (Unsafe) field.get(null);

        char[] sourceChars = new char[] { 'b', 'a', 'r', 0x2018 };

        // Encoding to a byte array; asBytes would be an LMDB entry
        byte[] asBytes = new byte[sourceChars.length * 2];
        UNSAFE.copyMemory(sourceChars, 
                UNSAFE.arrayBaseOffset(sourceChars.getClass()), 
                asBytes, 
                UNSAFE.arrayBaseOffset(asBytes.getClass()), 
                sourceChars.length*(long)UNSAFE.arrayIndexScale(sourceChars.getClass()));

        // Copying the byte array to the char array works, but is there a way to
        // have the char array simply point to the byte array without copying?
        char[] test = new char[sourceChars.length];
        UNSAFE.copyMemory(asBytes, 
                UNSAFE.arrayBaseOffset(asBytes.getClass()), 
                test, 
                UNSAFE.arrayBaseOffset(test.getClass()), 
                asBytes.length*(long)UNSAFE.arrayIndexScale(asBytes.getClass()));

        // Allocate a String object, but set its underlying 
        // byte array manually to avoid the extra memory copy   
        long stringOffset = UNSAFE.objectFieldOffset(String.class.getDeclaredField("value"));
        String stringTest = (String) UNSAFE.allocateInstance(String.class);
        UNSAFE.putObject(stringTest, stringOffset, test);
        System.out.println(stringTest);
    }
}

到目前为止,我已经弄清楚如何使用 Unsafe 包将字节数组复制到 char 数组并在 String 对象中设置底层数组。这应该会减少应用程序浪费在解码 UTF-8 字节上的 CPU 时间。

但是,这并不能解决内存问题。有没有办法让一个 char 数组指向一个内存位置并完全避免内存分配?完全避免复制将减少 JVM 为这些字符串进行的不必要分配的数量,从而为操作系统留出更多空间来缓存 LMDB 数据库中的条目。

标签: javaunsafe-pointers

解决方案


我认为您在这里采取了错误的方法。

到目前为止,我已经弄清楚如何使用 Unsafe 包将字节数组复制到 char 数组并在 String 对象中设置底层数组。这应该会减少应用程序浪费在解码 UTF-8 字节上的 CPU 时间。

呃……不。

使用内存复制从一个byte[]到复制char[]是行不通的。char目标中的每个char[]实际上将包含原始的 2 个字节。如果您尝试将其包装char[]为 a String,您将得到一种奇怪的mojibake

真正的 UTF-8String转换是将代表 UTF-8 代码点的 1 到 4 个字节(代码单元)转换为代表 UTF-16 中相同代码点的 1 或 2 个 16 位代码单元。使用普通内存副本无法做到这一点。

如果您不熟悉它,那么值得阅读有关 UTF-8 的 Wikipedia 文章,以便您了解文本的编码方式。


解决方案取决于您打算如何处理文本数据。

  • 如果数据真的必须是String(或StringBuilderchar[])对象的形式,那么您真的别无选择,只能进行完全转换。尝试其他任何方法,您都可能会搞砸;例如乱码文本和潜在的 JVM 崩溃。

  • 如果你想要“类似字符串”的东西,你可以想象实现一个自定义子类CharSequence,它将字节包装在消息中并即时解码 UTF-8。但是有效地做到这一点会成为一个问题,尤其是将该charAt方法作为一种O(1)方法来实现。

  • 如果您只是想保存和/或比较(整个)文本,这可以通过将它们表示为对象或在byte[]对象中来完成。这些操作可以直接对 UTF-8 编码的数据执行。

  • 如果输入文本实际上可以以具有固定 8 位字符大小(例如 ASCII、Latin-1 等)或 UTF-16 的字符编码发送,那将简化事情。


推荐阅读