【问题标题】:What is Java String interning?什么是 Java 字符串实习?
【发布时间】:2012-05-21 16:20:26
【问题描述】:

什么是 Java 中的 String Interning,我应该在什么时候使用它,以及为什么

【问题讨论】:

标签: java string string-interning


【解决方案1】:

http://docs.oracle.com/javase/7/docs/api/java/lang/String.html#intern()

基本上对一系列字符串执行 String.intern() 将确保所有具有相同内容的字符串共享相同的内存。因此,如果您有 'john' 出现 1000 次的名称列表,通过实习可以确保只有一个 'john' 实际分配了内存。

这对于减少程序的内存需求很有用。但请注意,缓存由 JVM 维护在永久内存池中,与堆相比,其大小通常有限,因此如果您没有太多重复值,则不应使用 intern。


更多关于使用 intern() 的内存限制

一方面,您确实可以通过以下方式删除重复的字符串 将它们内化。问题是内部化的字符串去 永久代,这是 JVM 中保留的区域 对于非用户对象,如类、方法和其他内部 JVM 对象。这个区域的大小是有限的,通常要小得多 比堆。在 String 上调用 intern() 具有移动的效果 它从堆中取出到永久代中,你冒着风险 PermGen 空间不足。

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


从 JDK 7(我的意思是在 HotSpot 中)开始,发生了一些变化。

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

-- 来自Java SE 7 Features and Enhancements

更新:从 Java 7 开始,内部字符串存储在主堆中。 http://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html#jdk7changes

【讨论】:

  • "但是请注意,缓存是由 JVM 维护在永久内存池中的,通常大小有限......" 你能解释一下吗?我不明白
  • “interned”字符串存储在 JVM 的一个特殊内存区域中。此内存区域通常具有固定大小,并且不是存储其他数据的常规 Java 堆的一部分。由于固定大小,这个永久内存区域可能会被所有字符串填满,从而导致丑陋的问题(无法加载类和其他问题)。
  • @cello 那么,它类似于缓存吗?
  • @grassPro:是的,它是一种缓存,由 JVM 原生提供。注意,由于 Sun/Oracle JVM 和 JRockit 的合并,JVM 工程师试图摆脱 JDK 8 (openjdk.java.net/jeps/122) 中的永久内存区域,因此将来不会有任何大小限制.
  • 程序员还应该意识到字符串实习可能具有安全隐患。如果您在内存中有敏感文本(例如密码)作为字符串,即使实际的字符串对象早已被 GC 处理,它也可能会在内存中保留很长时间。如果坏人以某种方式访问​​内存转储,那可能会很麻烦。即使没有实习,这个问题也存在(因为 GC 从一开始就不确定等),但这会使情况变得更糟。对敏感文本使用 char[] 而不是 String 总是一个好主意,并在不再需要时立即将其归零。
【解决方案2】:

有一些“引人入胜的面试”问题,例如为什么你会得到equals!如果你执行下面的代码。

String s1 = "testString";
String s2 = "testString";
if(s1 == s2) System.out.println("equals!");

如果你想比较字符串,你应该使用equals()。以上将打印等于,因为testString 已经被编译器实习。您可以使用 intern 方法自己对字符串进行实习,如以前的答案所示......

【讨论】:

  • 您的示例很棘手,因为即使您使用equals 方法,它也会产生相同的打印结果。您可能需要添加 new String() 比较以更清楚地显示区别。
  • @giannischristofakis 但是如果我们使用 new String(), == 不会失败吗? java 是否也会自动内化新的字符串?
  • @giannischristofakis 当然如果你使用 new String() 它会在 == 上失败。但是 new String(...).intern() 不会在 == 上失败,因为 intern 将返回相同的字符串。简单假设编译器在字面量中执行 new String().intern
【解决方案3】:

JLS

JLS 7 3.10.5对其进行了定义并举了一个实际的例子:

