1. String的基本特性

  • String:字符串,使用一对 “” 引起来表示
    • String s1 = “hello”;
    • String s2 = new String(“hello”);
  • String:声明为final的,不可被继承
  • String 实现了Serializable接口:表示字符串是支持序列化的,实现了Comparable接口:表示String可以比较大小
  • 在JDK8以及之前,String内部定义了final char[] value 用来存储字符串数据,JDK9之后使用 byte[]

结论:String再也不用char[] 来存储了,改成了byte [] 加上编码标记,节约了一些空间

  • String:代表不可变的字符序列。简称:不可变性
    • 当对字符串重新赋值的时候,需要重写指定内存区域赋值,而不是修改原有的value进行赋值
    • 不能对现有的字符串进行连接操作,也需要重新指定内存区域,不是修改原有的value进行赋值
    • 当调用String的replace()方法修改指定字符或者字符串的时候,也需要重新指定内存区域赋值,不能使用原有的value赋值
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串声明在常量池中

  • 字符串常量池中是不会存储相同内容的字符串的

  • String的String Pool 是一个固定大小的HashTable 默认长度为1009,如果放进去String Pool 的String非常多,就会造成Hash冲突严重,从而导致链表很长,而链表长了之后,会直接影响调用 String.intern()的性能
  • 使用 -XX:StringTableSize 可设置StringTable的长度
  • 在JDK6中的StringTable是固定的,就是1009的长度,如果字符串常量池中的字符串过多就会导致效率下降很快,StirngTableSize设置没有要求
  • 在JDK7中,StringTable的默认长度为 60013,JDK7没有要求
  • JDK8开始1009 是可设置的最小值
  • Stirng#intern()方法:如果字符串常量池中没有对应的字符串的话,就添加在常量池

    2. String的内存分配

  • 在Java语言中有8种基本数据类型和String,这些类型为了使它们在运行速度中更快,更节省内存。都提供了常量池

  • 常量池就相似于一个Java系统级别提供的缓存。8中基本类型都是系统协调的,String类型的常量池比较特殊,主要的使用方法有2种
    • 直接使用双引号声明String的对象会直接存储在常量池种
    • 如果不是使用双引号声明的String对象,可以使用String提供的intern() 方法
  • 在JDK6以及以前,字符串常量池存放在永久代
  • JDK7种Oracle对字符串池的逻辑做了很大的改变,将 字符串常量池的位置调整到Java堆内
    • 所有的字符串都保存在堆中,和其他普通对象一样,这样可以让你在进行调优的时候仅仅需要调整堆大小就可以了
    • 字符串常量池的概念原本使用的比较多,但是这个改动使得我们有足够的理由考虑使用String.intern();
  • Java8元空间,字符串常量在堆
  • StringTable为什么需要调整
    • 之前的永久代比较小,放大量的字符串,会占用很大的空间
    • 永久代垃圾回收的频率很低

