![4313a8153010b7b0db4e0a733a149d00.png](https://img-blog.csdnimg.cn/img_convert/4313a8153010b7b0db4e0a733a149d00.png)
文章大纲
- String#intern 内存分布图
- 案例1
- 案例2
- 案例3
- String#intern 的存在意义
- String#intern 的弊端
- 案例4 垃圾回收
- OOM
- Guava Interners
接着上一篇文章 画图理解Java String的内存分布。
本文的讲解以jdk1.7为准。
1 String#intern
我们知道 String#intern 就是把首次遇到的字符串加载到字符串常量池中。
用上了 String#intern 后,String 的内存分布图的难度再次升级!
案例1
下面先看第一个单测试案例, 一起了解下 String#intern 。
@Test
经过上篇文章的铺垫, 我们知道 @1 这行代码执行后, 内存分布图如下所示, 此时常量池和堆中都有字符串 "ab" 存在。
![1b6a899851ae110c05937357bd6b4270.png](https://img-blog.csdnimg.cn/img_convert/1b6a899851ae110c05937357bd6b4270.png)
来到 @2 这行, 字符串 "ab" 调用了 intern 方法后, 编译器会先去字符串常量池中查找是否有 "ab", 如果存在, 则返回常量池字符串的地址值;
所以此时的内存分布图如下:
![bbcf134fa15e252a02b7e419c6aa6ab4.png](https://img-blog.csdnimg.cn/img_convert/bbcf134fa15e252a02b7e419c6aa6ab4.png)
来到 @3 这行,双引号创建字符串 "ab", 编译器同样会先去字符串常量池中查找是否有 "ab",
如果存在,则返回常量池字符串的地址值;
如果不存在,那么会直接在常量池生成一个字符串 “ab”, 然后返回常量池字符串的地址值。
所以此时,局部变量 test1 和 test2 都是指向常量池中的字符串 “ab” 。
![2f17508dc7a5f1c0a93755fc61d6f02e.png](https://img-blog.csdnimg.cn/img_convert/2f17508dc7a5f1c0a93755fc61d6f02e.png)
案例2
下面看第二个单测试案例:
@Test
同样的, 经过上篇文章的铺垫, 我们知道 @1 这行代码执行后, 字符串常量池没有"abcd", 如下图所示:
![af9c90317330ee9fc447321cf1b292ed.png](https://img-blog.csdnimg.cn/img_convert/af9c90317330ee9fc447321cf1b292ed.png)
来到 @2 这行,字符串 "abcd" 调用了 intern 方法后,编译器会先去字符串常量池中查找是否有 "abcd",很明显不存在,那么:
在 jdk1.6,就将堆内存中的 "abcd" 添加到常量池中; 在 jdk1.7,那么就将指向堆内存中 "abcd" 的地址值保存到常量池中。
本文的讲解以 jdk1.7 为准。
所以此时局部变量 test1 引用的是 new String("abcd") 的地址。
![fb142bf8b0ea1994d640302b3c7bb261.png](https://img-blog.csdnimg.cn/img_convert/fb142bf8b0ea1994d640302b3c7bb261.png)
来到 @3 这行, 双引号创建字符串 "abcd", 编译器同样会先去字符串常量池中查找是否有 "abcd", 此时存在,则返回常量池字符串的地址值。
所以此时,局部变量 test, test1 和 test2 都是堆内存中的 new String("abcd")。
![b4795f4501037992302eb32f1fbff46e.png](https://img-blog.csdnimg.cn/img_convert/b4795f4501037992302eb32f1fbff46e.png)
案例3
下面看第三个单测试案例:
@Test
@1 这行代码执行后的内存分布图如下,此时字符串常量池同样没有 "abcd" :
![d11ae851c3df440aa644a62560a4deb3.png](https://img-blog.csdnimg.cn/img_convert/d11ae851c3df440aa644a62560a4deb3.png)
来到 @2 这行,双引号创建字符串 "abcd", 此时会直接在常量池生成一个字符串 “abcd”, 然后返回常量池字符串的地址值。
![dbb5cc2d47ee9e1b6cfa2e605407dc28.png](https://img-blog.csdnimg.cn/img_convert/dbb5cc2d47ee9e1b6cfa2e605407dc28.png)
来到 @3 这行,字符串 "abcd" 调用了 intern 方法后,因为常量池已经存在该字符串,直接返回地址值。所以此时的内存分布图如下,局部变量 test1 和 test2 都指向常量池中的 “abcd” 。
![e516f39265fdbc85686d967199756bcf.png](https://img-blog.csdnimg.cn/img_convert/e516f39265fdbc85686d967199756bcf.png)
可以看到,案例 3 和案例 2 仅仅是调换了 intern 方法的执行顺序,内存的分布就已经天差地别!
说了这么多案例,除了被绕晕,还是不知道 String#intern 有什么用,为什么要整出个这么复杂的东西出来?
2 String#intern 存在的意义
想知道 String#intern 存在的意义, 我们来看看 String 类的 equals 方法:
public
可以明显地看到,在使用 equals 比较两个字符串变量的时候,如果他们指向的地址是同一个字符串,那么将会立刻返回 true;否者的话,就要通过 while 循环变量字符串中所有的字符!因此 @1 和 @2 的性能可以说是差异巨大。
在要求高性能的场景下,肯定要竭力避免进入 @2 的条件分支,这就是 String#intern 存在的意义!
3 String#intern 的弊端
虽然 String#intern 很好,但是在某些场景下还是要多加留意的,比如垃圾回收。下面我们来一起看看案例4,在垃圾回收后内存分布会出现什么问题。
案例4 垃圾回收
@Test
在执行完 @3 这行代码后,内存分布图如下所示:
![10990418cf1b4da0ef11c8f66cee3703.png](https://img-blog.csdnimg.cn/img_convert/10990418cf1b4da0ef11c8f66cee3703.png)
在没有垃圾回收的情况下,也就是没有执行 @4 和 @5 的时候,
字符串 “55” 调用 intern 后返回的常量池中的地址是 0x0001,这和变量 not 引用的地址 0x0002 明显不是同一个对象。
现在来垃圾回收之后 intern 的引用情况。
在 @4 这里我显式地调用了垃圾回收, 把 canonical 占用的对内存给回收了。
在 @5 这里会一直阻塞等待 canonical 被成功回收掉, 再执行后面的代码。
怎么知道 canonical 是否已经被成功回收掉? 这里我引入了 Guava 的测试包:
<dependency>
GcFinalization 这个类正是来自 Guava 测试工具包, GcFinalization#awaitClear 默认会通过 CountDownLatch 阻塞等到 canonical 变量被回收掉。
但是如果超过 10 s 变量 canonical 还没被垃圾回收则会直接抛异常,所以 @4 那里显式地调用垃圾回收就显得有必要了。
垃圾回收之后,内存分布如下所示:
![7ed2ef8769b3f03cdd7c1b1489f11ca8.png](https://img-blog.csdnimg.cn/img_convert/7ed2ef8769b3f03cdd7c1b1489f11ca8.png)
现在终于到了 @6 这行,字符串 “55” 调用 intern 后发现常量池中没有 “55”,于是把堆内存中 “55” 的地址保存到常量池并返回 0x0002,所以这个时候 inten 返回的地址跟变量 not 引用的地址 0x0002 都是同一个对象。
![5413ed14cc8d65d8356be841ea24e19f.png](https://img-blog.csdnimg.cn/img_convert/5413ed14cc8d65d8356be841ea24e19f.png)
String#intern 遇上垃圾回收之后,一切都变得不可捉摸了!
OOM
除了上面存在的问题,其实 String#intern 还是有缺陷的,字符串常量池空间是有限的, 数据多了之后, 就很可能会出现OOM。
4 解决方案 Interners
针对上面的缺陷, Guava 给出了新的方案 Interners , 把字符串常量池的内容存储到了堆内存里。
Interners内部基于ConcurrentHashMap实现,而且可以设置强引用类型, 防止实例被垃圾回收。
Interners 的简单用法如下,和 String#intern 的效果一致:
private
咱们下一篇再讲讲 Interners 的使用场景。
X References
字符串常量池、class常量池和运行时常量池http://tangxman.github.io/2015/07/27/the-difference-of-java-string-pool/
来看看String类和常量池内存分析以及8种基本类型和常量池例子https://blog.csdn.net/aodubi0638/article/details/102144579
学习JVM是如何从入门到放弃的?(修订版)
来自公众号 Java3y
往期回顾
画图理解Java Integer的“值传递”
画图理解Java String的内存分布
Kafka Topic为什么要分区
一张图理解Kafka时间轮(TimingWheel)
Java队列是怎么支撑起多人运动的?
画个花瓶理解Java线程池
![7b5fa2634b0a0c53bee2a20532687d0c.png](https://img-blog.csdnimg.cn/img_convert/7b5fa2634b0a0c53bee2a20532687d0c.png)
用d3动画讲解各种有趣的编程知识。