是否有一个内存有效的java.lang.String替换?

时间:2008-10-23 19:13:37

标签: java string optimization memory performance

在阅读this old article测量几种对象类型的内存消耗后,我惊讶地发现在Java中使用了多少内存String

length: 0, {class java.lang.String} size = 40 bytes
length: 7, {class java.lang.String} size = 56 bytes

虽然文章有一些提示可以最大限度地减少这种情况,但我并没有发现它们完全令人满意。使用char[]存储数据似乎很浪费。对于大多数西方语言来说,显而易见的改进是使用byte[]和类似UTF-8的编码,因为您只需要一个字节来存储最频繁的字符,而不是两个字节。

当然可以使用String.getBytes("UTF-8")new String(bytes, "UTF-8")。甚至String实例本身的开销也会消失。但是你失去了非常方便的方法,比如equals()hashCode()length(),......

据我所知,Sun在byte[]代表字符串上有patentString.intern()

  

在Java编程环境中有效表示字符串对象的框架
  ...可以实现这些技术,以便在适当的时候将Java字符串对象创建为单字节字符数组...

但是我找不到该专利的API。

为什么要关心?
在大多数情况下,我没有。但我使用包含大量字符串的巨大缓存来处理应用程序,这些字符串可以从更有效地使用内存中受益。

有人知道这样的API吗?或者是否有另一种方法可以保持Strings的内存占用空间小,即使以CPU性能或更丑陋的API为代价?

请不要重复上述文章中的建议:

  • 自己的SoftReferences变体(可能带有char[]
  • 存储单个String.subString(.)并利用当前的{{1}}实现来避免数据复制(讨厌)

更新

我从Sun目前的JVM(1.6.0_10)上的文章中运行了代码。它产生了与2002年相同的结果。

15 个答案:

答案 0 :(得分:24)

从JVM获得一点帮助......

警告:此解决方案现已在较新的Java SE版本中过时。请参阅下面的其他临时解决方案。

如果您使用HotSpot JVM,那么从Java 6更新21开始,您可以使用此命令行选项:

-XX:+UseCompressedStrings

JVM Options页面显示:

  

对字符串使用byte [],可以表示为纯ASCII。 (介绍   在Java 6 Update 21性能发布中)

更新:此功能在更高版本中被破坏,并且应该在{SE 3 6}中再次修复,如6u25 b03 release notes所述(但我们在6u25 final release notes)。出于安全原因,bug report 7016213不可见。因此,请务必小心使用并先检查。与任何-XX选项一样,它被认为是实验性的,可能会在没有太多通知的情况下发生变化,因此在生产服务器的启动脚本中不使用它可能并不总是最佳。

更新2013-03 (感谢Aleksey Maximus的评论):请参阅此related questionits accepted answer。现在这个选择似乎已经死了。错误7129417报告中进一步确认了这一点。

结束证明手段

警告:(丑陋)针对特定需求的解决方案

这是一个开箱即用的低级别,但是因为你问...不要打信使!

您自己的打火机字符串表示

如果您需要ASCII就可以了,那么为什么不推出自己的实现呢?

正如您所提到的,您可以在内部byte[]而不是char[]。但这不是全部。

为了做得更轻量级,而不是将字节数组包装在一个类中,为什么不简单地使用一个辅助类,它主要包含在这些字节数组上运行的静态方法?当然,它会感觉很漂亮C-ish,但它会起作用,并且可以节省String个对象带来的巨大开销。

当然,它会遗漏一些不错的功能......除非你重新实现它们。如果你真的需要它们,那么没有太多选择。感谢OpenJDK和许多其他好的项目,你可以很好地推出你自己的LiteStrings类,它只对byte[]参数进行操作。每当你需要召唤一个函数时,你会觉得要洗个澡,但你会节省大量的记忆。

我建议使它与String类的合约非常相似,并提供有意义的适配器和构建器来转换为String,并且您可能希望还有来自StringBuffer的适配器。 {1}}和StringBuilder,以及您可能需要的其他一些镜像实现。绝对是一些工作,但可能是值得的(请参阅“Make it Count!”部分下面的内容。)

即时压缩/解压缩

你可以很好地压缩内存中的字符串,并在需要时动态解压缩它们。毕竟,你只需要能够在访问它们时阅读它们,对吗?

当然,暴力意味着:

  • 更复杂(因此维护性较差)的代码,
  • 更多处理能力,
  • 需要相对较长的字符串才能使压缩相关(或者通过实现自己的存储系统将多个字符串压缩为一个,以使压缩更有效)。

同时执行

对于一个令人头疼的问题,当然你可以做到这一切:

  • C-ish helper class,
  • 字节数组,
  • 即时压缩商店。

一定要开源。 :)

让它计数!

顺便说一句,请参阅N. Mitchell和G. Sevitsky撰写的关于构建内存高效Java应用程序的精彩演示文稿:[2008 version],[{{3} }]。

