什么是Java String interning?

sap*_*Pro 219 java string string-interning

什么是Java中的String Interning,何时应该使用它,为什么?

Ash*_*Jha 221

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

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

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


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

一方面,您可以通过内化它们来删除String重复项.问题是内化字符串转到永久生成,它是为非用户对象保留的JVM区域,如类,方法和其他内部JVM对象.该区域的大小有限,通常比堆小得多.在String上调用intern()会将其从堆中移出到永久生成中,并且您可能会耗尽PermGen空间.

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


从JDK 7(我的意思是在HotSpot中),有些东西发生了变化.

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

- 来自Java SE 7的功能和增强功能

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

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

mas*_*lan 64

你得到了一些"吸引人的采访"问题

String s1 = "testString";
String s2 = "testString";
if(s1 == s2) System.out.println("equals!");
Run Code Online (Sandbox Code Playgroud)

如果你应该比较你应该使用的字符串equals().上面会打印等于,因为testString被媒体链接实习编译器为你.您可以使用intern方法自己实习字符串,如前面的答案所示....

  • 您的示例很棘手,因为即使使用“等于”方法,也将得到相同的打印结果。您可能想添加一个新的String()比较,以更清楚地显示区别。 (2认同)

Cir*_*四事件 39

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"; }
Run Code Online (Sandbox Code Playgroud)

和编译单位:

package other;
public class Other { public static String hello = "Hello"; }
Run Code Online (Sandbox Code Playgroud)

产生输出:

true true true true false true
Run Code Online (Sandbox Code Playgroud)

JVMS

JVMS 7 5.1表示,使用专用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"
Run Code Online (Sandbox Code Playgroud)

为了派生字符串文字,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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我们有恒定的池:

#2 = String             #32   // abc
[...]
#32 = Utf8               abc
Run Code Online (Sandbox Code Playgroud)

并且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
Run Code Online (Sandbox Code Playgroud)

请注意:

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

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

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

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

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

结论:对字符串池有直接的字节码支持,并且内存表示是有效的.

额外:将其与整数池进行比较,整数池没有直接的字节码支持(即没有CONSTANT_String_info模拟).


Ham*_*ada 20

由于字符串是对象,并且由于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 
Run Code Online (Sandbox Code Playgroud)

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

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

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
Run Code Online (Sandbox Code Playgroud)

OCP Java SE 11 程序员,Deshmukh


ngu*_*ntt 17

Java 8或更新的更新.在Java 8中,PermGen(永久生成)空间被删除并由Meta Space替换.String池内存被移动到JVM堆.

与Java 7相比,堆中的String池大小增加.因此,内置字符串有更多空间,但整个应用程序的内存较少.

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

我们来看看这段代码:

String value1 = "70";
String value2 = "70";
String value3 = new Integer(70).toString();
Run Code Online (Sandbox Code Playgroud)

结果:

value1 == value2 --->真的

value1 == value3 --->假

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

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

这就是你应该使用' equals'来比较2个String对象的原因.这intern()就是有用的.