3. String的基本操作

  • Java语言规范要求完全相同的字符字面常量,应该包含同样的Unicode字符序列,并且必须是指向同一个String类实例 ```java public class Memory { public static void main(String[] args) {

    1. int i = 1;
    2. Object obj = new Object();
    3. Memory mem = new Memory();
    4. mem.foo(obj);

    }

    public void foo(Object param) {

    1. String str = param.toString();
    2. System.out.println(str);

    } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27187548/1660373919507-ede4b245-4bd8-4da5-aa94-80463ba0b775.png#clientId=ub7c3e170-f547-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u1eb9eb7f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=461&originWidth=1077&originalType=url&ratio=1&rotation=0&showTitle=false&size=117697&status=done&style=none&taskId=u0fc26116-32ec-461a-8db5-795b2d4e4fe&title=)
  2. <a name="nzSGV"></a>
  3. ### 4. 字符串拼接操作
  4. - **常量与常量**的拼接结果在常量池,原理是编译期优化
  5. - 常量池中不会存在相同内容的常量
  6. - **只要其中有一个是变量,结果就在堆中**。变量拼接的原理是StringBuilder
  7. - 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象的地址
  8. <a name="HDmo3"></a>
  9. #### 拼接操作的原理
  10. - 实例代码
  11. ```java
  12. public class CreateStringDemo {
  13. public static void main(String[] args) {
  14. String str = "hello ";
  15. String str2 = "world";
  16. String s = str + str2;
  17. System.out.println(s);
  18. }
  19. }
  • javap 反编译之后的字节码

    1. 0 ldc #2 <hello >
    2. 2 astore_1
    3. 3 ldc #3 <world>
    4. 5 astore_2
    5. 6 new #4 <java/lang/StringBuilder>
    6. 9 dup
    7. 10 invokespecial #5 <java/lang/StringBuilder.<init>>
    8. 13 aload_1
    9. 14 invokevirtual #6 <java/lang/StringBuilder.append>
    10. 17 aload_2
    11. 18 invokevirtual #6 <java/lang/StringBuilder.append>
    12. 21 invokevirtual #7 <java/lang/StringBuilder.toString>
    13. 24 astore_3
    14. 25 getstatic #8 <java/lang/System.out>
    15. 28 aload_3
    16. 29 invokevirtual #9 <java/io/PrintStream.println>
    17. 32 return
  • String拼接的原理:只要是变量,就先创建一个StringBuilder对象,然后调用StringBuilder的append()方法,分别拼接两个字符串,拼接结束之后,调用StringBuilder的toString()方法,返回为新的字符串

    • 在JDK5.0之前使用的是StringBuffer进行拼接,在JDK5.0之后,StringBuilder出现,然后使用了StringBuilder来拼接
  • 拼接的实际操作如下

    1. StringBuilder sb = new StringBuilder();
    2. sb.append("hello ");
    3. sb.append("world");
    4. sb.toString(); // 类似于 new String("hello world");
  • 字符串拼接操作,不一定是使用的是StringBuilder,如果两边都是final的字符串常量或者常量引用,仍然使用编译器优化,此时不使用StringBuilder的方式

image.png

  • 针对final修饰类、方法、基本数据类型、引用数据类型的量的结果的时候,能使用fianl就使用final

  • 在使用StringBuilder和StringBuffer的时候,如果已知具体的大小,可以进行有参构造函数,创建一个指定初始大小的数组,可以避免返回扩容转移数组,耗费空间和资源