从这个演示文稿中,我们看到32位系统上的 8-char字符串占用64字节(64位系统为96 !!),其中大部分是由于JVM开销。从这个2009 version我们看到 8字节数组只吃“24”字节:12字节的标头,8 x 1字节+ 4字节的对齐)。

如果你真的操纵了很多东西(并且可能加速了一些事情,因为你花费更少的时间分配内存,但是不要引用我并对其进行基准测试;加上它在很大程度上取决于你的实施。)

答案 1 :(得分:21)

在Terracotta,我们遇到一些情况,我们压缩大字符串,因为它们是在网络上发送的,实际上是压缩它们直到需要解压缩。我们通过将char []转换为byte [],压缩byte [],然后将该byte []编码回原始char []来实现。对于哈希和长度等特定操作,我们可以在不解码压缩字符串的情况下回答这些问题。对于像大XML字符串这样的数据,您可以通过这种方式获得大量压缩。

在网络中移动压缩数据是一个明确的胜利。保持压缩取决于用例。当然,我们有一些旋钮可以关闭它并改变压缩开启的长度等。

这一切都是通过java.lang.String上的字节代码检测完成的,我们发现这是非常精细的,因为在启动时使用了早期的String,但是如果你遵循一些指导则它是稳定的。

答案 2 :(得分:10)

文章指出了两件事:

  1. 字符数组以8个字节的块增加。
  2. char []和String对象之间的大小差异很大。
  3. 开销是由于包含一个char []对象引用和三个int:一个偏移量,一个长度和用于存储String的哈希码的空间,加上简单地作为对象的标准开销。

    与String.intern()略有不同,或者String.substring()使用的字符数组对所有字符串使用单个char [],这意味着您不需要在包装器String中存储对象引用 - 喜欢对象。你仍然需要偏移量,并且你会对你可以拥有多少个字符引入(大)限制。

    如果使用字符串标记的特殊结尾,则不再需要长度。这样可以节省4个字节的长度,但是标记需要花费两个字节,加上额外的时间,复杂性和缓冲区溢出风险。

    如果您不经常需要,那么不存储哈希的时空权衡可能对您有所帮助。

    对于我曾经使用的应用程序,我需要对大量字符串进行超快速和内存有效处理,我能够以编码形式保留数据,并使用字节数组。我的输出编码与我的输入编码相同,我不需要将字节解码为字符,也不需要再次编码回字节输出。

    此外,我可以将输入数据保留在最初读入的字节数组中 - 内存映射文件。

    我的对象包括一个int偏移量(适合我的情况的限制),一个int长度和一个int哈希码。

    java.lang.String是我想要做的熟悉的锤子,但不是最好的工具。

答案 3 :(得分:7)

我认为你应该非常谨慎地从2002年的javaworld.com文章中提出任何想法和/或假设。从那以后的六年里,编译器和JVM发生了很多很多变化。至少,首先针对现代JVM测试您的假设和解决方案,以确保解决方案甚至值得付出努力。

答案 4 :(得分:7)

内部UTF-8编码有其优点(例如您指出的内存占用量较小),但它也有缺点。

例如,确定UTF-8编码字符串的字符长度(而不是字节长度)是O(n)操作。在java字符串中,确定字符长度的成本是O(1),而生成UTF-8表示的是O(n)。

这都是关于优先事项的。

数据结构设计通常被视为速度和空间之间的权衡。在这种情况下,我认为Java字符串API的设计者根据这些标准做出了选择:

  • String类必须支持所有可能的unicode字符。

  • 虽然unicode定义了1字节,2字节和4字节变体,但4字节字符(实际上)非常罕见,因此可以将它们表示为代理对。这就是为什么java使用2字节的char原语。

  • 当人们调用length(),indexOf()和charAt()方法时,他们对字符位置感兴趣,而不是字节位置。为了创建这些方法的快速实现,有必要避免内部UTF-8编码。

  • 像C ++这样的语言通过定义三种不同的字符类型并迫使程序员在它们之间进行选择,使程序员的生活变得更加复杂。大多数程序员开始使用简单的ASCII字符串,但是当他们最终需要支持国际字符时,修改代码以使用多字节字符的过程非常痛苦。我认为Java设计者通过说所有字符串都包含2个字节的字符来做出一个很好的折衷选择。

答案 5 :(得分:2)

使用gzip压缩它们。 :)开个玩笑......但是我看到了陌生的东西,它会以很大的CPU费用给你更小的数据。

我所知道的唯一其他String实现是Javolution类中的那些。不过,我认为它们的内存效率并不高:

http://www.javolution.com/api/javolution/text/Text.html
http://www.javolution.com/api/javolution/text/TextBuilder.html

答案 6 :(得分:2)

