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

时间:2009-07-07 08:35:42

标签: java string

关于String.intern()的Javadoc没有提供太多细节。 (简而言之:它返回字符串的规范表示,允许使用==比较实习字符串)

  • 我何时使用此功能赞成String.equals()
  • 是否存在Javadoc中未提及的副作用,即JIT编译器或多或少地进行了优化?
  • String.intern()还有其他用途吗?

20 个答案:

答案 0 :(得分:193)

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

答案 1 :(得分:125)

  

我什么时候使用这个函数来支持String.equals()

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

  

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

主要的缺点是你必须记住确保你实际上做了你要比较的所有字符串intern()。很容易忘记intern()所有字符串,然后你可以得到令人困惑的错误结果。此外,为了每个人的利益,请务必非常清楚地记录您依赖于内部化的字符串。

如果你决定内化字符串,第二个缺点是intern()方法相对昂贵。它必须管理唯一字符串池,以便它做相当多的工作(即使字符串已经内化)。所以,在你的代码设计中要小心,这样你就可以在输入中输入所有适当的字符串,这样你就不用再担心它了。

(来自JGuru)

第三个缺点(仅限Java 7或更低版​​本):实习字符串存在于PermGen空间中,通常非常小;你可能会遇到一个有足够空闲堆空间的OutOfMemoryError。

(来自Michael Borgwardt)

答案 2 :(得分:36)

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();
        }
    }
}

myth of non GCed String.intern()上查看更多(来自我)。

答案 3 :(得分:16)

我最近在Java 6,7和8中写了一篇关于String.intern()实现的文章: String.intern in Java 6, 7 and 8 - string pooling

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

简而言之:

  • 在Java 6中避免String.intern(),因为它进入了PermGen
  • 在Java 7&amp ;;中优先选择String.intern() Java 8:它使用比滚动自己的对象池少4-5倍的内存
  • 请务必调整-XX:StringTableSize(默认值可能太小;设置素数)

答案 4 :(得分:13)

使用==比较字符串比使用equals()

快得多

5时间更快,但由于字符串比较通常只占应用程序总执行时间的一小部分,因此整体增益远小于此,并且最终收益将被稀释到几个百分点。

String.intern()将字符串拉离Heap并将其放入PermGen

内部化的字符串放在不同的存储区域中:Permanent Generation,它是为非用户对象保留的JVM区域,如类,方法和其他内部JVM对象。这个区域的大小是有限的,并且比堆更珍贵。由于此区域比Heap小,因此使用所有空间并获得OutOfMemoryException的可能性更大。

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

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

记住以上3点你可以推断String intern()仅在少数几种情况下才有用,当你进行大量的字符串比较时,如果你不知道最好不要使用内部字符串正是你在做什么......

答案 5 :(得分:7)

  

我什么时候使用这个函数来支持String.equals()

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

出于性能原因的内联字符串,以便您可以将它们作为参考相等性进行比较,只有在您持有对字符串的引用一段时间才会有好处 - 来自用户输入的字符串或IO将不会被实现。

这意味着在您的应用程序中,您从外部源接收输入并将其处理为具有语义值的对象 - 标识符说 - 但该对象具有与原始数据无法区分的类型,并且具有不同的规则如何程序员应该使用它。

创建一个UserId类型(通常很容易创建一个线程安全的通用实习机制)并且就像一个开放的枚举,而不是重载java.lang.String类型,这几乎总是更好如果它恰好是用户ID,则为引用语义。

通过这种方式,您不会混淆特定字符串是否已被实现,并且您可以在开放枚举中封装所需的任何其他行为。

答案 6 :(得分:6)

我不知道有任何优点,如果有人认为equals()本身会在内部使用intern()(它没有)。

Busting intern() myths

答案 7 :(得分:4)

  

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

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

JVMS

JVMS 7 5.1 says

  

字符串文字是对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代码点序列相同的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

请注意:

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

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

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

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

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

奖励:将其与Integer pool进行比较,{{3}}没有直接的字节码支持(即没有CONSTANT_String_info类似物。)

答案 8 :(得分:4)

丹尼尔布鲁克纳绝对是对的。 字符串实习意味着节省内存(堆)。我们的系统目前有一个用于保存特定数据的巨型哈希映射。随着系统规模的扩大,hashmap将足以使堆内存不足(正如我们测试的那样)。通过将所有重复的字符串实际存储在散列映射中的所有对象中,它为我们节省了大量的堆空间。

Also in Java 7, interned strings no long live in PermGen but heap instead.所以你不需要担心它的大小,是的,它会被垃圾收集:

  

在JDK 7中,永久性中不再分配实习字符串   生成Java堆,而是在main中分配   Java堆的一部分(称为年轻和老一代)   与应用程序创建的其他对象。这种改变会   导致更多数据驻留在主Java堆中,并且数据中的数据更少   永久世代,因此可能需要堆大小   调整。大多数应用程序只会看到相对较小的差异   由于此更改导致堆使用,但加载的应用程序更大   很多类或者大量使用String.intern()方法都会看到   更显着的差异。

