一、提纲

  • String 为什么不可变?
  • String a=new String(“A”)在内存里创建多少个对象?
  • 字符串常量池的变化(Java7前后)
  • String intern 方法

二、String 为什么定义为不可变?

1. String常量池的要求

字符串常量池是一个特殊的存储空间在堆中,当字符串被创建时,如果常量池中存在该字符串,则把该字符串的引用返回,而不会重新创建

  1. //以下语句,只会在常量池中创建abcd 一个对象
  2. String string1 = "abcd";
  3. String string2 = "abcd";
  4. System.out.println(string1 == string2); //true

image.png

2. 可以使用String来缓存HashCode

3. 安全

String被广泛用作许多java类的参数,例如网络连接、打开文件等。如果字符串不是不可变的,连接或文件将被更改,并导致严重的安全威胁。该方法认为它连接到一台机器上,但实际上并没有。可变字符串也可能导致反射中的安全问题,因为参数是字符串。

三、String a=new String(“A”)在内存里创建多少个对象?

使用new String(“A”) 会在堆中创建一个对象,如果常量池中没有A对象,则创建一个A对象在常量池中
image.png

反汇编查看创建情况

  1. public class Test {
  2. public static void main(String[] args) {
  3. String a=new String("A");
  4. }
  5. }

先编译java文件 javac Test.java
再反汇编class文件,输出内容到Test.javap中: 

  1. javap -v Test.class > Test.javap

或者 直接输出控制台:

  1. javap -v Test.class

