什么是Java字符串实习?

什么是字符串实习在Java中,当我应该使用它,和为什么?

136219 次浏览

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()具有移动的效果 它从堆中取出到永久代中,你有风险

.

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

从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

有一些“吸引人的面试”问题,比如如果你执行下面的代码段,为什么会得到= !

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

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

JLS

JLS 7 3.10.5定义了它,并给出了一个实际的例子:

而且,string字面值总是引用string类的同一个实例。这是因为字符串字面量——或者更一般地说,是常量表达式的值的字符串(§15.28)——被“internned”,以便使用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

jvm

JVMS 7 5.1说表示使用专用的CONSTANT_String_info结构体神奇而有效地实现了实习(不像大多数其他对象具有更通用的表示):

string字面值是类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.intern之前在包含与CONSTANT_String_info结构所给出的相同的Unicode代码点序列的String类实例上调用过,则字符串文字派生的结果是对相同的String类实例的引用。

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

字节码

让我们反编译一些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: ac作为常规对象与if_acmpne进行比较

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

上面的jvm引用似乎说,只要Utf8所指向的是相同的,那么ldc就会加载相同的实例。

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

  • static final String s = "abc"通过ConstantValue属性指向常量表
  • 非final字段没有这个属性,但仍然可以用ldc初始化

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

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

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

我来自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 !?

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

注2: typeof (int)。名称由反射方法求值,因此在编译时不会求值。这里这些比较是在编译时进行的。

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

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

3) FALSE,因为str2和obj没有相同的字面值。看到注2

< p > 针对Java 8或以上版本进行更新。 在Java 8中,永久生成(Permanent Generation)空间被移除,并被元空间(Meta space)取代。String池内存被移动到JVM的堆中。< / p >

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

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

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

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

结果:

value1 == value2——> true

value1 == value3——> false

value1.equals(value3)——> true

value1 == value3.intern()——> true

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

因为字符串是对象,而且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。 事实上,当你执行新的字符串(“hello");时,会创建两个String对象,而不是一个,如果这是字符串&;hello&;在程序中的anywhere中使用-一个在字符串池中,因为使用了带引号的字符串,另一个在常规堆空间中,因为使用了new关键字

字符串池是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

OCP Java SE 11程序员,Deshmukh

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.

通过使用堆对象引用,如果我们想要对应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
}
}

实习生流程图