文章重要参考https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html。
文章目录
1 深入理解String#intern方法以及在各JDK版本中的差异
1.1 字符串常量池
字符串常量池存在的意义是使字符串在运行时速度更快,更节省内存。主要的使用方法有三种。
(1)直接双引号引用的String对象会直接存储在字符串常量池中;
(2)直接new String对象时,如果常量池不存在对应String对象,会在池中新创建;
(3)通过String#intern()方法,从字符串常量池中查询当前字符串是否存在,如果不存在就会将当前字符串放入常量池中。
1.2 字符串常量池在不同JDK版本中的位置
在JDK7之前运行时常量池包含字符串常量池存放在方法区,此时hotspot虚拟机对方法区的实现为永久代。
在JDK7中字符串常量池由方法区单独移到了堆中,运行时常量池剩下的东西还在方法区,仍是永久代。
在JDK8中,hotspot移除了永久代而采用元空间(Metaspace)代替,此时字符串常量池仍在堆中,运行时常量池还在方法区,但方法区的实现由永久代变为元空间。
1.3 String#intern()
定义:返回字符串对象的规范化表示形式。
public native String intern();
需要注意的是,intern()方法是一个本地方法;方法的返回值类型是字符串,内容与该字符串相同,但一定取自字符串常量池。
字符串常量池,初始时为空,由String类所私有维护。当intern()方法触发,如果池中已经包含与该String对象相等的字符串,则返回池中字符串。相等的判断,通过equals(Object)方法来实现。否则,将字符串添加到池中,并返回。可以确定的是,通过字符串对象的intern()方法返回的字符串必然取自字符串常量池。
jdk1.6和jdk1.7最大的区别是jdk1.6中 intern 方法会把首次遇到的字符串实例复制到字符串常量池中,并返回此引用;但在jdk1.7中,只是会把首次遇到的字符串实例的引用添加到常量池中(没有复制),并返回此引用。
1.4 通过案例理解String#intern()方法
代码示例1
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
打印结果
jdk6:false false
jdk7:false true
JDK6中常量池位于PermGen区域中,如图PermGen区和Heap区是独立的区域。由引号声明的字符串直接在字符串常量池中生成,存放在PermGen区,而new生成的String对象存放在Heap区。比较Heap区的对象地址和字符串常量池的对象地址必然是不同的。
Jdk6中,由于PermGen区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m。大量使用intern方法容易导致内存溢出java.lang.OutOfMemoryError: PermGen space。正是因为PermGen区域太小,JDK7对字符串常量池做了迁移,迁移到了Java Heap区域。JDK8中直接取消了PermGen区,以元空间来替代。
1.4.1 s和s2字符串的分析
String s = new String(“1”); 语句的运行会创建2个对象,一个是“1”字符串存放在常量池,另一个是Java Heap中的String对象。
接着执行s.intern();语句,s对象会去字符串常量池查找是否存在"1"字符串,"1"已存在。
String s2 = “1”; 执行该语句,生成引用s2指向字符串常量池中的"1"对象。s的引用地址在Java Heap区,s的引用地址在常量池,地址明显不同。
1.4.2 s3和s4字符串的分析
String s3 = new String(“1”) + new String(“1”);如果忽略前面的s和s2字符串,执行该语句会产生4个对象,分别是字符串常量池中的"1"对象、Java Heap中s3指向的对象以及两个中间对象。此时s3引用对象的内容是“11”,但是字符串常量池中只有"1",而没有"11"对象。
接着指向s3.intern();语句,字符串常量池没有"11",s3会将"11"放入到String常量池中。如果是JDK6,此时PermGen区的字符串常量池会新生成一个"11"对象。而在JDK7中常量池不在PermGen区域,不需要再存储一个对象,而是直接存储堆中对象引用,这个引用指向s3引用对象。
最后,执行String s4 = “11”; 此时字符串常量池已经有"11"这个对象了,即指向s3引用对象的一个引用。此时我们可以察觉到s3和s4两个引用都是指向了Java Heap中的同一个对象,因此JDK7运行结果为true。
如下代码,分析如s3和s4情形大同小异。
String str = new StringBuilder("awe").append("coder").toString();
System.out.println(str.intern() == str);
假设对示例1代码做些许改动:将s3.intern()移到后一句。
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
打印结果
jdk7:false
参考前面案例的分析思路,很容易就可以分析出原因。s3语句同上,生成Java Heap对象和字符串常量池"1"对象。执行String s4 = “11”;语句时,池中会新创建"11"对象。再执行s3.intern(),池中"11"对象已经存在。因此s3和s4引用是不同的。
1.5 String#intern()方法的最佳实践
合适地使用String#intern()方法可以节省大量内存空间。不合理使用也会严重影响性能。
重要参考:https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.html