字符串生成分布均匀_画图理解 Java String#intern 的内存分布

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


来到 @2 这行, 字符串 "ab" 调用了 intern 方法后, 编译器会先去字符串常量池中查找是否有 "ab", 如果存在, 则返回常量池字符串的地址值;
所以此时的内存分布图如下:

bbcf134fa15e252a02b7e419c6aa6ab4.png


来到 @3 这行,双引号创建字符串 "ab", 编译器同样会先去字符串常量池中查找是否有 "ab",
如果存在,则返回常量池字符串的地址值;
如果不存在,那么会直接在常量池生成一个字符串 “ab”, 然后返回常量池字符串的地址值。
所以此时,局部变量 test1 和 test2 都是指向常量池中的字符串 “ab” 。

2f17508dc7a5f1c0a93755fc61d6f02e.png


案例2

下面看第二个单测试案例:

@Test


同样的, 经过上篇文章的铺垫, 我们知道 @1 这行代码执行后, 字符串常量池没有"abcd", 如下图所示:

af9c90317330ee9fc447321cf1b292ed.png


来到 @2 这行,字符串 "abcd" 调用了 intern 方法后,编译器会先去字符串常量池中查找是否有 "abcd",很明显不存在,那么:
在 jdk1.6,就将堆内存中的 "abcd" 添加到常量池中; 在 jdk1.7,那么就将指向堆内存中 "abcd" 的地址值保存到常量池中。
本文的讲解以 jdk1.7 为准。
所以此时局部变量 test1 引用的是 new String("abcd") 的地址。

fb142bf8b0ea1994d640302b3c7bb261.png


来到 @3 这行, 双引号创建字符串 "abcd", 编译器同样会先去字符串常量池中查找是否有 "abcd", 此时存在,则返回常量池字符串的地址值。
所以此时,局部变量 test, test1 和 test2 都是堆内存中的 new String("abcd")。

b4795f4501037992302eb32f1fbff46e.png

案例3

下面看第三个单测试案例:

@Test


@1 这行代码执行后的内存分布图如下,此时字符串常量池同样没有 "abcd" :

d11ae851c3df440aa644a62560a4deb3.png


来到 @2 这行,双引号创建字符串 "abcd", 此时会直接在常量池生成一个字符串 “abcd”, 然后返回常量池字符串的地址值。

dbb5cc2d47ee9e1b6cfa2e605407dc28.png


来到 @3 这行,字符串 "abcd" 调用了 intern 方法后,因为常量池已经存在该字符串,直接返回地址值。所以此时的内存分布图如下,局部变量 test1 和 test2 都指向常量池中的 “abcd” 。

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


在没有垃圾回收的情况下,也就是没有执行 @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


现在终于到了 @6 这行,字符串 “55” 调用 intern 后发现常量池中没有 “55”,于是把堆内存中 “55” 的地址保存到常量池并返回 0x0002,所以这个时候 inten 返回的地址跟变量 not 引用的地址 0x0002 都是同一个对象。

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

用d3动画讲解各种有趣的编程知识。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值