使用java.lang.String.intern()是一种好习惯吗?


194

关于Javadoc的内容String.intern()不多。(简而言之:它返回字符串的规范表示形式,从而允许使用来比较被嵌入的字符串==

  • 什么时候有利于使用此功能String.equals()
  • 是否存在Javadoc中未提及的副作用,即JIT编译器或多或少的优化?
  • 还有其他用途String.intern()吗?

14
调用intern()有其自身的性能影响,使用intern()来提高性能需要进行测试,以确保它确实可以显着加快程序的速度,值得付出额外的复杂性。您也可以使用此方法来减少具有可靠值的大型表的内存消耗。但是,在两种情况下,都有其他更好的选择。
彼得·劳瑞

是的,intern()有其自身的性能影响。尤其是因为intern()成本随着您实习字符串并保持对它们的引用而线性增加。至少在sun / oracle 1.6.0_30 vm上。
lacroix1547 2012年

Answers:


125

我何时会使用此函数来支持String.equals()

当您需要速度时,因为可以按引用比较字符串(==比等于快)

是否有Javadoc中未提及的副作用?

主要缺点是您必须记住要确保实际上要对要比较的所有字符串执行intern()。忘记intern()所有字符串很容易,然后您会得到令人困惑的错误结果。另外,为了大家的缘故,请确保非常清楚地证明您所依赖的是内部化的字符串。

如果您决定内部化字符串的第二个缺点是intern()方法相对昂贵。它必须管理唯一的字符串池,因此需要做很多工作(即使字符串已经被内部化了)。因此,在代码设计中要格外小心,例如,在输入时使用intern()所有合适的字符串,这样就不必担心它了。

(来自JGuru)

第三个缺点(仅限Java 7或更低版​​本):实习字符串生活在PermGen空间中,该空间通常很小;您可能会遇到带有大量可用堆空间的OutOfMemoryError。

(摘自Michael Borgwardt)


64
第三个缺点:实习字符串生活在PermGen空间中,通常很小。您可能会遇到带有大量可用堆空间的OutOfMemoryError。
Michael Borgwardt

15
AFAIK较新的VM还会垃圾收集PermGen空间。
Daniel Rikowski 09年

31
实习生是关于内存管理,而不是比较速度。if (s1.equals(s2))和之间的差异if (i1 == i2)很小,除非您有很多长字符串相同的前导字符。在大多数实际用途中(URL除外),字符串在前几个字符内会有所不同。无论如何,长的if-else链仍然是代码的味道:使用枚举和函子映射。
kdgregory 2010年

25
您仍然可以在整个程序中使用s1.equals语法,请勿在内部使用== ,. equals使用==内部进行短路评估
gtrak 2011年

15
迈克尔·伯格沃德(Michael Borgwardt)并未表示无法对垃圾回收的字符串进行垃圾收集。那是一个假的断言。迈克尔的评论(正确)说的比这更微妙。
Stephen C

193

这(几乎)与字符串比较无关。字符串实习的目的是保存记忆,如果你有在你的应用程序相同的内容许多字符串。通过使用String.intern()该应用程序,从长远来看将只有一个实例,并且副作用是您可以执行快速引用相等比较而不是普通的字符串比较(但这通常是不可取的,因为通过忘记仅使用intern确实很容易破坏它)一个实例)。


4
那是不对的。求值每个字符串表达式时,始终会自动自动进行字符串中间操作。对于所使用的每个唯一字符串,总会有一个副本;如果发生多种用法,则它是“内部共享”的。调用String.intern()不会使所有事情都发生-它只是返回内部规范表示。参见javadoc。
格伦·贝斯特

16
需要澄清-对于编译时常量字符串(文字和固定表达式),实习总是自动发生。另外,它在运行时动态评估的Strings上调用String.intern()时发生。
格伦·贝斯特

