什么是 Java String 实习?

What is Java String interning?

提问人:saplingPro 提问时间:5/14/2012 最后编辑:HearensaplingPro 更新时间:11/17/2023 访问量:142573

问:

什么是 Java 中的字符串实习,我应该在什么时候使用它,为什么

java 字符串 string-interning

评论

0赞 zeller 5/14/2012
请参阅何时应该使用 String 的 intern 方法?
2赞 Asanka Siriwardena 4/1/2016
如果那么String a = new String("abc");String b = new String("abc");a.intern() == b.intern()
0赞 Ronak Poriya 11/10/2016
Checkout String 实习示例:algs4.cs.princeton.edu/12oop/MutableString.java.html
0赞 AlikElzin-kilaka 6/3/2018
是否取决于,也就是说,不同的类加载器是否创建了“不同的”s,从而导致了不同的s?String.intern()ClassLoaderStringintern
1赞 Holger 2/4/2020
@AlikElzin-kilaka 不,类加载器与字符串实习完全无关。下次您有问题时,请打开一个新问题,而不是将其作为对其他问题的评论发布。

答:

289赞 Ashwinee K Jha 5/14/2012 #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 特性和增强功能

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

评论

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

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

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

如果要比较字符串,则应使用 .上面将打印 equals,因为编译器已经为您嵌入了 。您可以使用 intern 方法自己练习字符串,如之前的答案所示。equals()testString

评论

9赞 giannis christofakis 3/31/2019
您的示例很棘手,因为即使您使用该方法,也会导致相同的打印。您可能需要添加比较以更清楚地显示区别。equalsnew String()
1赞 Deepak Selvakumar 4/16/2020
@giannischristofakis但是如果我们使用 new String(),== 不会失败吗?java 是否也会自动内化新的字符串?
0赞 maslan 4/16/2020
@giannischristofakis当然,如果你使用 new String(),它会在 == 上失败。但新的字符串(...)。intern() 不会在 == 上失败,因为 intern 将返回相同的字符串。简单假设编译器正在文本中执行新的 String().intern
51赞 Ciro Santilli OurBigBook.com 2/12/2016 #3

JLS系列

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

此外,字符串文本始终引用类 String 的同一实例。这是因为字符串文本(或者更一般地说,作为常量表达式 (§15.28) 值的字符串)是“intered”的,以便使用 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 说,interning 是用一个专用的结构体神奇而高效地实现的(不像大多数其他对象,它们具有更通用的表示形式):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

注意如何:

  • 0并且:加载相同的常量(文字)3ldc #2
  • 12:创建一个新的字符串实例(带有 as 参数)#2
  • 35:并作为常规对象与acif_acmpne

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

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

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

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

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

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

2赞 Robin Gupta 9/24/2017 #4

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

我有 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

评论

3赞 user207421 10/30/2017
它比这更强大。同一类加载器加载的任何 String 文字都将引用同一 String。请参阅 JLS 和 JVM 规范。
1赞 Holger 2/4/2020
@user207421事实上,它甚至与字符串文字属于哪个类加载器无关。
0赞 user207421 11/17/2023
事实上,它是 JVM 的优化,而不是编译器的优化。编译器执行字符串池化,而 JLS 说它通过 进行字符串池化,这很难相信,但您可以随时调用自己,这与编译器完全无关。String.intern()String.intern()
30赞 nguyentt 8/4/2018 #5

Java 8+ 更新

在 Java 8 中,PermGen(永久生成)空间被移除并被元空间取代。字符串池内存被移动到 JVM 的堆中。

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

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

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

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

结果:

value1 == value2--->真的

value1 == value3---> false

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

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

这就是为什么你应该使用 '' 来比较 2 个 String 对象。这就是有用的方式。equalsintern()

18赞 Hamza Assada 3/8/2020 #6

由于字符串是对象,并且 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 == str3 和 str3 == str4 返回 false 的原因。 事实上,当你执行 new String(“hello”) 时,如果这是第一次在程序的任意位置使用字符串 “hello”,则会创建两个 String 对象,而不仅仅是一个 - 一个在字符串池中,因为使用了带引号的字符串,另一个在常规堆空间中,因为使用了 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

2赞 Rohan Kshirsagar 6/1/2020 #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.
0赞 Saroj 6/11/2021 #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
}
}

实习生流程图

评论

1赞 user207421 11/17/2023
什么是SCP?请不要发布图片链接。如果可以的话,请不要发布图片,当然也不要通过链接发布。