字符串常量池与intern方法分析

文章重要参考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

发布了17 篇原创文章 · 获赞 41 · 访问量 9991

猜你喜欢

转载自blog.csdn.net/awecoder/article/details/100679243