此外,字符串字面量总是引用 String 类的同一个实例。这是因为字符串字面量 - 或者更一般地说,作为常量表达式值的字符串(第 15.28 节) - 是“内部的”,以便使用 String.intern 方法共享唯一实例。

示例 3.10.5-1。字符串字面量

由编译单元组成的程序(§7.3):

package testPackage;
class Test {
    public static void main(String[] args) {
        String hello = "Hello", lo = "lo";
        System.out.print((hello == "Hello") + " ");
        System.out.print((Other.hello == hello) + " ");
        System.out.print((other.Other.hello == hello) + " ");
        System.out.print((hello == ("Hel"+"lo")) + " ");
        System.out.print((hello == ("Hel"+lo)) + " ");
        System.out.println(hello == ("Hel"+lo).intern());
    }
}
class Other { static String hello = "Hello"; }

和编译单元:

package other;
public class Other { public static String hello = "Hello"; }

产生输出:

true true true true false true

JVMS

JVMS 7 5.1 says 说实习是通过专用的CONSTANT_String_info 结构神奇而高效地实现的(与大多数其他具有更通用表示的对象不同):

字符串字面量是对 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 结构给出的代码点序列。

  • 如果先前已在包含与 CONSTANT_String_info 结构给出的相同的 Unicode 代码点序列的类 String 的实例上调用方法 String.intern,则字符串文字派生的结果是对该类的引用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 作为参数)
  • 35: acif_acmpne 比较为常规对象

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

上面的 JVMS 引用似乎是说,每当指向的 Utf8 相同时,ldc 就会加载相同的实例。

我对字段做了类似的测试,并且:

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

结论:字符串池有直接字节码支持,内存表示效率高。

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