因此,您的意思是,如果堆中有1000个“ Hello”对象,而我对其中之一执行了intern(),那么剩下的999个对象将被自动销毁?
阿伦·拉伊

@ArunRaaj不,您将仍然有1000个在堆上,在内部池中还有一个额外的堆,可以str.intern()stris 以后再使用"Hello"
Matthieu

37

String.intern()绝对是现代JVM中收集的垃圾。
由于GC活动,以下NEVER不会耗尽内存:

// java -cp . -Xmx128m UserOfIntern

public class UserOfIntern {
    public static void main(String[] args) {
        Random random = new Random();
        System.out.println(random.nextLong());
        while (true) {
            String s = String.valueOf(random.nextLong());
            s = s.intern();
        }
    }
}

非GCed String.intern()神话中了解更多(来自我


26
OutOfMemoryException-不,不是上面的代码,在我的脑海:链接到javaturning文章,该文章指向此文章,指向javaturning文章,其中... :-)
user85421,2010年

尽管您可以看到该帖子已被编辑以添加该链接;)
2012年

3
您可能想提到您也是链接到的外部参考的作者。
托尔比约恩Ravn的安徒生

11
@Carlos连接一个外部引用,链接回计算器应该引起.. :)#1
Seiti

2
这些年来,@ Seiti的循环引用很容易被发现:p
Ajay

16

我最近写了一篇有关Java 6、7和8中String.intern()实现的文章: Java 6、7和8字符串池中的String.intern

我希望它应该包含有关Java字符串池当前情况的足够信息。

简而言之:

  • 避免String.intern()在Java 6中使用,因为它已包含在PermGen中
  • 不想String.intern()用Java 7的Java 8:它使用较少的4-5倍的内存比滚动自己的对象池
  • 确保调整-XX:StringTableSize(默认值可能太小;设置质数)

2
请不要仅仅发布指向您博客的链接,有些人将其视为垃圾邮件。另外,博客链接有明显的死于404死亡的趋势。请在此处内联总结您的文章,或在该问题的评论中保留该链接。

3
感谢您编写@ mik1!非常丰富,清晰和最新的文章。(我回来时打算自己发布一个链接。)
卢克·乌舍伍德

1
感谢您提及-XX参数。您也可以使用它来查看表状态:-XX:+ PrintStringTableStatistics
csadler 2014年

13

用==比较字符串比用equals()比较字符串快得多

5时间更快,但是由于字符串比较通常只占应用程序总执行时间的一小部分,因此总收益要小得多,最终收益将被稀释到百分之几。

String.intern()将字符串从Heap中拉出并放入PermGen中

内部化的字符串放在不同的存储区域中:永久生成,这是JVM的一个区域,保留给非用户对象,例如类,方法和其他内部JVM对象。这个区域的大小是有限的,比堆大得多。由于此区域小于Heap,因此更有可能使用所有空间并获得OutOfMemoryException。

String.intern()字符串被垃圾回收

在新版本的JVM中,当未由任何对象引用时,也会对内部化的字符串进行垃圾回收。

请牢记以上三点,您可以推断出String intern()仅在您进行大量字符串比较时仅在少数情况下才有用,但是最好不要使用内部字符串,如果您不确切知道自己在做什么。是做 ...



1
只需补充一点,有时可以从中恢复堆内存异常,尤其是在线程模型(例如Web应用程序)中。当permgen耗尽时,应用程序通常将永久无法运行,并且经常会浪费资源直到被杀死。
泰勒,

7

我何时会使用此函数来支持String.equals()

鉴于他们做不同的事情,可能永远不会。

出于性能原因而对字符串进行internation,以便您可以比较它们是否具有引用相等性,这仅在您保留对字符串的引用一段时间时才有好处-来自用户输入或IO的字符串不会被intern。

这意味着在您的应用程序中,您将接收来自外部源的输入,并将其处理为具有语义值的对象(例如标识符),但是该对象的类型与原始数据没有区别,并且对于程序员应该如何使用具有不同的规则用它。

