【问题标题】:Use Java unsafe to point char array to a memory location使用 Java unsafe 将 char 数组指向内存位置
【发布时间】:2018-10-13 03:39:33
【问题描述】:

对 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 数据库缓存条目留出更多空间。

【问题讨论】:

  • 如果性能很关键,为什么不实现一个 CharSequence 来满足您的需求?字符串只是一种特殊类型的 CharSequence,它强调的是不变性和封装性,而不是性能。您的 CharSequence 实现可能会考虑其他优先级,例如接受 byte[] 并且不执行复制或额外的内存分配。 docs.oracle.com/javase/7/docs/api/java/lang/CharSequence.html
  • 我可以,但这需要重构我们已经拥有的一堆代码,(equals 必须替换为 compareTo 等)。
  • 一些澄清,我对使用 CharSequence 犹豫不决,因为编译器不会通过用 CharSequence 替换 String 实例来帮助发现潜在的代码破坏更改。然而,这是一个有效的选择,我会考虑的。但是,保留 String 将是理想的。

标签: java unsafe-pointers


【解决方案1】:

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

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

呃……没有。

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

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

如果您不熟悉它,值得阅读Wikipedia article on UTF-8,以便您了解如何文本是如何编码的。


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

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

  • 1234563但有效地执行此操作会成为一个问题,尤其是将 charAt 方法实现为 O(1) 方法。
  • 如果您只是想保存和/或比较(整个)文本,这可以通过将它们表示为byte[] 对象来完成。这些操作可以直接对 UTF-8 编码的数据进行。

  • 1234563 /li>

【讨论】:

  • 抱歉没有说清楚,但我不会在将字符串存储在数据库中时将它们编码为 UTF-8。我将保留 UTF-16 编码,但我不知道我是要从 Protobuf 切换还是编写自定义序列化程序。我知道存在字节序之类的问题,但应用程序不与其他服务通信,所以它不应该受此影响。
  • 您使用 protobuf 将文本作为 UTF-16 发送。但是如果要优化到这个程度,则需要查看整个应用程序(客户端、服务、数据库等)。
  • 我已经查看并修复了应用程序中的各种瓶颈和性能障碍代码。解码 UTF-8 是我要解决的下一个瓶颈。
  • 我的意思是你需要从整体上看待这个问题,而不是零碎的。如果您更改为传输 UTF-16,您很可能会将消息中的文本大小加倍,并增加客户端和服务器端的 protobuf 编组/解组成本。您还需要更改所有客户端。这不是一个简单的改变。
  • 有一个客户。我将对此进行基准测试。如果事实证明解码 UTF-8 比复制一些内存更快,我会放弃它。
猜你喜欢
  • 1970-01-01
  • 2016-07-18
  • 1970-01-01
  • 1970-01-01
  • 2021-03-30
  • 2021-01-26
  • 1970-01-01
  • 2014-08-17
  • 2011-02-23
相关资源
最近更新 更多