陷阱 - 实用的字符串,以便你可以使用是一个坏主意
当一些程序员看到这个建议:
“使用
==
测试字符串是不正确的(除非字符串被中断)”
他们最初的反应是实习字符串,以便他们可以使用 ==
。 (毕竟 ==
比调用 String.equals(...)
更快,不是吗。)
从许多方面来看,这是错误的方法:
脆弱性
首先,如果你知道你正在测试的所有 String
对象都被实习,你只能安全地使用 ==
。JLS 保证源代码中的字符串文字将被实现。但是,除了 String.intern(String)
本身之外,没有一个标准的 Java SE API 保证返回实习字符串。如果你错过了一个未被实习的 String
对象来源,你的应用程序将不可靠。这种不可靠性将表现为假阴性,而不是容易使其更难检测的异常。
使用’intern()
‘的成本
在引擎盖下,实习通过维护包含先前实习的 String
对象的哈希表来工作。使用某种弱引用机制使得实习哈希表不会成为存储泄漏。虽然哈希表是用本机代码实现的(与 HashMap
,HashTable
等不同),但就所使用的 CPU 和内存而言,intern
调用仍然相对昂贵。
这个成本必须与我们将通过使用 ==
而不是 equals
获得的节省进行比较。实际上,除非将每个实习字符串与其他字符串进行几次比较,否则我们不会收支平衡。
(旁白:少数实习的情况往往是减少应用程序的内存占用,其中相同的字符串重复多次,并且这些字符串具有较长的生命周期。)
对垃圾收集的影响
除了上面描述的直接 CPU 和内存成本之外,实际的字符串也会影响垃圾收集器的性能。
对于 Java 7 之前的 Java 版本,实习字符串保存在不经常收集的 PermGen
空间中。如果需要收集 PermGen,则(通常)会触发完整的垃圾回收。如果 PermGen 空间完全填满,即使常规堆空间中有可用空间,JVM 也会崩溃。
在 Java 7 中,字符串池从 PermGen
移出到普通堆中。但是,哈希表仍然是一个长期存在的数据结构,这将导致任何实习字符串长寿。 (即使实际的字符串对象是在 Eden 空间中分配的,它们很可能在收集之前被提升。)
因此,在所有情况下,实习字符串将延长其相对于普通字符串的生命周期。这将增加 JVM 生命周期内的垃圾收集开销。
第二个问题是哈希表需要使用某种弱引用机制来防止字符串内部泄漏内存。但是这样的机制对垃圾收集器来说更有用。
这些垃圾收集开销难以量化,但毫无疑问它们确实存在。如果你广泛使用 intern
,它们可能很重要。
字符串池哈希表大小
根据这个来源 ,从 Java 6 开始,字符串池被实现为具有链的固定大小的散列表,以处理散列到同一桶的字符串。在 Java 6 的早期版本中,哈希表具有(硬连线)常量大小。调整参数(-XX:StringTableSize
)被添加为 Java 6 的中期更新。然后,在 Java 7 的中期更新中,池的默认大小从 1009
更改为 60013
。
最重要的是,如果你打算在代码中密集使用 intern
,建议选择哈希表大小可调的 Java 版本,并确保适当调整大小。否则,随着池变大,intern
的性能可能会降低。
作为潜在的拒绝服务向量实习
字符串的哈希码算法是众所周知的。如果你是恶意用户或应用程序提供的实习字符串,则可以将其用作拒绝服务(DoS)攻击的一部分。如果恶意代理安排它提供的所有字符串都具有相同的哈希码,这可能导致不平衡的哈希表和 O(N)
性能为 intern
…其中 N
是冲突字符串的数量。
(对服务发起 DoS 攻击有更简单/更有效的方法。但是,如果 DoS 攻击的目标是破坏安全性或转义一线 DoS 防御,则可以使用此向量。)