创建一个内部UserId类型(创建一个线程安全的通用内部机制很容易)并且像一个开放的枚举,几乎总是比java.lang.String使用引用语义重载该类型(如果它恰好是用户ID)更好。

这样一来,您就不会在特定的String是否已被插入之间感到困惑,并且可以将所需的任何其他行为封装在开放枚举中。


6

我没有意识到任何优势,如果有人认为,equals()本身会在内部使用intern()(实际上没有)。

破坏intern()神话


7
尽管您说自己不知道任何优势,但您发布的链接通过==标识的比较速度提高了5倍,因此对于以文本为中心的性能代码很重要
Brian Agnew

3
当您要进行大量的文本比较时,您最终会用完PermGen空间。当没有太多文本比较可做时,速度差异并不重要。无论哪种方式,都不要intern()您的字符串。这不值得。
孟买

还要继续说,总体相对增益通常很小。
对象

我认为这种逻辑无效。好的链接!
Daniel Rikowski 09年

1
@DR:什么逻辑?那是一大谬论。@objects:对不起,但您的论点缺乏理由。有非常好的理由使用intern,并很好的理由equals不默认这样做。您发布的链接是完整的错误提示。最后一段甚至承认intern有一个有效的使用场景:繁重的文本处理(例如解析器)。得出结论:“ [XYZ]如果您不知道自己在做什么就很危险”,这太平庸了,以至于身体受到伤害。
康拉德·鲁道夫

4

丹尼尔·布鲁克纳(DanielBrückner)绝对正确。字符串实习是为了节省内存(堆)。我们的系统目前有一个用于保存某些数据的巨型哈希图。随着系统的扩展,哈希图将足够大以使堆内存不足(如我们所测试)。通过遍历哈希图中所有对象的所有重复字符串,它为我们节省了大量的堆空间。

同样在Java 7中,实习生字符串不再存在于PermGen中,而是堆中。因此,您不必担心它的大小,是的,它可以收集垃圾:

在JDK 7中,不再将永久字符串分配给Java堆的永久代,而是分配给Java堆的主要部分(称为年轻代和旧代),以及由应用程序创建的其他对象。 。此更改将导致更多数据驻留在主Java堆中,而永久生成中的数据更少,因此可能需要调整堆大小。由于此更改,大多数应用程序只会看到相对较小的堆使用差异,但是加载许多类或大量使用String.intern()方法的大型应用程序将看到更大的差异。


我必须第二点:在我的软件上,堆转储显示String实例使用了大部分堆空间。查看它们的内容时,我看到很多重复项,并决定切换到intern(),从而节省了数百MB。
Matthieu

4

是否存在Javadoc中未提及的副作用,即JIT编译器或多或少的优化?

我不了解JIT级别,但是对字符串池有直接的字节码支持,该字符串池通过专用CONSTANT_String_info结构神奇而有效地实现了(不同于大多数其他具有更通用表示形式的对象)。

虚拟机

JVMS 7 5.1说

字符串文字是对String类实例的引用,它是从CONSTANT_String_info结构(第4.4.3节)以类或接口的二进制表示形式派生的。CONSTANT_String_info结构给出了构成字符串文字的Unicode代码点的序列。

Java编程语言要求相同的字符串文字(即,包含相同代码点序列的文字)必须引用String类的相同实例(JLS§3.10.5)。另外,如果在任何字符串上调用String.intern方法,则结果是对该类实例的引用,如果该字符串以文字形式出现,则将返回该实例。因此,以下表达式必须具有值true:

("a" + "b" + "c").intern() == "abc"

