是否有一个java.lang.String的内存有效的替代?
在阅读了这篇测量几种对象types的内存消耗的旧文章之后,我很惊讶地看到在Java中使用了多less内存String
:
length: 0, {class java.lang.String} size = 40 bytes length: 7, {class java.lang.String} size = 56 bytes
虽然文章有一些技巧,以尽量减less这一点,我没有发现他们完全满意。 使用char[]
存储数据似乎是浪费。 大多数西方语言的明显改进是使用byte[]
和UTF-8编码,因为只需要一个字节来存储最常见的字符,而不是两个字节。
当然可以使用String.getBytes("UTF-8")
和new String(bytes, "UTF-8")
。 即使String实例本身的开销也不见了。 但是,那么你失去了非常方便的方法,如equals()
, hashCode()
, length()
,…
据我所知,Sun在Strings的byte[]
表示方面拥有专利 。
用于在Java编程环境中高效地表示string对象的框架
…这些技术可以实现创buildJavastring对象为适当的单字节字符数组…
但是我没有find该专利的API。
为什么我在乎
在大多数情况下,我不这样做。 但是我使用了大量caching的应用程序,包含大量的string,这些string可以更有效地使用内存。
有人知道这样的API吗? 还是有另一种方法来保持您的内存占用string很小,即使在CPU性能或更丑陋的API的代价?
请不要重复上述文章的build议:
- 自己的
String.intern()
的变体(可能与SoftReferences
) - 存储一个
char[]
并利用当前的String.subString(.)
实现来避免数据复制(讨厌的)
更新
我运行了Sun当前JVM(1.6.0_10)的文章中的代码。 它取得了与2002年相同的结果。
从JVM的一点点帮助…
警告:此解决scheme现在已在更新的Java SE版本中过时。 请参阅下面的其他特别解决scheme。
如果您使用HotSpot JVM,则自Java 6 update 21以来,可以使用以下命令行选项:
-XX:+UseCompressedStrings
“ JVM选项”页面显示如下:
对string使用byte []可以表示为纯ASCII。 (在Java 6 Update 21性能版本中引入)
更新 :这个function在后来的版本中被打破了,应该在6u25 b03发行说明中提到的Java SE 6u25中再次修复(但是我们没有在6u25最终发布说明中看到它)。 出于安全原因, 错误报告7016213不可见。 所以,请小心使用并先检查。 像任何-XX
选项一样,它被认为是实验性的,并且可以在没有多less注意的情况下进行更改,所以在生产服务器的启动脚本中使用它可能并不总是最好的。
更新2013-03 (感谢Aleksey Maximus的评论) :看到这个相关的问题 及其接受的答案 。 现在的select似乎已经死亡。 这在bug 7129417报告中得到进一步证实。
最终certificate了手段
警告:(丑)解决scheme的具体需求
这是一个开箱即用的低层次,但是因为你问了…不要打信使!
你自己打火机的string表示
如果ASCII码适合您的需要,那么您为什么不推出自己的实施?
正如你所提到的,你可以在内部byte[]
而不是char[]
。 但是,这不是全部。
要做到这一点更轻量级,而不是将你的字节数组包装在一个类中,为什么不简单地使用一个辅助类,其中大部分是静态方法,这些方法在你传递的字节数组上运行? 当然,它会感觉到C-ISH,但是它会工作,并且会为您节省String
对象带来的巨大开销。
当然,它会错过一些很好的function,除非你重新实现它们。 如果你真的需要他们,那么没有多lessselect。 感谢OpenJDK和许多其他好的项目,你可以很好地推出你自己的只是在byte[]
参数上操作的复杂的LiteStrings
类。 每当你需要调用一个函数,你都会觉得自己要洗澡,但是你将会节省大量的内存。
我build议使它类似于String
类的契约,并提供有意义的适配器和构build器来从String
转换,并且您可能还需要有来自和来自StringBuffer
和StringBuilder
适配器,以及其他一些其他的镜像实现你可能需要的东西。 绝对是一些工作,但可能是值得的(看下面的“做它计数”一节)。
即时压缩/解压缩
你可以很好地压缩你的内存中的string,并在需要时dynamic地解压缩它们。 毕竟,你只需要能够读取它们,当你访问它们,对吧?
当然,暴力就是说:
- 更复杂(因此更less维护)的代码,
- 更多的处理能力,
- 需要相对较长的string才能使压缩相关(或者通过实现自己的存储系统将多个string压缩为一个string,以使压缩更有效)。
做两个
对于一个头痛的问题,当然你可以做所有的事情:
- C-ish帮手类,
- 字节数组,
- 即时压缩商店。
一定要使这个开源。 🙂
做它计数!
顺便说一下,看看这个关于构build内存高效的Java应用程序的精彩演讲作者: N. Mitchell和G. Sevitsky:[ 2008版 ],[ 2009版 ]。
从这个演示中,我们看到一个8字符的string在一个32位系统上消耗了64个字节 (对于一个64位系统而言是96 个字节 !!),其中大部分是由于JVM的开销。 从这篇文章中我们可以看到,一个8字节的数组只能“吃”24个字节 :12个字节的头,8个1字节+ 4个字节的alignment)。
听起来这样可能是值得的,如果你真的操纵了很多东西(可能会加速一些事情,因为你花的时间less,分配内存,但不要引用我的基准,再加上它会很大程度上取决于你的实现)。
在兵马俑,我们有一些情况,我们压缩大的string,因为它们被发送到networking上,实际上保持压缩,直到解压缩是必要的。 我们通过将char []转换为byte []来压缩字节[],然后将该字节[]编码回原来的char []。 对于哈希和长度等特定操作,我们可以在不解码压缩string的情况下回答这些问题。 对于像大XMLstring这样的数据,您可以通过这种方式获得实质的压缩
在networking上移动压缩的数据是一个肯定的胜利。 保持压缩取决于用例。 当然,我们有一些旋钮来closures它,并改变压缩打开的长度等。
这一切都是通过java.lang.String上的字节代码实现的,我们发现它是非常微妙的,因为在启动时如何使用早期的String,但是如果遵循一些指导原则,它是稳定的。
文章指出了两点:
- 字符数组以8个字节为单位增加。
- char []和String对象之间的大小差别很大。
开销是由于包含一个char []对象引用,以及三个int:一个偏移量,一个长度和存储String的哈希码的空间,再加上作为一个对象的标准开销。
与String.intern()稍有不同,或者String.substring()使用的字符数组对所有string都使用单个字符[],这意味着您不需要将对象引用存储在包装类string对象中。 你仍然需要偏移量,并且你可以总共引入多less个字符(大)。
如果使用string标记的特殊末尾,则不再需要长度。 这样可以节省四个字节的长度,但是会花费两个字节的时间,再加上额外的时间,复杂度和缓冲区溢出风险。
如果不经常需要,那么不存储散列的时空权衡可以帮助你。
对于我曾经使用过的应用程序来说,我需要对大量string进行超快速和高效的内存处理,所以我能够将数据保留为其编码forms,并使用字节数组。 我的输出编码与我的input编码相同,我不需要将字节解码为字符,也不需要再次编码回字节输出。
另外,我可以将input数据保留在最初读入的字节数组中 – 一个内存映射文件。
我的对象由一个int偏移量(适合我的情况的限制),一个int长度和一个int哈希码组成。
java.lang.String是我想做的事情的熟悉锤子,但不是工作的最佳工具。
我认为你应该谨慎从2002年的一篇javaworld.com文章中提出任何想法和/或假设。自那以后的六年中,编译器和JVM已经发生了许多变化。 至less,首先针对现代JVMtesting您的假设和解决scheme,以确保解决scheme是值得的。
内部的UTF-8编码有其优势(比如你指出的内存占用less),但也有缺点。
例如,确定UTF-8编码string的字符长度(而不是字节长度)是O(n)操作。 在一个javastring中,确定字符长度的代价是O(1),而生成UTF-8表示的代价是O(n)。
这是关于优先事项。
数据结构devise经常被看作是速度与空间的折衷。 在这种情况下,我认为JavastringAPI的devise者根据这些标准做出了select:
-
String类必须支持所有可能的unicode字符。
-
尽pipeunicode定义了1个字节,2个字节和4个字节的变体,但4字节字符(实际上)是非常罕见的,所以可以将它们表示为代理对。 这就是为什么java使用一个2字节的char原语。
-
当人们调用length(),indexOf()和charAt()方法时,他们对字符位置感兴趣,而不是字节位置。 为了创build这些方法的快速实现,有必要避免内部的UTF-8编码。
-
像C ++这样的语言通过定义三种不同的字符types,迫使程序员在它们之间进行select,使程序员的生活变得更加复杂。 大多数程序员开始使用简单的ASCIIstring,但是当他们最终需要支持国际字符时,修改代码以使用多字节字符的过程是非常痛苦的。 我认为Javadevise者通过说所有string由2字节字符组成是一个很好的折中select。
用gzip压缩它们。 :)只是在开玩笑…但是我看到了一些陌生的东西,它会给你很多较小的CPU资源。
我唯一知道的其他string实现是Javolution类中的实现。 但我不认为它们更有效率,
http://www.javolution.com/api/javolution/text/Text.html
http://www.javolution.com/api/javolution/text/TextBuilder.html
Javaselect了UTF-16来降低速度和存储容量。 处理UTF-8数据比处理UTF-16数据的PITA要多得多(例如,当试图在字节数组中寻找字符X的位置时,你如何以快速的方式来处理,如果每个字符都可以有一个,两个,三个甚至六个字节?曾经考虑过这个问题吗?字节逐字节地移动并不是很快,你看?)。 当然,UTF-32最容易处理,但浪费了两倍的存储空间。 自Unicode早期以来,事情已经发生了变化。 现在,即使使用UTF-16,某些字符也需要4个字节。 正确处理UTF-16几乎与UTF-8一样差。
无论如何,请放心,如果您使用UTF-8的内部存储实现了一个String类,您可能会赢得一些内存,但是对于许多string方法,您将失去处理速度。 你的观点也是一个太有限的观点。 你的论点对于日本人来说并不适用,因为UTF-8中的日文字符不会小于UTF-16(实际上UTF-8中的字节数是3个字节,而UTF-16中只有2个字节) 。 我不明白为什么像今天这样全球化的互联网世界中的程序员仍然在谈论“西方语言”,就好像这是所有的东西一样,仿佛只有西方世界有电脑,其余的都在洞穴。 任何应用程序迟早会因无法有效处理非西方字符而被咬。
有创build一个对象(至less一个调度表)的开销,它使用每个字母2个字节的事实的开销,以及创build一些额外的variables的开销,实际上提高了速度和内存使用很多情况下。
如果你打算使用OO编程,这是代码清晰,可用,可维护的代价。
除了显而易见的(也就是说,如果内存使用是重要的,你应该使用C),你可以用BCD字节数组实现你自己的string。
这实际上听起来很有趣,我可能只是为了踢:)
Java数组每个项目需要2个字节。 BCD编码的数字每个字母IIRC需要6位,使您的string显着变小。 会有一点转换成本,但不是真的太糟糕了。 真正的大问题是,你必须转换为string做任何事情。
你仍然有一个对象实例的开销,担心…但通过修改你的devise,比试图消除实例更好。
最后一个笔记。 我完全反对这样的部署,除非你有三件事情:
- 一个实现以最可读的方式完成
- testing结果和要求显示如何实现不符合要求
- 关于“改进”实现如何满足要求的testing结果。
如果没有这三个,我会踢任何开发人员给我的优化解决scheme。
我目前正在实现一个压缩方法如下(我正在处理一个应用程序,需要在内存中存储大量的文件,所以我们可以做文档到文档计算):
- 将string拆分为4个字符的“单词”(如果您需要全部Unicode),并将这些字节存储在
long
掩码/位移位中。 如果你不需要完整的Unicode字符集,只需要255个ASCII字符,那么每个字符就可以容纳8个字符。 在string的末尾添加(char) 0
,直到长度均匀地除以4(或8)。 - 重写一个散列集的实现(比如Trove的
TLongHashSet
)并且把每个“单词”添加到这个集合中,编译一个内部索引的数组,最后在这个集合中结束(当集合重新集合的时候, - 使用二维
int
数组来存储这些索引(所以第一个维度是每个压缩的string,第二个维度是散列集中的每个“单词”索引),并将单个int
索引返回到该数组中,返回给调用者(你必须拥有单词数组,所以你可以像上面提到的那样,在一个rehash上全局地更新索引)
优点:
- 定时压缩/解压
- 长度为n的string被表示为长度为n / 4的
int
数组,long
字集的额外开销随着越来越less的唯一“单词”渐近地增长 - 用户传回一个简单小的
int
string“ID”来存储它们的对象
Distadvantages:
- 因为它涉及到位移,哈希集合的内部错误等等( 比尔K不会赞成)
- 当你不希望有很多重复的string时,效果很好。 检查库中是否存在string是非常昂贵的。
今天(2010年),您添加到服务器的每GB成本大约为80或120美元。 在你重新devisestring之前,你应该问自己这是否真的值得。
如果你要保存一个GB的内存,也许。 十GB,确定无疑。 如果你想节省10MB的话,你可能会花费更多的时间。
你如何压缩string真的取决于你的使用模式。 有很多重复的string? (使用对象池)是否有很多长string? (使用压缩/编码)
你可能想要更小的string的另一个原因是减less高速caching的使用 即使是最大的CPU也有大约8MB-12MB的caching。 这可能是一个更宝贵的资源,不容易增加。 在这种情况下,我build议你看看string的替代scheme,但是你必须记住,它会花费多less时间,而不是花费多less时间。
UseCompressedStrings编译器选项看起来是最简单的路线。 如果你只使用string进行存储,而没有执行任何equals / substring / split操作,那么像这样的CompactCharSequence类可以工作:
http://www.javamex.com/tutorials/memory/ascii_charsequence.shtml
出于好奇,是几个字节保存真的值得吗?
通常情况下,我build议为了性能的原因而对string进行开槽,以支持StringBuffer(请记住,string是不可变的)。
你是否认真地从string引用中耗尽你的堆?
我相信,一段时间以来,string的内存密集程度已经降低了,因为Java工程师已经实现了轻量级的devise模式来尽可能地共享。 实际上,与我记忆中同一个对象具有相同价值的string我相信。
你说不要重复这个文章中关于自己实习计划的build议,但是String.intern
本身有什么问题呢? 文章包含以下一次性评论:
存在许多原因来避免String.intern()方法。 一个是很less有现代的JVM可以实习大量的数据。
但是即使2002年的内存使用数据仍然持续了六年之后,如果没有取得进展的数据JVM可以实习,我会感到惊讶。
这不是一个纯粹的反问题 – 我很想知道是否有足够的理由来避免这个问题。 对于高度multithreading的使用,它效率低下吗? 它是否填满了堆的特定JVM特定区域? 你真的有几百兆字节的独特的string(所以实习会无用吗)?
请记住,有很多types的压缩。 使用哈夫曼编码是一个很好的通用目的 – 但它是相对CPU密集型的。 对于几年前我研究过的B +树的实现,我们知道这些键可能具有共同的主键字符,所以我们在B +树中为每个页面实现了一个主要的字符压缩algorithm。 代码很简单,速度非常快,导致内存使用量是我们开始的1/3。 在我们的例子中,这样做的真正原因是为了节省磁盘空间,并减less在磁盘上花费的时间 – > RAM传输(节省1/3的磁盘在有效的磁盘性能上有很大的不同)。
我提出这个问题的原因是一个自定义的String实现在这里没有什么帮助。 我们只能够实现我们所做的收益,因为我们处理了string所在的容器层。
试图优化几个字节在这里和那里的String对象可能不值得比较。