目录

背景

String基本特性

不可变性

值传递

String的内存分配

String的基本操作

字符串拼接操作

intern()的使用

StringTable的垃圾回收

G1中的String去重操作

结语

背景

学了半天JVM,是时候复习一下String了

String基本特性

String是字符串final类,不可被继承;实现了Serializable接口和Comparable接口,表示可序列化和可比较大小

jdk8及以前内部定义了final char[]来存储字符串,jdk9改用final byte[];改变的原因是堆中的String对象主要是拉丁字符,这些使用1个字节就足够了,所以String内部改用byte[],同时加入了编码标记,以针对中文等复杂文字进行适配。同样的改变也在StringBuffer、StringBuilder等字符串类中发生

不可变性

String s1 = "szc";
String s2 = "szc";System.out.println(s1 == s2); // true,
// 因为String内部覆写了compare()方法,实现了逐字符比较;而且s1和s2都指向的是同一个常量池对象System.out.println(s1.hashCode()); // 114396
s1 += "is"; // 同样的还有重新赋值、replace()
System.out.println(s1.hashCode()); // 109937926

从拼接前后的s1哈希码不一样,可见对象不一样。

值传递

以下代码说明String传参是值传递

public class StringTest1 {private String str = "szc";private char[] ch = {'1', '2', '3'};public void change(String str, char[] ch) {str = "sss"; // str的哈希和this.str的哈希不一样ch[0] = 'a';}public static void main(String[] args) {StringTest1 test = new StringTest1();test.change(test.str, test.ch);System.out.println(test.str); // szcSystem.out.println(test.ch); // a23}
}

字符串字面量存储在常量池中,是不会存储相同的字符串的。

String Pool是一个固定大小的哈希表,即数组+链表,默认大小为60013(jdk7及以后),长度变大的原因是减少链表长度,提高效率,jdk8中可设置最小值为1009,可通过-XX:StringTableSize来设置。

String的内存分配

字符串常量池和字符串对象都在堆中

1)、直接用双引号声明出来的String对象会直接存在常量池中,比如String info = "szc";

2)、intern()方法

StringTable放在堆中的原因:

1)、jdk6中字符串常量池是在永久代里,这块区域比较小

2)、永久代回收频率很低,不利于释放内存

String的基本操作

用下面的例子证明字符串常量池里不会添加重复的字符串

public class StringTest2 {public static void main(String[] args) {System.out.println("1");System.out.println("2");System.out.println("3");System.out.println("1");System.out.println("2");System.out.println("3");}
}

在执行第一行System.out.println("1");前,常量池里总共有2513个字面量

执行完第一行System.out.println("1");后,最增加两个:换行和"1"

执行完第一个System.out.println("2");后,最增加一个:"2"

执行完第一个System.out.println("3");后,最增加一个:"3"

再执行下面的分别输出123,就不会有新的字符串存储了

以下例子说明栈对象、堆对象和常量池对象之间的关系

public class Memory {public static void main(String[] args) {int i = 1;Object obj = new Object();Memory mem = new Memory();mem.foo(obj);}private void foo(Object param) {String str = param.toString();System.out.println(str);}
}

结构图如下

可见toString()方法在字符串池中创建了个字符串对象,然后foo()方法中的str指向这个对象

字符串拼接操作

常量与常量的拼接结果在常量池,原理是编译期优化

常量池中不会存在相同内容的常量

只要拼接时其中有一个是变量,结果就在堆中,其拼接原理是StringBuilder

如果拼接结果调用inter()方法,并且此字符串内容还不在常量池中,则主动将其放入池中,并返回此对象地址

案例1:

