黑马程序员技术交流社区

标题: 提高 Java 代码性能的各种技巧 [打印本页]

作者: Liveei    时间: 2015-8-26 22:21
标题: 提高 Java 代码性能的各种技巧
本帖最后由 Liveei 于 2015-8-26 22:24 编辑

这篇文章将要讨论 Java 6 中是如何实现 String.intern 方法的,以及这个方法在 Java 7 以及 Java 8 中做了哪些调整。

字符串池
字符串池(有名字符串标准化)是通过使用唯一的共享 String 对象来使用相同的值不同的地址表示字符串的过程。你可以使用自己定义的 Map<String, String> (根据需要使用 weak 引用或者 soft 引用)并使用 map 中的值作为标准值来实现这个目标,或者你也可以使用 JDK 提供的 String.intern()。
很多标准禁止在 Java 6 中使用 String.intern() 因为如果频繁使用池会市区控制,有很大的几率触发 OutOfMemoryException。Oracle Java 7 对字符串池做了很多改进,你可以通过以下地址进行了解 http://bugs.sun.com/view_bug.do?bug_id=6962931以及 http://bugs.sun.com/view_bug.do?bug_id=6962930

Java 6 中的 String.intern()
在美好的过去所有共享的 String 对象都存储在 PermGen 中 — 堆中固定大小的部分主要用于存储加载的类对象和字符串池。除了明确的共享字符串,PermGen 字符串池还包含所有程序中使用过的字符串(这里要注意是使用过的字符串,如果类或者方法从未加载或者被条用,在其中定义的任何常量都不会被加载)
Java 6 中字符串池的最大问题是它的位置 — PermGen。PermGen 的大小是固定的并且在运行时是无法扩展的。你可以使用 -XX:MaxPermSize=N 配置来调整它的大小。据我了解,对于不同的平台默认的 PermGen 大小在 32M 到 96M 之间。你可以扩展它的大小,不过大小使用都是固定的。这个限制需要你在使用 String.intern 时需要非常小心 — 你最好不要使用这个方法 intern 任何无法控制的用户输入。这是为什么在 JAVA6 中大部分使用手动管理 Map 来实现字符串池

Java 7 中的 String.intern()
Java 7 中 Oracle 的工程师对字符串池的逻辑做了很大的改变 — 字符串池的位置被调整到 heap 中了。这意味着你再也不会被固定的内存空间限制了。所有的字符串都保存在堆(heap)中同其他普通对象一样,这使得你在调优应用时仅需要调整堆大小。这 个改动使得我们有足够的理由让我们重新考虑在 Java 7 中使用 String.intern()。

字符串池中的数据会被垃圾收集
没错,在 JVM 字符串池中的所有字符串会被垃圾收集,如果这些值在应用中没有任何引用。这是用于所有版本的 Java,这意味着如果 interned 的字符串在作用域外并且没有任何引用 — 它将会从 JVM 的字符串池中被垃圾收集掉。
因为被重新定位到堆中以及会被垃圾收集,JVM 的字符串池看上去是存放字符串的合适位置,是吗?理论上是 — 违背使用的字符串会从池中收集掉,当外部输入一个字符传且池中存在时可以节省内存。看起来是一个完美的节省内存的策略?在你回答这个之前,可以肯定的是你 需要知道字符串池是如何实现的。

在 Java 6,7,8 中 JVM 字符串池的实现
字符串池是使用一个拥有固定容量的 HashMap 每个元素包含具有相同 hash 值的字符串列表。一些实现的细节可以从 Java bug 报告中获得 http://bugs.sun.com/view_bug.do?bug_id=6962930
默认的池大小是 1009 (出现在上面提及的 bug 报告的源码中,在 Java7u40 中增加了)。在 JAVA 6 早期版本中是一个常量,在随后的 java6u30 至 java6u41 中调整为可配置的。而在java 7中一开始就是可以配置的(至少在java7u02中是可以配置的)。你需要指定参数 -XX:StringTableSize=N, N 是字符串池 Map 的大小。确保它是为性能调优而预先准备的大小。
在 Java 6 中这个参数没有太多帮助,因为你仍任被限制在固定的 PermGen 内存大小中。后续的讨论将直接忽略 Java 6

Java 7 (直至 Java7u40)
在 Java7 中,换句话说,你被限制在一个更大的堆内存中。这意味着你可以预先设置好 String 池的大小(这个值取决于你的应用程序需求)。通常说来,一旦程序开始内存消耗,内存都是成百兆的增长,在这种情况下,给一个拥有 100 万字符串对象的字符串池分配 8-16M 的内存看起来是比较适合的(不要使用1,000,000 作为 -XX:StringTaleSize 的值 – 它不是质数;使用 1,000,003代替)
你可能期待关于 String 在 Map 中的分配 — 可以阅读我之前关于 HashCode 方法调优的经验。
你必须设置一个更大的 -XX:StringTalbeSize 值(相比较默认的 1009 ),如果你希望更多的使用 String.intern() — 否则这个方法将很快递减到 0 (池大小)。
我没有注意到在 intern 小于 100 字符的字符串时的依赖情况(我认为在一个包含 50 个重复字符的字符串与现实数据并不相似,因此 100 个字符看上去是一个很好的测试限制)

总结
由于 Java 6 中使用固定的内存大小(PermGen)因此不要使用 String.intern() 方法
Java7 和 8 在堆内存中实现字符串池。这以为这字符串池的内存限制等于应用程序的内存限制。
在 Java 7 和 8 中使用 -XX:StringTableSize 来设置字符串池 Map 的大小。它是固定的,因为它使用 HashMap 实现。近似于你应用单独的字符串个数(你希望保留的)并且设置池的大小为最接近的质数并乘以 2 (减少碰撞的可能性)。它是的 String.intern 可以使用相同(固定)的时间并且在每次插入时消耗更小的内存(同样的任务,使用java WeakHashMap将消耗4-5倍的内存)。
在 Java 6 和 7(Java7u40以前) 中 -XX:StringTableSize 参数的值是 1009。Java7u40 以后这个值调整为 60013 (Java 8 中使用相同的值)
如果你不确定字符串池的用量,参考:-XX:+PrintStringTableStatistics JVM 参数,当你的应用挂掉时它告诉你字符串池的使用量信息。


作者: 没有如果    时间: 2015-8-26 22:34
不错啊,很实用,帮助很大




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2