结果

  1. Classfile /home/hdj/IdeaProjectsC/demo/demo-java-core/src/main/java/cn/hdj/String/Test.class
  2. Last modified Nov 5, 2019; size 344 bytes
  3. MD5 checksum e800b007fd8cad4e386d512e5e4f7504
  4. Compiled from "Test.java"
  5. public class cn.hdj.String.Test
  6. minor version: 0
  7. major version: 52
  8. flags: ACC_PUBLIC, ACC_SUPER
  9. Constant pool: //常量池
  10. #1 = Methodref #6.#15 // java/lang/Object."<init>":()V
  11. #2 = Class #16 // java/lang/String
  12. #3 = String #17 // A  
  13. #4 = Methodref #2.#18 // java/lang/String."<init>":(Ljava/lang/String;)V
  14. #5 = Class #19 // cn/hdj/String/Test
  15. #6 = Class #20 // java/lang/Object
  16. #7 = Utf8 <init>
  17. #8 = Utf8 ()V
  18. #9 = Utf8 Code
  19. #10 = Utf8 LineNumberTable
  20. #11 = Utf8 main
  21. #12 = Utf8 ([Ljava/lang/String;)V
  22. #13 = Utf8 SourceFile
  23. #14 = Utf8 Test.java
  24. #15 = NameAndType #7:#8 // "<init>":()V
  25. #16 = Utf8 java/lang/String
  26. #17 = Utf8 A //常量池中的A对象
  27. #18 = NameAndType #7:#21 // "<init>":(Ljava/lang/String;)V
  28. #19 = Utf8 cn/hdj/String/Test
  29. #20 = Utf8 java/lang/Object
  30. #21 = Utf8 (Ljava/lang/String;)V
  31. {
  32. public cn.hdj.String.Test();
  33. descriptor: ()V
  34. flags: ACC_PUBLIC
  35. Code:
  36. stack=1, locals=1, args_size=1
  37. 0: aload_0
  38. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  39. 4: return
  40. LineNumberTable:
  41. line 9: 0
  42. public static void main(java.lang.String[]);
  43. descriptor: ([Ljava/lang/String;)V
  44. flags: ACC_PUBLIC, ACC_STATIC
  45. Code:
  46. stack=3, locals=2, args_size=1
  47. 0: new #2 // class java/lang/String //创建String对象
  48. 3: dup
  49. 4: ldc #3 // String A //加载常量池中的A(此时常量池和堆中都会存字符串对象)
  50. 6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
  51. 9: astore_1
  52. 10: return
  53. LineNumberTable:
  54. line 12: 0
  55. line 13: 10
  56. }
  57. SourceFile: "Test.java"

四、常量池在Java7后的改变

Area: HotSpot Standard/Platform: JDK 7 Synopsis: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and lessw data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the String.intern() method will see more significant differences. RFE: 6962931

在JDK7之前,所有的interned strings都存储在PermGen(永久代)中——堆中一个固定大小的区域,主要用来存储加载了的类和字符串常量池。而在JDK7后,interned strings 不再存储在PermGen(永久代),而是与应用程序创建的其他对象一起分配在 Java 堆(称为年轻代和老年代)的主要部分中

五、String intern 方法

  1. String.intern方法究竟做了什么?

当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(用 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用

注意:

  • 在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回是这个字符串实例在永久代存储的引用
  • 在 JDK 7中,intern()方法实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到Java堆中,那只需要在常量池里记录一下首次出现的实例引用,返回的引用与堆中字符串的实例引用相同
  • 还有所有的字符串字面量和字符串值常量表达式都会被存入字符串常量池中

image.png


  1. native方法

/home/hdj/ClionProjects/jdk13/openjdk/src/java.base/share/native/libjava/String.c

  1. Java_java_lang_String_intern(JNIEnv *env, jobject this)
  2. {
  3. return JVM_InternString(env, this);
  4. }

/home/hdj/ClionProjects/jdk13/openjdk/src/hotspot/share/include/jvm.h

  1. /*
  2. * java.lang.String
  3. */
  4. JNIEXPORT jstring JNICALL
  5. JVM_InternString(JNIEnv *env, jstring str);

/home/hdj/ClionProjects/jdk13/openjdk/src/hotspot/share/prims/jvm.cpp

  1. // String support ///////////////////////////////////////////////////////////////////////////
  2. JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  3. JVMWrapper("JVM_InternString");
  4. JvmtiVMObjectAllocEventCollector oam;
  5. if (str == NULL) return NULL;
  6. oop string = JNIHandles::resolve_non_null(str);
  7. oop result = StringTable::intern(string, CHECK_NULL);
  8. return (jstring) JNIHandles::make_local(env, result);
  9. JVM_END

/home/hdj/ClionProjects/jdk13/openjdk/src/hotspot/share/classfile/stringTable.cpp

  1. oop StringTable::intern(Handle string_or_null_h, const jchar* name, int len, TRAPS) {
  2. // shared table always uses java_lang_String::hash_code
  3. unsigned int hash = java_lang_String::hash_code(name, len);
  4. oop found_string = lookup_shared(name, len, hash);
  5. if (found_string != NULL) {
  6. return found_string;
  7. }
  8. if (_alt_hash) {
  9. hash = hash_string(name, len, true);
  10. }
  11. found_string = do_lookup(name, len, hash);
  12. if (found_string != NULL) {
  13. return found_string;
  14. }
  15. return do_intern(string_or_null_h, name, len, hash, CHECK_NULL);
  16. }
  1. 实际例子了解

example1_代码

  1. public static void testCase5() {
  2. // S1 引用的堆中的对象
  3. String s1 = new String("Java");
  4. // S2 引用在常量池中的Java
  5. String s2 = s1.intern();
  6. // == 比较两个对象的内存地址
  7. // s1 在堆中
  8. // s2 在常量池中
  9. //结果false
  10. System.out.println(s1 == s2);
  11. // 比较两个的值
  12. //true
  13. System.out.println(s1.equals(s2));
  14. // 因为常量池中已存在Java 对象,s3直接引用
  15. String s3 = "Java";
  16. //s2, s3 引用同一个对象
  17. //内存地址相等
  18. //结果true
  19. System.out.println(s2 == s3);
  20. }

图示:
image.png

example2_代码

  1. public static void testCase6(){
  2. //在堆中创建String 对象
  3. //把"1"对象放到字符串常量池中
  4. //s引用堆中String 对象
  5. String s = new String("1");
  6. //去常量池中寻找后发现 “1” 已经在常量池里了
  7. s.intern();
  8. String s2 = "1";
  9. System.out.println(s == s2);//false
  10. //jvm 会优化使用StringBuilder 进行拼接
  11. //最终会在堆中创建String 对象
  12. //也在字符串常量池中创建"1", 但还没有创建"11"
  13. String s3 = new String("1") + new String("1");
  14. //情况一
  15. //去常量池中寻找 "11"对象, 没有创建并将s3 指向"11"的引用
  16. s3.intern();
  17. String s4 = "11";
  18. //现在s3 、s4都指向常量池中的"11"
  19. System.out.println(s3 == s4); //true
  20. //情况二
  21. //String s4 = "11";
  22. //在s4赋值后调用s3.intern(),此时在常量池中已经存在了对象"11",
  23. //所以返回该对象"11",没有改变s3的引用
  24. //s3.intern();
  25. //System.out.println(s3 == s4); //false
  26. }

图示
image.png

参考