public class StringTest3 {public static void main(String[] args) {String s1 = "a" + "b" + "c"; // a + b + c在编译期就被优化为abcString s2 = "abc";System.out.println(s1 == s2); // true}
}

案例2:

public class StringTest3 {public static void main(String[] args) {String s2 = "abc";System.out.println(s1 == s2);String s3 = "a";String s4 = "b";String s5 = "c";String s6 = s3 + "bc"; // 拼接有一个变量,结果就在堆中新建一个String对象,内容为拼接后的结果String s7 = "a" + s4 + "c";String s8 = "ab" + s5;System.out.println(s1 == s6); // falseSystem.out.println(s1 == s7); // falseSystem.out.println(s1 == s8); // falseSystem.out.println(s6 == s8); // falseString s9 = s8.intern(); // intern()方法在常量池创建新的字面量对象,或者复用已有的,然后返回对象地址System.out.println(s2 == s9); // 由于s8的字面量abc已经有了,所以返回的s9的地址就是s2的地址,故而输出为true}
}

案例3:

public static void f() {String s1 = "a";String s2 = "b";String s3 = "ab";String s4 = s1 + s2;System.out.println(s3 == s4);
}

对应字节码

0 ldc #5 <a>
2 astore_0
3 ldc #6 <b>
5 astore_1
6 ldc #13 <ab>
8 astore_2
9 new #8 <java/lang/StringBuilder>
12 dup
13 invokespecial #9 <java/lang/StringBuilder.<init>>
16 aload_0
17 invokevirtual #10 <java/lang/StringBuilder.append>
20 aload_1
21 invokevirtual #10 <java/lang/StringBuilder.append>
24 invokevirtual #12 <java/lang/StringBuilder.toString>
27 astore_3
28 getstatic #3 <java/lang/System.out>
31 aload_2
32 aload_3
33 if_acmpne 40 (+7)
36 iconst_1
37 goto 41 (+4)
40 iconst_0
41 invokevirtual #4 <java/io/PrintStream.println>
44 return

当字符串拼接里出现变量时,都会先创建一个StringBuilder(字节码第9行),然后拼接操作实际是调用StringBuilder的append()方法(字节码第17行、21行),这里是分别append了个a,append了个b,最后调用StringBuilder的toString(字节码第24行),≈ new String("ab"),所以最后的输出为false。最后jdk5之后用的是StringBuilder,jdk5及之前用的是StringBuffer

拼接操作的效率比append()方法要低很多,因为每拼接一次都要创建新的StringBuilder和新的String,而append()方法不会,可以通过在构造StringBuilder对象时传入字符串长度的上限值来进一步优化,以免对字符数组的多次扩容。

注意,这里的常量包括final对象

案例4:

public static void g() {final String s1 = "a";final String s2 = "b";String s3 = "ab";String s4 = s1 + s2;System.out.println(s3 == s4); // true
}

对应字节码

0 ldc #5 <a>
2 astore_0
3 ldc #6 <b>
5 astore_1
6 ldc #13 <ab>
8 astore_2
9 ldc #13 <ab>
11 astore_3
12 getstatic #3 <java/lang/System.out>
15 aload_2
16 aload_3
17 if_acmpne 24 (+7)
20 iconst_1
21 goto 25 (+4)
24 iconst_0
25 invokevirtual #4 <java/io/PrintStream.println>
28 return

从字节码第6、8、9行可见程序对String s4 = s1 + s2也使用了编译期优化

intern()的使用

此方法在jdk8中的核心描述如下所述

A pool of strings, initially empty, is maintained privately by the class String.

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.

大意就是此方法被调用时,如果常量池包含此字符串对象的字面值,就会把池中的字面值对象返回;如果不包含,就把新的字面值加入池中,返回新的字面量对象的引用。对于两个字符串s和t,当且仅当两者字面值相等,两者的intern()方法的返回值才会相等

保证变量s指向字符串常量池中数据的两种方法:字面量赋值、调用intern()

new String()到底创建了几个对象?2个,java代码如下

public class StringNewTest {public static void main(String[] args) {String s = new String("szc");}
}

对应字节码

0 new #2 <java/lang/String>
3 dup
4 ldc #3 <szc>
6 invokespecial #4 <java/lang/String.<init>>
9 astore_1
10 return

根据第0行和第4行字节码,显然是新建了2个,一个是new的对象,一个szc

同理new String("a") + new String("b"),创建了6个,对应字节码

0 new #2 <java/lang/StringBuilder>
3 dup
4 invokespecial #3 <java/lang/StringBuilder.<init>>
7 new #4 <java/lang/String>
10 dup
11 ldc #5 <a>
13 invokespecial #6 <java/lang/String.<init>>
16 invokevirtual #7 <java/lang/StringBuilder.append>
19 new #4 <java/lang/String>
22 dup
23 ldc #8 <b>
25 invokespecial #6 <java/lang/String.<init>>
28 invokevirtual #7 <java/lang/StringBuilder.append>
31 invokevirtual #9 <java/lang/StringBuilder.toString>
34 astore_1
35 return

根据第0行、第7行、第11行、第19行、第23行可知,构造了两个new的String对象、a和b字面量对象、一个StringBuilder对象,然后第31行调用StringBuilder的toString()方法,可知新建了一个字符串对象

@Override
public String toString() {// Create a copy, don't share the arrayreturn new String(value, 0, count);
}

而new String(value, 0, count)对应的源码如下

public String(char value[], int offset, int count) {if (offset < 0) {throw new StringIndexOutOfBoundsException(offset);}if (count <= 0) {if (count < 0) {throw new StringIndexOutOfBoundsException(count);}if (offset <= value.length) {this.value = "".value;return;}}// Note: offset or count might be near -1>>>1.if (offset > value.length - count) {throw new StringIndexOutOfBoundsException(offset + count);}this.value = Arrays.copyOfRange(value, offset, offset+count);
}

由最后一行可知没有在常量池中新建ab对象(分析字节码指令也可以得到同样的结论,没有类似ldc <ab>的指令),所以总共有6个对象被新建。

分析以下代码执行结果(jdk7及以上)

public class StringNewTest {public static void main(String[] args) {String s = new String("a");s.intern();String s1 = "a";System.out.println(s == s1); // falseString s2 = new String("a") + new String("b");s2.intern();String s3 = "ab";System.out.println(s2 == s3); // true}
}

执行s.intern()时,由于常量池中已经有a字面量(new String时创建),所以这一行代码其实这里没啥用。s1直接指向常量池,s指向的是堆空间中被创建的字符串对象,所以结果为false

执行s2.intern()时,由于创建s2时,字符串常量池里没有ab,所以执行intern()方法后,会在字符串常量池中创建ab。而jdk7及以后,如果intern()方法传入的字符串值没有在常量池中,那么JVM只会在堆中新建字符串对象,然后常量池中创建的是指向堆中新字符串对象地址的引用,而s3指向的是常量池中对象的地址,实际也是堆中字符串对象,所以s2 == s3为true

jdk6及以前,intern()新字符串值时,会实打实在永久代常量池和堆中新建两个对象,所以那时s2 == s3为false

对于以下代码

String s4 = "cd";
String s5 = new String("c") + new String("d");
s4.intern();
System.out.println(s4 == s5); // false

由于这里intern()的是s4,s4指向的"cd"已经在创建s4时在常量池中新建了,所以这个intern()没什么用。而s5指向的是堆中的String对象,因此s4不等于s5

对于以下代码

String s6 = new String("e") + new String("f");
String s7 = s6.intern();System.out.println(s6 == "ef"); // true
System.out.println(s7 == "ef"); // true

s6在intern时,常量池里没有ef,所以常量池中保存的是堆中字符串对象ef的地址(自然也是s6的地址),返回给了s7。所以s6、"ef"、s7两两相等

对于以下代码

String s6 = new String("ef");
s6.intern();
String s7 = "ef";System.out.println(s6 == s7); // false

由于创建s6时,ef是通过new String()出来的,所以常量池中保存的是字面量ef,而s6指向的是堆中的字符串对象,所以s6 == s7为false

当构造大量重复字符串对象时,intern()方法会大大提高时空效率

StringTable的垃圾回收

可以使用-XX:+PrintStringTableStatistics打印字符串常量池的统计信息

测试代码

public class StringGcTest {public static void main(String[] args) {for (int i = 0; i < 100000; i++) {String.valueOf(i).intern();}}
}

参数:-Xms10m -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails。输出结果

[GC (Allocation Failure) [PSYoungGen: 2048K->504K(2560K)] 2048K->889K(9728K), 0.0797190 secs] [Times: user=0.00 sys=0.00, real=0.08 secs]
[GC (Allocation Failure) [PSYoungGen: 2552K->504K(2560K)] 2937K->1009K(9728K), 0.0012426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 2552K->488K(2560K)] 3057K->1057K(9728K), 0.0012180 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen      total 2560K, used 2086K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 2048K, 78% used [0x00000000ffd00000,0x00000000ffe8f980,0x00000000fff00000)from space 512K, 95% used [0x00000000fff00000,0x00000000fff7a020,0x00000000fff80000)to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen       total 7168K, used 569K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 7% used [0x00000000ff600000,0x00000000ff68e4b8,0x00000000ffd00000)
Metaspace       used 3241K, capacity 4496K, committed 4864K, reserved 1056768Kclass space    used 350K, capacity 388K, committed 512K, reserved 1048576K
SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     13291 =    318984 bytes, avg  24.000
Number of literals      :     13291 =    568080 bytes, avg  42.742
Total footprint         :           =   1047152 bytes
Average bucket size     :     0.664
Variance of bucket size :     0.664
Std. dev. of bucket size:     0.815
Maximum bucket size     :         6
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :     30235 =    725640 bytes, avg  24.000
Number of literals      :     30235 =   1752696 bytes, avg  57.969
Total footprint         :           =   2958440 bytes
Average bucket size     :     0.504
Variance of bucket size :     0.464
Std. dev. of bucket size:     0.681
Maximum bucket size     :         4

由StringTable statistics信息中的Number of entries       :     30235和Number of literals      :     30235两个值可见,发生了常量池的GC

不启用时,把+改成-即可

G1中的String去重操作

对于每一个访问的对象都要检查是否是候选要去重的String对象,如果是,把这个对象的一个引用插入到队列中。

一个后台线程专门用来去重,对这个队列的处理意味着从队列中删除这个元素,再对此元素引用的对象进行去重 。

对String对象去重的方法是,使用一个哈希表来记录所有被字符串对象使用的且不重复的char数组,去重时,根据这个哈希表查看堆中是否存在一个一模一样的char数组。

如果存在,String对象会引用表中已经存在的数组,释放对堆中数组的引用,堆中数组从而被GC掉;如果不存在,char数组会把插入到哈希表中,以便后来者共用之。

结语

行文至此,结合JVM的String覆写就结束了,后面我会整理垃圾回收器的学习笔记,与君共享

JVM学习笔记之StringTable相关推荐

