java - 使用 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 数据库中的条目。
解决方案
我认为您在这里采取了错误的方法。
到目前为止,我已经弄清楚如何使用 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
(或StringBuilder
或char[]
)对象的形式,那么您真的别无选择,只能进行完全转换。尝试其他任何方法,您都可能会搞砸;例如乱码文本和潜在的 JVM 崩溃。如果你想要“类似字符串”的东西,你可以想象实现一个自定义子类
CharSequence
,它将字节包装在消息中并即时解码 UTF-8。但是有效地做到这一点会成为一个问题,尤其是将该charAt
方法作为一种O(1)
方法来实现。如果您只是想保存和/或比较(整个)文本,这可以通过将它们表示为对象或在
byte[]
对象中来完成。这些操作可以直接对 UTF-8 编码的数据执行。如果输入文本实际上可以以具有固定 8 位字符大小(例如 ASCII、Latin-1 等)或 UTF-16 的字符编码发送,那将简化事情。
推荐阅读
- sql - 没有聚合函数的枢轴
- console.log - console.log 请求中有许多等号是什么意思
- python - 在没有 HDF5 压缩过滤器的情况下压缩 HDF5 文件
- ios - 如何以较低的分辨率渲染 SceneKit 着色器?
- ansible - Ansible 剧本:变量
- python - 熊猫连接产生重复的结果和标题 - python
- reactjs - 自定义反应钩子设置输入表单不起作用
- mysql - 如何使用 MySQl 的 ODBC 连接器在 MS Access 中的 VBA 中执行和查询?
- apache-kafka - 支持集群的 Apache Camel 幂等存储库
- python - 同时运行多个函数(线程)