Java选择UTF-16来降低速度和存储容量。处理UTF-8数据比处理UTF-16数据要多得多(例如,当试图在字节数组中找到字符X的位置时,如果每个字符都可以有一个,那么如何以快速方式执行此操作,两个,三个甚至多达六个字节?曾经想过这个吗?逐字节地读取字符串并不是很快,你知道吗?)。当然,UTF-32最容易处理,但浪费了两倍的存储空间。自Unicode早期以来,情况发生了变化。现在某些字符需要4个字节,即使使用UTF-16也是如此。正确处理这些使UTF-16几乎与UTF-8一样糟糕。

无论如何,请放心,如果你实现一个带有使用UTF-8的内部存储的String类,你可能会赢得一些内存,但是你会失去许多字符串方法的处理速度。你的论点也是一种过于有限的观点。你的论点不适用于日本的人,因为UTF-8中的日文字符不会小于UTF-16(实际上它们在UTF-8中占用3个字节,而在UTF-16中只占两个字节) 。我不明白为什么像今天这样的全球化世界的程序员无处不在的互联网仍然在谈论“西方语言”,好像这就是所有可以计算的,好像只有西方世界有计算机而其余部分都存在于洞穴。任何应用程序迟早都会被它无法有效处理非西方字符的事实所困扰。

答案 7 :(得分:1)

创建一个对象(至少是一个调度表)的开销,它每个字母使用2个字节这一事实的开销,以及为实际提高速度而创建的一些额外变量的开销在许多情况下使用内存。

如果您打算使用OO编程,那么这就是拥有清晰,可用,可维护代码的成本。

除了显而易见的答案之外(如果内存使用很重要,你可能应该使用C),你可以用BCD字节数组中的内部表示来实现自己的字符串。

这实际上听起来很有趣,我可能只是为了踢球而做:)

Java数组每个项目占用2个字节。 BCD编码的数字每个字母IIRC需要6位,使您的字符串显着缩小。转换成本会有一点时间,但实际上并不算太糟糕。真正的大问题是你必须转换为字符串来做任何事情。

您仍然需要担心对象实例的开销......但通过改进设计而不是尝试消除实例,可以更好地解决这个问题。

最后一个说明。除非你有3件事,否则我完全反对部署这样的东西:

  • 以最易读的方式完成的实施
  • 显示该实施不符合要求的测试结果和要求
  • 关于“改进”实施如何满足要求的测试结果。

如果没有这三个,我会给开发人员提供给我的任何优化解决方案。

答案 8 :(得分:1)

答案 9 :(得分:1)

答案 10 :(得分:1)

UseCompressedStrings编译器选项似乎是最简单的选择。如果您只使用字符串进行存储,而不进行任何equals / substring / split操作,那么像CompactCharSequence类这样的东西可以工作:

http://www.javamex.com/tutorials/memory/ascii_charsequence.shtml

答案 11 :(得分:0)

出于好奇,节省的几个字节真的值得吗?

通常情况下,我建议出于性能原因抛弃字符串,而不是StringBuffer(请记住,字符串是不可变的)。

你是否认真地从字符串引用中耗尽你的堆?

答案 12 :(得分:0)

我认为字符串在一段时间内内存密集程度较低,因为Java工程师已经实现了flyweight设计模式以尽可能多地共享。 事实上,具有相同值的字符串指向内存中的同一个对象。我相信。

答案 13 :(得分:0)

你说不要重复文章关于推行自己的实习计划的建议,但String.intern本身有什么问题?这篇文章包含以下一次性评论:

  

存在许多避免String.intern()方法的原因。一个是现代JVM很少可以实现大量数据。

但是,即使2002年的内存使用数据仍然存在六年之后,如果JVM数据的实际数量没有取得进展,我会感到惊讶。

这不仅仅是一个修辞问题 - 我很想知道是否有充分理由避免它。对于高度多线程的使用,它是否实现效率低下?它是否填满了堆的一些特殊的JVM特定区域?你真的拥有数百兆字节的独特字符串(所以实际上实际上是没用的吗?)

答案 14 :(得分:0)

请记住,有许多类型的压缩。使用霍夫曼编码是一种很好的通用方法 - 但它相对来说是CPU密集型的。对于我在几年前工作的B + Tree实现,我们知道密钥可能具有共同的主要字符,因此我们为B + Tree中的每个页面实现了主要的字符压缩算法。代码很简单,非常非常快,并且内存使用量是我们开始时的1/3。在我们的例子中,这样做的真正原因是为了节省磁盘空间,并减少在磁盘上花费的时间 - > RAM传输(并且1/3节省使有效磁盘性能产生巨大差异)。

我提出这个问题的原因是自定义String实现在这里不会有太大帮助。我们只能实现我们所做的收益,因为我们处理了字符串所在的容器层。

尝试优化String对象内部和那里的几个字节可能不值得比较。