在Java中零垃圾大字符串反序列化,巨型对象问题

9
我正在寻找一种在Java中从byte[]反序列化String且产生的垃圾最少的方法。由于我正在创建自己的序列化器和反序列化器,因此我完全可以在服务器端(即在序列化数据时)和客户端(即在反序列化数据时)实现任何解决方案。
我已经成功地通过迭代String的字符(String.charAt(i))并将每个char(16位值)转换为2个8位值来高效地序列化String而不会产生任何垃圾开销。关于这个问题有一个很好的辩论here。另一种选择是使用反射直接访问String的底层char[],但这超出了问题的范围。
然而,在没有创建char[]两次的情况下,似乎不可能对byte[]进行反序列化,这似乎有点奇怪。
步骤如下:
  1. 创建 char[]
  2. 遍历 byte[] 并填充 char[]
  3. 使用 String(char[]) 构造函数创建字符串

由于 Java 的 String 不可变规则,使用构造函数会复制 char[],从而导致 2 倍的 GC 开销。我可以使用机制来规避这个问题(Unsafe String 分配 + 反射设置 char[] 实例),但我只是想问一下,除了违反 String 不可变性的约定之外,还有什么后果吗?

当然,对此最明智的回应是:“来吧,停止这样做,并相信GC,原始的char[]将非常短暂,G1会立即摆脱它”,如果char[]小于G1区域大小的1/2,这是有道理的。如果它更大,则char[]将直接分配为巨型对象(即自动传播到G1区域之外)。这些对象在G1中极难有效地进行垃圾回收。这就是为什么每个分配都很重要的原因。
有什么解决这个问题的想法吗?
非常感谢。

你有没有考虑过不使用字符串,而是直接序列化原始字节数据,并在绝对必要时对子段进行字符集转换? - the8472
我有一个想法。我的想法是创建一个新的类MutableString,并在其上实现许多传统上会产生大量垃圾的操作(例如快速路径的String分割),然后有一个方法toString(from, to),它创建了一个类型为String的“视图”实例。我可以这样做。但这将需要完全重构我们的应用程序,并尽可能地使用MutableString。这是一个好主意,但我想先探索其他选择。 - SergioTCG
1
你知道这些东西已经存在吗?有 CharBufferStringBuilder,它们都是可变的 String 类型(除非你创建了一个不可变的视图),它们都有创建轻量级子序列的方法,并且它们都实现了 CharSequence,这个接口上的正则表达式包实际上实现了 split 操作。虽然在查看源代码时,转换 StringCharBufferStringBuilder 之间似乎会一直复制字符内容,但 HotSpot 对它们进行了特殊优化... - Holger
3个回答

4

这些对象在G1中非常难以有效地进行垃圾回收。

这个说法可能已经不再正确了,但您需要为自己的应用程序进行评估。JDK Bugs 80279598048179 引入了新的机制来收集庞大的、短暂的对象。根据bug标志,您可能需要运行jdk版本≥8u40和≥8u60才能获得它们各自的好处。

感兴趣的实验选项:

-XX:+G1ReclaimDeadHumongousObjectsAtYoungGC

追踪:

-XX:+G1TraceReclaimDeadHumongousObjectsAtYoungGC

如果您需要进一步了解有关这些功能的建议和问题,我建议您访问hotspot-gc-use邮件列表。


谢谢,我会看一下。 - SergioTCG

1
我找到了一个解决方案,但是如果您处于非托管环境中,则此方案无效。 java.lang.String类有一个包私有构造函数String(char[] value, boolean share)
来源:
/*
* Package private constructor which shares value array for speed.
* this constructor is always expected to be called with share==true.
* a separate constructor is needed because we already have a public
* String(char[]) constructor that makes a copy of the given char[].
*/
String(char[] value, boolean share) {
    // assert share : "unshared not supported";
    this.value = value;
}

这在Java中被广泛使用,例如在Integer.toString()Long.toString()String.concat(String)String.replace(char, char)String.valueOf(char)等方法中。
解决方案(或者叫做hack)是将该类移动到java.lang包中,并访问该包私有的构造函数。这可能会与安全管理器不兼容,但可以规避此问题。

5
你可以通过反射访问构造函数,然后构建一个方法句柄/lambda表达式来避免调用开销,而无需将一个类移入包中。 - the8472

0

找到了一个可行的解决方案,使用简单的“秘密”本地 Java 库:

String longString = StringUtils.repeat("bla", 1000000);
char[] longArray = longString.toCharArray();
String fastCopiedString = SharedSecrets.getJavaLangAccess().newStringUnsafe(longArray);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接