为了派生字符串文字,Java虚拟机检查由CONSTANT_String_info结构给出的代码点序列。

  • 如果先前在类String的实例上调用了String.intern方法,该实例包含与CONSTANT_String_info结构给出的序列相同的Unicode代码点序列,则字符串文字派生的结果是对该类String的该实例的引用。

  • 否则,将创建一个新的String类实例,其中包含由CONSTANT_String_info结构给出的Unicode代码点序列;对该类实例的引用是字符串文字派生的结果。最后,调用新的String实例的intern方法。

字节码

查看OpenJDK 7上的字节码实现也很有帮助。

如果我们反编译:

public class StringPool {
    public static void main(String[] args) {
        String a = "abc";
        String b = "abc";
        String c = new String("abc");
        System.out.println(a);
        System.out.println(b);
        System.out.println(a == c);
    }
}

我们在常量池上有:

#2 = String             #32   // abc
[...]
#32 = Utf8               abc

main

 0: ldc           #2          // String abc
 2: astore_1
 3: ldc           #2          // String abc
 5: astore_2
 6: new           #3          // class java/lang/String
 9: dup
10: ldc           #2          // String abc
12: invokespecial #4          // Method java/lang/String."<init>":(Ljava/lang/String;)V
15: astore_3
16: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: invokevirtual #6          // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload_2
27: invokevirtual #6          // Method java/io/PrintStream.println:(Ljava/lang/String;)V
30: getstatic     #5          // Field java/lang/System.out:Ljava/io/PrintStream;
33: aload_1
34: aload_3
35: if_acmpne     42
38: iconst_1
39: goto          43
42: iconst_0
43: invokevirtual #7          // Method java/io/PrintStream.println:(Z)V

注意如何:

  • 03ldc #2加载相同的常量(文字)
  • 12:创建了一个新的字符串实例(#2作为参数)
  • 35ac作为常规对象进行比较if_acmpne

在字节码上,常量字符串的表示非常神奇:

并且上面的JVMS引用似乎表明,只要Utf8指向相同,就会通过加载相同的实例ldc

我已经对字段进行了类似的测试,并且:

  • static final String s = "abc"通过ConstantValue属性指向常量表
  • 非最终字段没有该属性,但仍可以使用进行初始化 ldc

奖励:将其与没有直接字节码支持(即无模拟)的Integer池进行比较CONSTANT_String_info


2

我只会在字符串的多次比较中使equals-comparison成为瓶颈的情况下,检查intern和==-comparison而不是equals。因为intern()不是免费的,所以这极不可能帮助进行少量的比较。在积极地实习字符串之后,您会发现对intern()的调用越来越慢。


2

subString()与源字符串相比,结果的使用量较小且对象的寿命较长时,可能会导致使用某种内存泄漏。

正常的解决方案是使用,new String( s.subString(...))但是当您有一个存储潜在/可能结果的类subString(...)并且无法控制调用者时,您可能会考虑存储intern()传递给构造函数的String参数。这释放了潜在的大缓冲区。


有趣,但这也许取决于实现。
akostadinov

1
在Java 1.8和1.7.06(及更高版本)中,不会发生上述潜在的内存泄漏,请参阅对Java 1.7.0_06中对String内部表示的更改
eremmel 2014年

确认仅在性能和/或内存配置文件之后的必要时才应用微优化。谢谢。
akostadinov 2014年

2

equals()频繁调用该方法的情况下,字符串插入很有用,因为该equals()方法会快速检查以查看对象在方法开始时是否相同。

if (this == anObject) {
    return true;
}

当搜索Collection其他代码也可能进行字符串相等性检查时,通常会发生这种情况。

但是,进行实习需要一定的成本,我对一些代码进行了微基准测试,发现实习过程使运行时间增加了10倍。

进行实习的最佳位置通常是在读取存储在代码外部的键时,因为代码中的字符串会被自动实习。通常,这会在应用程序的初始化阶段发生,以防止第一用户的损失。

可以完成此操作的另一个地方是在处理可用于进行键查找的用户输入时。这通常发生在您的请求处理器中,请注意,应该将传入的字符串向下传递。

除此之外,对代码的其余部分进行实习没有多大意义,因为它通常不会带来任何好处。


1

我为它不值得进行维护麻烦而投票。

在大多数情况下,除非您的代码对子字符串做了很多工作,否则就没有任何需要,也不会带来性能上的好处。在这种情况下,String类将使用原始字符串加上偏移量以节省内存。如果您的代码大量使用子字符串,那么我怀疑这只会导致您的内存需求激增。


1

http://kohlerm.blogspot.co.uk/2009/01/is-javalangstringintern-really-evil.html

断言String.equals()用于根据之前的对象"=="进行比较String

http://www.codeinstructions.com/2009/01/busting-javalangstringintern-myths.html

它比较字符串的长度,然后比较内容。

(顺便说一句,销售目录中的产品代码字符串可能具有相同的长度-BIC0417是骑自行车的人的安全帽,TIG0003是活的成年雄性老虎-您可能需要各种许可证才能订购其中的一个。也许您最好同时订购安全帽。)

因此,听起来好像您可以从用Strings intern()版本替换Strings中受益,但是无需equals()在编程中使用“ ==”即可获得安全性-可读性和标准合规性- 。我要说的大部分内容都取决于事实是否正确。

但是String.equals()在使用前是否测试过是否将字符串而不是其他对象传递给了它"=="?我没有资格说,但我不会猜到,因为绝大多数此类equals()操作大多数都是从String到String,因此几乎总是通过测试。的确,在内部String.equals()对“ ==”设置优先级意味着您可以经常将String与相同的实际对象进行比较。

我希望没人对以下几行产生“ false”的结果感到惊讶:

    Integer i = 1;
    System.out.println("1".equals(i));

但是,如果更改ii.toString()第二行,那当然是true

您可能希望从实习中受益的场所包括SetMap。我希望对实习生字符串的哈希码进行缓存...我认为这是必要条件。而且我希望我不仅放弃了一个可以赚到我一百万美元的想法。:-)

