Clark To Do The blog of Clark

JAVA 6, 7, 8中的字符串池

字符串池

字符串池用于存储独立共享的String对象,此对象将代替其他等值的String对象. 你也可以通过维护一个自定义的Map<String, String>(可以根据需求,通过软引用或弱引用实现)来实现字符串池的功能, 并以Map的value作为标准值. 同时, 你也可以利用JDK提供的String.intern()方法. 由于字符串池操作失去控制时, 非常容易导致OutOfMemoryException, Java 6中的String.intern()已经被很多标准禁用. Oracle Java 7中, 对于字符串池的实现做了很大程度的修改. 详情可以参见: bug_6962931bug_6962930.

Java 6中的String.intern()

在过去, 所有的标准化字符串都被存在内存永久代中(堆内存中一块固定区域, 用于存储已经加载的class信息与字符串池). 除了显式的标准化字符串, 永久代字符串池也包含所有在程序中用到的常量字符串(如果累或方法从未被加载或调用, 任何常量都不会被加载)

Java 6中此种类型的字符串池最大的问题就是在永久代中的定位. 永久代中含有一个固定大小并且无法在运行时扩展的内存区域.你可以通过-XX:MaxPermSize=N来进行设置. 据我所知, 根据运行平台的不同, 默认的永久代的大小在32M至96M之间不等. 你可以增加它的大小, 但是它的大小仍旧是固定的. 因此, 这个限制要求对于String.intern的应用非常谨慎(最好不要在任何不可控的用户输入上应用此方法). 这就是为什么很多场景下, Java 6中的字符串池会通过手动管理的map来进行重新实现.

Java 7中的String.intern()

Oracle的工程师们对于Java 7中的字符串池做了相当显著的改动(字符串池被重新定位到了堆中). 这意味着已经不再被一块独立固定大小的内存区域所限制. 所有的字符串都存储在堆内存中, 同大多数对象一样, 你可以通过设置堆存内大小来实现程序的调优.技术层面上, 这可能是让你重新启用String.intern()的最显著的原因了.不过, 还是有一个其他原因的.

字符串池的垃圾收集

是的, 如果JVM字符串池中的字符串没有了任何引用, 它就具备了被垃圾回收的资格. 这种行为适用与所有版本的Java. 这意味着如果标准化字符串超出了作用域并且没有任何引用, 它就会从JVM字符串池中被垃圾回收.

对于字符串, 具备垃圾回收的资格并存储在堆内存中, JVM字符串池似乎是存放所有字符串的最佳场所. 理论上这是正确的,无用的字符串会被垃圾收集, 已用的字符串会被保存在内存中, 这样你就可以在输入中得到一个等值的字符串. 似乎这是一个完美的存储策略? 基本如此. 在做出任何决定之前, 你必须了解字符串池是如何被实现的.

Java6,7,8中JVM字符串池的实现

字符串池的实现方式是一个固定容量的HashMap, Map中的每个单元包含一个拥有相同哈希码的字符串的list. 具体实现可以参考: bug_6962930

池的大小默认为1009(这是上诉BUG报告中源码中的值, 在Java7u40已被增大). 在早期Java6的版本中, 这个值是一个常量,在Java6u30和Java6u41中实现可以配置化. 在Java7的早期版本中就实现了可配置化(至少Java7u02中是可配置的). 配置通过指定-XX:StringTableSize=N实现, N代表字符串池Map的大小.为了更好的性能, 请保证他是质数.

这个参数在Java6中不会起到什么作用, 因为仍会被永久代的固定大小限制. 进一步的探讨将会不包含Java 6

Java7 (至 Java7u40)

Java7中, 换句话说, 会被更大的堆内存限制. 这意味着可以将字符串池大小设置为一个相当大的值(具体值应参考应用的需求). 作为一个规则, 当内存中数据大小增长到几百兆时, 需要开始考虑内存消耗问题. 在这种情况下, 为字符串池分配可以容纳一百万个元素的8-16兆的空间似乎是一个合理折衷的做法(配置-XX:StringTableSize时, 不要使用1,000,000,因为他不是质数, 建议使用1,000,003).

Java 7u40以上 与 Java 8

字符串池的大小在Java7u40之后被提升至60013(这是一个主要的性能调整). 这个大小可以容纳大约30000个不同的字符串. 对于值得标准化的字符串来说, 这是一个显著的提升. 你可以通过设置XX:+PrintFlagsFinal来获取这个值.

comments powered by Disqus