  1. JVM学习笔记之-StringTable String的基本特性,内存分配,基本操作,拼接操作,intern()的使用,垃圾回收 ,G1中的String去重操作

    String的基本特性 string:字符串,使用一对""引起来表示. String s1 = ""; //字面量的定义方式 String s2 = new S ...

  2. JVM学习笔记汇总:结合尚硅谷宋红康老师视频教程及PPT

    JVM学习笔记汇总:结合尚硅谷宋红康老师视频教程及PPT 第一章:JVM虚拟机的介绍 1.1虚拟机的分类 虚拟机通常分为两类:系统虚拟机和程序虚机.其中,系统虚拟机是指完全对物理计算机的仿真,而程序虚 ...

  3. JVM学习笔记(自用)

    JVM学习笔记(自用) 文章目录 JVM学习笔记(自用) 1.简介 2.程序计数器 3. 虚拟机栈 4. 方法区 5. 直接内存 6. 垃圾回收 Young Collection Young Coll ...

  4. JVM学习笔记(四)------内存调优

    首先需要注意的是在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提 ...

  5. JVM学习笔记(四)

    JVM学习笔记(四) 文章目录 JVM学习笔记(四) 笔记链接 1.GC算法 1.1GC-判断对象是否可回收 1.1.1 引用计数法 1.1.1 可达性分析 1.2GC-回收算法 标记清除法(Mark ...

  6. jvm学习笔记(三)

    jvm学习笔记(三) 文章目录 jvm学习笔记(三) 1.全部笔记链接 2.堆 2.1堆的划分 使用JVM参数查看划分 Hotspot堆内存划分图(JDK8之前) 2.2 GC对堆的回收 GC的种类 ...

  7. jvm学习笔记(二)

    jvm学习笔记(二) 文章目录 jvm学习笔记(二) 1.全部笔记链接 2. Native关键字 3.关于JVM规范 3.1 JVM规范中运行时数据区的概念 4.HotSpot的JVM运行时数据区 4 ...

  8. jvm学习笔记(一)

    jvm学习笔记(一) 文章目录 jvm学习笔记(一) 1.全部笔记链接 3.类加载器 作用 类别 加载步骤 获得类加载器 4.双亲委派机制 5.沙箱安全机制 沙箱概念 JAVA沙箱的基本组件 基本组件 ...

  9. JVM学习笔记-04-java历史-沙箱安全机制

    JVM学习笔记-04-java历史-沙箱安全机制 文章目录 JVM学习笔记-04-java历史-沙箱安全机制 视频链接-最新JVM教程IDEA版[Java面试速补篇]-04-java历史-沙箱安全机制 ...

最新文章

  1. 最大字段和 冲出暴力枚举
  2. 石头扫地机器人离线了怎么办_关于激光头故障,石头扫地机器人无限次复活记!...
  3. html 表格文字颜色 css,CSS 表格-JavaScript中文网-JavaScript教程资源分享门户
  4. python RandomTrees特征编码
  5. ubuntu命令安装中文语言包_win10之linux子系统ubuntu安装中文包(三)
  6. java数据结构图_java总结数据结构和算法
  7. 进击的程序媛:从 Google 第一位程序媛到硅谷女王进化史
  8. php中is_uploaded_file()函数的用法
  9. Microsemi Libero使用技巧4——使用命令行模式下载程序
  10. R语言、Meta分析、MATLAB在生态环境领域里的应用
  11. sunshine in the rainsunshine in the rain
  12. ping 丢包 网络摄像头_ping丢包故障处理方法
  13. LabVIEW进制转换总结
  14. 贝尔维尤游戏巨头融资2亿美元!
  15. Linux的常用命令就是记不住,还在百度找?于是推出了这套教程,
  16. 公鸡五钱,母鸡三钱,小鸡三只一文钱,求百钱买百鸡
  17. hdu 4622 Reincarnation(hash)
  18. 3D技术一些回答以及前景
  19. u盘安装LINUX键盘失灵,U盘装Win7系统进入pe后鼠标键盘失灵不能用怎么办?
  20. 完全依赖XP必将自食其果

热门文章

  1. 苹果receipt样例
  2. ケータイ少女 script.arc的解压缩程序 (Java)
  3. python网页优化_400% 的飞跃-web 页面加载速度优化实战
  4. 如何在团队开展codeReview
  5. 计算机图像处理与分析研究,计算机图像处理与分析浅析
  6. Oracle 表分区的理解整理
  7. PAT 乙级1072 开学寄语 (20分)
  8. 手机游戏断线重连的实现
  9. Word图片自动编号,调整图片顺序自动更新图片编号,引用该图片的地方也对应更新
  10. Bluehost主机同一站点绑定多个顶级域名的方法