答案 9 :(得分:2)

我会检查实习生和== - 比较而不是等于仅在等于比较的情况下,在多重比较字符串中是瓶颈。这很少有助于少量比较,因为intern()不是免费的。在积极实施字符串之后,你会发现对intern()的调用越来越慢。

答案 10 :(得分:2)

在经常调用equals()方法的情况下,字符串实习很有用,因为equals()方法会快速检查方法开头的对象是否相同。 / p>

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

这通常在搜索Collection时发生,但其他代码也可以进行字符串相等检查。

虽然实习涉及成本,但我执行了一些代码的微基准测试,发现实习过程会使运行时间增加10倍。

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

可以完成的另一个地方是处理可用于进行密钥查找的用户输入。这通常发生在您的请求处理器中,请注意应该传递实习字符串。

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

答案 11 :(得分:2)

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

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

答案 12 :(得分: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是一只活的成年雄性虎 - 您可能需要各种许可才能订购其中一种。也许你最好同时订购安全头盔。)

所以听起来好像用你的intern()版本替换你的字符串可以获得好处,但是你得到了安全性 - 可读性和标准合规性 - 无需使用“==”equals()在你的编程。如果确实如此,那么我要说的大部分内容取决于这是真的。

但是String.equals()在使用"=="之前是否测试过你传递了一个字符串而不是其他一些对象?我没资格说,但我猜不会,因为绝大多数这样的equals()操作都是String to String,因此测试几乎总是通过。实际上,在String.equals()中优先考虑“==”意味着您经常将字符串与同一实际对象进行比较。

我希望以下几行产生“假”的结果并不让人感到惊讶:

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

但如果您在第二行中将i更改为i.toString(),当然是true

显然,您希望从实习中受益的场所包括SetMap。我希望interned字符串缓存其哈希码...我认为这是一个要求。我希望我不仅仅给出了一个可以赚到一百万美元的想法。 : - )

至于内存,如果您的字符串数量很大,或者您希望程序代码使用的内存非常小,那么这也是一个重要的限制。如果-distinct-Strings的卷非常大,那么可能需要考虑使用专用的数据库程序代码来管理它们,以及单独的数据库服务器。同样,如果你可以通过让它根本不存储它的字符串本身来改进一个小程序(需要同时运行10000个实例)。

创建一个新的String然后立即丢弃它的intern()替换它会感到浪费,但除了保留重复的String之外,没有明确的替代方法。因此,执行成本实际上是在实习池中搜索您的字符串,然后允许垃圾收集器处理原始字符串。如果它是一个字符串文字,那么无论如何它都已经实现了。

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

答案 13 :(得分:1)

我会投票支持它不值得维护麻烦。

大多数情况下,没有必要,也没有性能优势,除非您的代码在子字符串中做了很多工作。在这种情况下,String类将使用原始字符串加上偏移量来节省内存。如果你的代码经常使用子字符串,那么我怀疑这只会导致你的内存需求爆炸。

答案 14 :(得分:0)

让我们面对现实:主要的用例场景是当你读取数据流时(通过输入流或来自JDBC ResultSet),并且有无数的小字符串在整个过程中重复。

这是一个小技巧,可以让你控制你想用什么类型的机制来内化Strings和其他不可变的东西,以及一个示例实现:

/**
 * 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,使用它来内化字符串和其他小的不可变对象,然后释放它。例如:

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

答案 15 :(得分:0)

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

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

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

答案 16 :(得分:0)

我使用实习生来节省内存,我在内存中保存了大量的String数据并且移动到使用intern()节省了大量的内存。不幸的是,虽然它使用的内存很少,但它使用的内存存储在PermGen内存而不是Heap中,很难向客户解释如何增加这类内存的分配。

因此有一个替代intern()来减少内存消耗,(= =对等于性能优势对我来说不是一个问题)

答案 17 :(得分:0)

实际输入字符串的成本远远超过单个stringA.equals(B)比较中保存的时间。仅在重复使用相同的未更改字符串变量时才使用它(出于性能原因)。例如,如果您经常迭代一个稳定的字符串列表来更新一些键入相同字符串字段的地图,您可以获得很好的保存。

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

还要记住String是不可变的,不要犯

的愚蠢错误
String a = SOME_RANDOM_VALUE
a.intern()

记得要做

String a = SOME_RANDOM_VALUE.intern()

答案 18 :(得分:0)

使用实习生的真正原因不是上述原因。 在出现内存不足错误后,您可以使用它。典型程序中的大量字符串是其他大字符串的String.substring()[想想从100K xml文件中取出用户名。 java实现是这样的,子字符串包含对原始字符串的引用以及该巨大字符串中的start + end。 (背后的想法是重用相同的大字符串)

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

答案 19 :(得分: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