对于内存,很明显,如果String的数量很大,或者您希望程序代码使用的内存很小,则这是一个重要的限制。如果-distinct-字符串的数量非常大,那么可能是时候考虑使用专用的数据库程序代码来管理它们以及单独的数据库服务器了。同样,如果您可以改进小型程序(需要同时在10000个实例中运行),则可以使其根本不存储其字符串。

创建一个新的String并立即丢弃以intern()替换它是浪费时间,但是除了保留重复的String之外,没有其他明确的选择。因此,实际上执行成本是在内部池中搜索您的字符串,然后允许垃圾回收器处置原始字符串。而且,如果它是字符串文字,那么无论如何它都已经被内联了。

我想知道是否intern()可以被恶意程序代码滥用,以检测某些String及其对象引用是否已在intern()池中存在,因此是否应该在Java会话的其他位置存在(不应该知道)。我猜这只有在程序代码已经以信任的方式使用时才有可能。尽管如此,您仍然需要考虑程序中所包含的第三方库来存储和记住您的ATM PIN码!


0

使用实习生的真正原因并非以上所述。您遇到内存不足错误后就可以使用它。在典型程序中,很多字符串是其他大字符串的String.substring()[考虑从100K xml文件中提取用户名。Java实现是,子字符串包含对原始字符串的引用以及该巨大字符串中的start + end。(其背后的思想是重用相同的大字符串)

在1000个大文件之后,您仅会保存1000个短名称,您将在内存中保留整个1000个文件!解决方案:在这种情况下,只需使用smallsubstring.intern()


如果需要,为什么不从子字符串中创建一个新字符串呢?
托尔比约恩Ravn的安徒生

0

我正在使用intern来节省内存,我在内存中保存了大量的String数据,然后使用intern()保存了大量的内存。不幸的是,尽管它使用的内存少得多,但它确实使用的内存存储在PermGen内存中而不是Heap中,并且很难向客户解释如何增加此类内存的分配。

因此,可以使用intern()来减少内存消耗,(对我而言= =与equals相比性能优势不是问题)


0

让我们面对现实:主要的用例场景是当您读取数据流(通过输入流或从JDBC ResultSet中读取)时,有无数的小字符串重复出现。

这是一个小技巧,使您可以控制要用于内部化String和其他不可变对象的机制,并提供示例实现:

/**
 * Extends the notion of String.intern() to different mechanisms and
 * different types. For example, an implementation can use an
 * LRUCache<T,?>, or a WeakHashMap.
 */
public interface Internalizer<T> {
    public T get(T obj);
}
public static class LRUInternalizer<T> implements Internalizer<T> {
    private final LRUCache<T, T> cache;
    public LRUInternalizer(int size) {
        cache = new LRUCache<T, T>(size) {
            private static final long serialVersionUID = 1L;
            @Override
            protected T retrieve(T key) {
                return key;
            }
        };
    }
    @Override
    public T get(T obj) {
        return cache.get(obj);
    }
}
public class PermGenInternalizer implements Internalizer<String> {
    @Override
    public String get(String obj) {
        return obj.intern();
    }
}

当我从流或ResultSet中读取字段时,我经常使用它。注意:LRUCache是基于的简单缓存LinkedHashMap<K,V>retrieve()对于所有的缓存未命中,它将自动调用用户提供的方法。

使用此方法的方法是LRUInternalizer在读取之前创建一个,然后使用它内部化Strings和其他小的不可变对象,然后释放它。例如:

Internalizer<String> internalizer = new LRUInternalizer(2048);
// ... get some object "input" that stream fields
for (String s : input.nextField()) {
    s = internalizer.get(s);
    // store s...
}

0

我正在使用它来缓存链接到相关名称的大约36000个代码的内容。我在缓存中实习字符串,因为许多代码都指向相同的字符串。

通过在缓存中插入字符串,我可以确保指向同一字符串的代码实际上指向同一内存,从而节省了我的RAM空间。

如果实习生的弦实际上是垃圾收集的,那对我来说根本不起作用。这将基本上否定实习的目的。我不会被垃圾收集,因为我持有对缓存中每个字符串的引用。


不,在某个时间在内存中的所有相等的相等字符串仍将是同一对象。它与垃圾回收之前在内存中的相等字符串的对象不同。但这没有问题,因为旧字符串不再存在。
bdruemen '16

0

字符串的实习成本远远大于单个字符串A.equals(B)比较中节省的时间。(出于性能原因)仅在重复使用相同的未更改字符串变量时才使用它。例如,如果您定期遍历稳定的字符串列表以更新在相同字符串字段上键入的某些映射,则可以节省很多。

我建议您在优化代码的特定部分时使用字符串实习来调整性能。

还要记住,字符串是不可变的,不要犯以下愚蠢的错误

String a = SOME_RANDOM_VALUE
a.intern()

记得做

String a = SOME_RANDOM_VALUE.intern()

0

如果您正在寻找String.intern的无限替代品,同时也进行了垃圾回收,那么以下内容对我来说很有效。

private static WeakHashMap<String, WeakReference<String>> internStrings = new WeakHashMap<>();
public static String internalize(String k) {
    synchronized (internStrings) {
        WeakReference<String> weakReference = internStrings.get(k);
        String v = weakReference != null ? weakReference.get() : null;
        if (v == null) {
            v = k;
            internStrings.put(v, new WeakReference<String>(v));
        }
        return v;
    }
}

当然,如果可以大致估计会有多少个不同的字符串,则只需将String.intern()与-XX:StringTableSize = highEnoughValue一起使用


SoftRef会带来更多意义。
vach

@vach通过使用WeakReference(而不是SoftReference)可以更早地释放内存,因此其他分配可能会更快。这取决于应用程序在做什么,任何一个都可以理解。
bdruemen '18
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.