5. intern()的使用

  • 如果不是双引号声明的String对象,可以使用String提供的intern方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在,就会将当前字符串放入常量池中
    • String myInfo = new String(“icanci.cn”).intern();
  • 也就是说,如果在任意字符串上调用String#intern方法,那么其返回结果所指向的哪个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必为true
    • (“a”+”b”+”c”).intern() == “abc”
  • 通俗点讲,Interned String 就是确保字符串在内存中只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意:这个值会被存放在字符串内部池(String Intern Pool)
  • 只要字符串对象调用了 intern() 方法,那么返回值是指向字符串常量池中的数据
  • 实例代码 ```java public class StringInternDemo { public static void main(String[] args) {
    1. String s = new String("1");
    2. s.intern();
    3. String s2 = new String("1");
    4. System.out.println(s == s2);
    5. String s3 = new String("1") + new String("2");
    6. s3.intern();
    7. String s4 = "11";
    8. System.out.println(s3 == s4);
    } }

// 打印结果 // false // false

  1. - **题目**
  2. - new String("ab") 会创建几个对象?
  3. ```java
  4. public class StringNewDemo {
  5. public static void main(String[] args) {
  6. String ab = new String("ab");
  7. }
  8. }
  1. 0 new #2 <java/lang/String>
  2. 3 dup
  3. 4 ldc #3 <ab>
  4. 6 invokespecial #4 <java/lang/String.<init>>
  5. 9 astore_1
  6. 10 return
  • 从上面反编译看出,创建了两个对象,一个放在堆中,一个放在了字符串常量池 ldc指令
  • 如果之前常量池是有的,就字符串的值就直接引用到此常量

  • new String(“a”) + new String(“b”) 创建了几个对象?

    1. public class StringNewDemo {
    2. public static void main(String[] args) {
    3. String ab = new String("b") + new String("b");
    4. }
    5. }
    1. 0 new #2 <java/lang/StringBuilder>
    2. 3 dup
    3. 4 invokespecial #3 <java/lang/StringBuilder.<init>>
    4. 7 new #4 <java/lang/String>
    5. 10 dup
    6. 11 ldc #5 <b>
    7. 13 invokespecial #6 <java/lang/String.<init>>
    8. 16 invokevirtual #7 <java/lang/StringBuilder.append>
    9. 19 new #4 <java/lang/String>
    10. 22 dup
    11. 23 ldc #5 <b>
    12. 25 invokespecial #6 <java/lang/String.<init>>
    13. 28 invokevirtual #7 <java/lang/StringBuilder.append>
    14. 31 invokevirtual #8 <java/lang/StringBuilder.toString>
    15. 34 astore_1
    16. 35 return
  • 创建了哪些对象

    • new StringBuilder()
    • new String(“a”)
    • 常量池中的 “a”
    • new String(“b”)
    • 常量池中的 “b”

  • 深入刨析StringBuilderd的toString()方法
    • 对象 new String(“ab”)
    • 注意:toString()方法的调用,在字符串常量池中没有生成 “ab”
  • 实例代码

    1. public class StringInternDemo {
    2. public static void main(String[] args) {
    3. String s = new String("1");
    4. // 调用此方法之前,字符串常量池已经有了 "1"
    5. s.intern();
    6. // 此时 s 指的是堆空间的地址
    7. // 此时 s2 指向的是常量池中的地址
    8. String s2 = "1";
    9. // JDK6:false
    10. // JDK7/8:false
    11. System.out.println(s == s2);
    12. // 因为 new String("1") + new String("1"); 这个过程没有把"11"放在常量池
    13. // 此时 s3 的地址就是相当于 new String("11"); 字符串常量池没有"11"
    14. String s3 = new String("1") + new String("1");
    15. // 在字符串常量池生成"11"
    16. // 这个"11"如何理解
    17. // 在JDK6中就是创建了一个新的对象
    18. // 在JDK7中,调用 s3.intern(),字符串常量池中 "11"的值就是创建对象的地址
    19. s3.intern();
    20. // 使用的是上一行代码执行时,在常量池中生成的"11" 的地址
    21. String s4 = "11";
    22. // JDK6:false
    23. // JDK7/8:true
    24. System.out.println(s3 == s4);
    25. }
    26. }
    1. 0 new #2 <java/lang/String>
    2. 3 dup
    3. 4 ldc #3 <1>
    4. 6 invokespecial #4 <java/lang/String.<init>>
    5. 9 astore_1
    6. 10 aload_1
    7. 11 invokevirtual #5 <java/lang/String.intern>
    8. 14 pop
    9. 15 ldc #3 <1>
    10. 17 astore_2
    11. 18 getstatic #6 <java/lang/System.out>
    12. 21 aload_1
    13. 22 aload_2
    14. 23 if_acmpne 30 (+7)
    15. 26 iconst_1
    16. 27 goto 31 (+4)
    17. 30 iconst_0
    18. 31 invokevirtual #7 <java/io/PrintStream.println>
    19. 34 new #8 <java/lang/StringBuilder>
    20. 37 dup
    21. 38 invokespecial #9 <java/lang/StringBuilder.<init>>
    22. 41 new #2 <java/lang/String>
    23. 44 dup
    24. 45 ldc #3 <1>
    25. 47 invokespecial #4 <java/lang/String.<init>>
    26. 50 invokevirtual #10 <java/lang/StringBuilder.append>
    27. 53 new #2 <java/lang/String>
    28. 56 dup
    29. 57 ldc #3 <1>
    30. 59 invokespecial #4 <java/lang/String.<init>>
    31. 62 invokevirtual #10 <java/lang/StringBuilder.append>
    32. 65 invokevirtual #11 <java/lang/StringBuilder.toString>
    33. 68 astore_3
    34. 69 aload_3
    35. 70 invokevirtual #5 <java/lang/String.intern>
    36. 73 pop
    37. 74 ldc #12 <11>
    38. 76 astore 4
    39. 78 getstatic #6 <java/lang/System.out>
    40. 81 aload_3
    41. 82 aload 4
    42. 84 if_acmpne 91 (+7)
    43. 87 iconst_1
    44. 88 goto 92 (+4)
    45. 91 iconst_0
    46. 92 invokevirtual #7 <java/io/PrintStream.println>
    47. 95 return

    image.png

  • 练习 ```java public class StringInternDemo2 { public static void main(String[] args) {

    1. // 拓展
    2. // 执行完之后,字符串常量池不存在 "11"
    3. String s3 = new String("1") + new String("1");
    4. // 在字符串常量池生成 "11"
    5. String s4 = "11";
    6. String s5 = s3.intern();
    7. System.out.println(s3 == s4);
    8. System.out.println(s4 == s5);

    } }

// 打印 // false // true

  1. **总结**
  2. - jdk6中,将这个字符串对象尝试放入串池
  3. - 如果串池中有,则不会放入,返回已有的串池中的对象的地址
  4. - 如果没有,**会把此对象复制一份**,放入串池,并返回串池中的对象地址
  5. - jdk7中,将这个字符串对象尝试放入串池
  6. - 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  7. - 如果没有,**会把此对象的引用地址复制一份**,放入串池,并返回串池中引用地址地址
  8. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27187548/1660374434796-61fd46aa-5908-4e23-8b65-db6da8270b55.png#clientId=ub7c3e170-f547-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ubde164c8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=595&originWidth=1151&originalType=url&ratio=1&rotation=0&showTitle=false&size=178721&status=done&style=none&taskId=uc32839dd-7868-48f6-88e9-c74b7c53876&title=)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/27187548/1660374434883-1275fc53-b067-485d-9642-b0eb03f75dc9.png#clientId=ub7c3e170-f547-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u54da140d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=596&originWidth=1101&originalType=url&ratio=1&rotation=0&showTitle=false&size=188105&status=done&style=none&taskId=u84c0cfa3-87a6-407b-95a1-5c585f2f20d&title=)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/27187548/1660374434918-f7a680c2-d68d-4b9b-80a9-f8ace12c0e29.png#clientId=ub7c3e170-f547-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ue8997ebc&margin=%5Bobject%20Object%5D&name=image.png&originHeight=613&originWidth=1139&originalType=url&ratio=1&rotation=0&showTitle=false&size=200069&status=done&style=none&taskId=uef6e27c4-3f27-4b1f-a81c-6093f09fa25&title=)
  9. <a name="iuoJb"></a>
  10. ### 6. intern()的空间效率测试:空间角度
  11. - 实例代码
  12. ```java
  13. public class StringIntern {
  14. static final int MAX_COUNT = 1000 * 10000;
  15. static final String[] arr = new String[MAX_COUNT];
  16. public static void main(String[] args) {
  17. Integer[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 1};
  18. long start = System.currentTimeMillis();
  19. for (int i = 0; i < MAX_COUNT; i++) {
  20. // 花费的时间:2815 ms String 实例 1000W
  21. // arr[i] = new String(String.valueOf(data[i % data.length]));
  22. // 花费的时间:1304 ms
  23. arr[i] = new String(String.valueOf(data[i % data.length])).intern();
  24. }
  25. long end = System.currentTimeMillis();
  26. System.out.println("花费的时间:" + (end - start) + " ms");
  27. try {
  28. Thread.sleep(1000000);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }

image.png
image.png

  • 对于程序中大量的重复的字符串,可以使用intern() 方法节省空间
  • 大的社交网站,需要内存中存储大量的字符串。如果都调用 intern() 方法,就会明显降低内存的大小

7. StringTable的垃圾回收

  1. public class StringGCTest {
  2. public static void main(String[] args) {
  3. for (int i = 0; i < 100; i++) {
  4. String.valueOf(i).intern();
  5. }
  6. }
  7. }
  • 控制台结果 ```java Heap PSYoungGen total 4608K, used 2079K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000) eden space 4096K, 50% used [0x00000000ffb00000,0x00000000ffd07f58,0x00000000fff00000) from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) to space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000) ParOldGen total 11264K, used 0K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000) object space 11264K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ffb00000) Metaspace used 3494K, capacity 4498K, committed 4864K, reserved 1056768K class space used 387K, capacity 390K, committed 512K, reserved 1048576K SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 14191 = 340584 bytes, avg 24.000 Number of literals : 14191 = 604960 bytes, avg 42.630 Total footprint : = 1105632 bytes Average bucket size : 0.709 Variance of bucket size : 0.713 Std. dev. of bucket size: 0.844 Maximum bucket size : 6 StringTable statistics: Number of buckets : 60013 = 480104 bytes, avg 8.000 Number of entries : 1876 = 45024 bytes, avg 24.000 Number of literals : 1876 = 163400 bytes, avg 87.100 Total footprint : = 688528 bytes Average bucket size : 0.031 Variance of bucket size : 0.031 Std. dev. of bucket size: 0.177 Maximum bucket size : 2

Process finished with exit code 0

  1. - 上述的情况没有发生GC
  2. - 实例代码 增大数据量
  3. ```java
  4. public class StringGCTest {
  5. public static void main(String[] args) {
  6. for (int i = 0; i < 100000; i++) {
  7. String.valueOf(i).intern();
  8. }
  9. }
  10. }
  • 执行结果 ```java [GC (Allocation Failure) [PSYoungGen: 4096K->488K(4608K)] 4096K->696K(15872K), 0.0063979 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] Heap PSYoungGen total 4608K, used 3802K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000) eden space 4096K, 80% used [0x00000000ffb00000,0x00000000ffe3c950,0x00000000fff00000) from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000) to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000) ParOldGen total 11264K, used 208K [0x00000000ff000000, 0x00000000ffb00000, 0x00000000ffb00000) object space 11264K, 1% used [0x00000000ff000000,0x00000000ff034000,0x00000000ffb00000) Metaspace used 3497K, capacity 4498K, committed 4864K, reserved 1056768K class space used 387K, capacity 390K, committed 512K, reserved 1048576K SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, avg 8.000 Number of entries : 14191 = 340584 bytes, avg 24.000 Number of literals : 14191 = 604960 bytes, avg 42.630 Total footprint : = 1105632 bytes Average bucket size : 0.709 Variance of bucket size : 0.713 Std. dev. of bucket size: 0.844 Maximum bucket size : 6 StringTable statistics: Number of buckets : 60013 = 480104 bytes, avg 8.000 Number of entries : 61367 = 1472808 bytes, avg 24.000 Number of literals : 61367 = 3495800 bytes, avg 56.965 Total footprint : = 5448712 bytes Average bucket size : 1.023 Variance of bucket size : 0.808 Std. dev. of bucket size: 0.899 Maximum bucket size : 5

Process finished with exit code 0 ```

8. G1的String去重操作

  • 背景:对许多Java应用,(有大有小)做的测试结果得出以下结果
    • 堆存活数据集合里面的String对象占用了25%
    • 堆存活数据集合里面重复的对象有13.5%
    • String的平均长度时45
  • 许多大规模的Java应用瓶颈在于内存,Java堆中存活的数据集合差不多25是String对象,但是有一半是重复的,浪费了空间,所以有了G1对String的去重操作