什么是Java String实习?


234

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



2
如果是的String a = new String("abc"); String b = new String("abc");a.intern() == b.intern()
Asanka Siriwardena


是否String.intern()取决于ClassLoader,意思是,不同的类加载器是否会创建“不同的” String,从而导致不同intern的?
AlikElzin-kilaka

1
@ AlikElzin-kilaka不,类加载器与字符串实习完全无关。下次您有问题时,请打开一个新问题,而不是将其发布为其他问题的评论。
Holger

Answers:


233

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

-从Java SE 7功能和增强功能

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


1
“但是请注意,缓存是由JVM在永久内存池中维护的,该内存通常受大小限制……”您能解释一下吗?我听不懂
saplingPro's

2
“ interned”字符串存储在JVM中的特殊内存区域中。此内存区域通常具有固定大小,并且不属于存储其他数据的常规Java堆的一部分。由于大小固定,可能会在这个永久内存区域中填满所有字符串,从而导致难看的问题(无法加载类和其他内容)。
大提琴

@cello那么,它类似于缓存吗?
saplingPro 2012年

8
@grassPro:是的,这是一种缓存,是JVM本身提供的一种缓存。值得注意的是,由于Sun / Oracle JVM和JRockit的合并,JVM工程师试图摆脱JDK 8(openjdk.java.net/jeps/122)中的永久内存区域,因此不会将来有任何尺寸限制。
大提琴

9
程序员还应该意识到,字符串实习可能会带来安全隐患。如果内存中有诸如字符串之类的敏感文本(例如密码),即使很久以来实际的字符串对象都已被GC处理,它可能会在内存中保留很长时间。如果坏家伙以某种方式获得对内存转储的访问权限,那可能会很麻烦。即使没有实习也存在此问题(因为GC不确定以此类推开始),但它会使情况变得更糟。最好使用char[]而不是String用于敏感文本,并在不再需要它时将其归零。
克里斯

71

有一些“吸引人的面试”问题,例如为什么获得平等!如果执行以下代码。

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

如果要比较字符串,则应使用equals()。因为上面会打印平等testString已经实习编译器为你。您可以使用intern方法自己实习字符串,如先前的答案所示。


5
您的示例比较棘手,因为即使使用该equals方法,也将得到相同的打印结果。您可能需要添加new String()比较以更清楚地显示区别。
giannis christofakis

@giannischristofakis,但是如果我们使用新的String(),==会失败吗?java是否也会自动内部化新的字符串?
Deepak Selvakumar,

@giannischristofakis当然,如果您使用新的String(),它将在==上失败。但是new String(...)。intern()不会在==上失败,因为intern将返回相同的字符串。简单地假设编译器正在按字面
意义

42

JLS

JLS 7 3.10.5对其进行了定义并给出了一个实际示例:

而且,字符串文字总是引用类String的相同实例。这是因为使用方法String.intern,对“字符串文字”(或更一般而言,是作为常量表达式的值的字符串(第15.28节)进行“插入”),以便共享唯一的实例。

示例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 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"

为了派生字符串文字,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


19

Java 8或更新版本。在Java 8中,PermGen(永久生成)空间被删除并由Meta Space代替。字符串池内存将移至JVM的堆。

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

还有一件事情,您已经知道,在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'比较两个String对象。这intern()就是有用的方法。


2

字符串实习是编译器的一种优化技术。如果在一个编译单元中有两个相同的字符串文字,那么生成的代码将确保在程序集中为该文字的所有实例(双引号中的字符)创建一个字符串对象。

我来自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:对象通过引用进行比较。

注意2:typeof(int).Name是通过反射方法求值的,因此在编译时不会求值。这些比较是在编译时进行的。

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

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

3)否,因为str2和obj没有相同的文字。见注2


3
比这更强大。由相同的类加载器加载的任何String文字都将引用相同的String。请参阅JLS和JVM规范。
罗恩侯爵

1
实际上,@ user207421甚至与字符串文字所属的类加载器无关。
Holger

1
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.

0

从OCP Java SE 11程序员Deshmukh的书中,我找到了最简单的Interning解释,其解释如下:由于字符串是对象,并且由于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的原因。实际上,当您执行新的String(“ hello”);时, 如果这是第一次在程序中的任何地方使用字符串“ hello”,则将创建两个String对象,而不是一个对象-一个在字符串池中(因为使用了带引号的字符串),另一个在常规堆空间中,因为新关键字的使用。

字符串池是Java通过避免创建多个包含相同值的String对象来节省程序内存的方法。通过使用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
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.