【讨论】:

    【解决方案4】:

    Java 8 或更高版本的更新。 在 Java 8 中,PermGen(永久代)空间被移除并被 Meta Space 取代。字符串池内存被移动到 JVM 的堆中。

    与 Java 7 相比,堆中的字符串池大小有所增加。因此,您有更多空间用于内部化字符串,但您对整个应用程序的内存更少。

    还有一件事,你已经知道在Java中比较2个对象(的引用)时,'=='用于比较对象的引用,'equals'用于比较对象的内容.

    让我们检查一下这段代码:

    String value1 = "70";
    String value2 = "70";
    String value3 = new Integer(70).toString();
    

    结果:

    value1 == value2 ---> 是的

    value1 == value3 ---> 错误

    value1.equals(value3) ---> 是的

    value1 == value3.intern() ---> 是的

    这就是为什么你应该使用 'equals' 来比较 2 个 String 对象。这就是intern() 的用处所在。

    【讨论】:

      【解决方案5】:

      由于字符串是对象,并且由于 Java 中的所有对象始终只存储在堆空间中,因此所有字符串都存储在堆空间中。但是,Java 将不使用 new 关键字创建的字符串保存在堆空间的一个特殊区域中,该区域称为“字符串池”。 Java 将使用 new 关键字创建的字符串保存在常规堆空间中。

      字符串池的目的是维护一组唯一的字符串。每当您不使用 new 关键字创建新字符串时,Java 都会检查字符串池中是否已存在相同的字符串。如果是,Java 返回对同一个 String 对象的引用,如果不是,Java 在字符串池中创建一个新的 String 对象并返回它的引用。因此,例如,如果您在代码中使用字符串“hello”两次,如下所示,您将获得对同一字符串的引用。实际上,我们可以通过使用 == 运算符比较两个不同的参考变量来验证这个理论,如下面的代码所示:

      String str1 = "hello";
      String str2 = "hello";
      System.out.println(str1 == str2); //prints true
      
      String str3 = new String("hello");
      String str4 = new String("hello");
      
      System.out.println(str1 == str3); //prints false
      System.out.println(str3 == str4); //prints false 
      

      == 运算符只是检查两个引用是否指向同一个对象,如果它们指向则返回 true。在上面的代码中,str2 获取对之前创建的同一个 String 对象的引用。但是,str3str4 获得对两个完全不同的 String 对象的引用。这就是为什么 str1 == str2 返回 true 而 str1 == str3str3 == str4 返回 false 的原因。 实际上,当您执行 new String("hello"); 时,如果这是第一次在程序中的任何位置使用字符串“hello”,则会创建两个 String 对象,而不是仅创建一个 - 一个由于使用了带引号的字符串,因此在字符串池中,由于使用了 new 关键字,因此在常规堆空间中。

      字符串池是 Java 通过避免创建包含相同值的多个字符串对象来节省程序内存的方法。可以使用 String 的 intern 方法从字符串池中为使用 new 关键字创建的字符串获取字符串。它被称为字符串对象的“实习”。例如,

      String str1 = "hello";
      String str2 = new String("hello");
      String str3 = str2.intern(); //get an interned string obj
      
      System.out.println(str1 == str2); //prints false
      System.out.println(str1 == str3); //prints true
      

      OCP Java SE 11 Programmer, Deshmukh

      【讨论】:

        【解决方案6】:

        字符串实习是编译器的一种优化技术。如果您在一个编译单元中有两个相同的字符串字面量,则生成的代码可确保在程序集中为该字面量的所有实例(用双引号括起来的字符)只创建一个字符串对象。

        我是C#背景,所以我可以举个例子来解释一下:

        object obj = "Int32";
        string str1 = "Int32";
        string str2 = typeof(int).Name;
        

        以下比较的输出:

        Console.WriteLine(obj == str1); // true
        Console.WriteLine(str1 == str2); // true    
        Console.WriteLine(obj == str2); // false !?
        

        注意1:对象通过引用进行比较。

        Note2:typeof(int).Name 是通过反射方法评估的,因此它不会在编译时进行评估。 这些比较是在编译时进行的。

        结果分析: 1) true 因为它们都包含相同的文字,因此生成的代码将只有一个引用“Int32”的对象。 见注释 1

        2) true 因为两个值的内容都被检查过是否相同。

        3) FALSE 因为 str2 和 obj 没有相同的文字。请参阅注释 2

        【讨论】:

        • 比这更强大。由同一个类加载器加载的任何字符串字面量都将引用同一个字符串。请参阅 JLS 和 JVM 规范。
        • @user207421 实际上,它甚至与字符串文字属于哪个类加载器无关。
        【解决方案7】:
        Java interning() method basically makes sure that if String object is present in SCP, If yes then it returns that object and if not then creates that objects in SCP and return its references
        
        for eg: String s1=new String("abc");
                String s2="abc";
                String s3="abc";
        
        s1==s2// false, because 1 object of s1 is stored in heap and other in scp(but this objects doesn't have explicit reference) and s2 in scp
        s2==s3// true
        
        now if we do intern on s1
        s1=s1.intern() 
        
        //JVM checks if there is any string in the pool with value “abc” is present? Since there is a string object in the pool with value “abc”, its reference is returned.
        Notice that we are calling s1 = s1.intern(), so the s1 is now referring to the string pool object having value “abc”.
        At this point, all the three string objects are referring to the same object in the string pool. Hence s1==s2 is returning true now.
        

        【讨论】:

          【解决方案8】:

          通过使用堆对象引用,如果我们想要对应的SCP对象引用我们应该去intern()方法。

          示例

          class InternDemo
          {
          public static void main(String[] args)
          {
          String s1=new String("smith");
          String s2=s1.intern();
          String s3="smith";
          System.out.println(s2==s3);//true
          }
          }
          

          intern flow chart

          【讨论】:

            猜你喜欢
            • 2011-03-27
            • 2013-01-06
            • 2016-06-18
            • 1970-01-01
            • 1970-01-01
            • 2017-12-23
            • 2013-07-14
            • 2011-04-22
            • 2011-02-11
            相关资源
            最近更新 更多