1 简介
String.intern()是 JDK 一早就提供的 native 方法, 不由 Java 实现, 而是底层 JVM 实现, 这让我们对它的窥探提高了难度. 特别是在 Oracle 收购了 Sun 公司后, 源代码不开源了, 更无法深入研究了. 但我们还是有必要尽量地去探索.
本文将主要讲解一下 String.intern()方法的原理, 特点, 并介绍一个新奇的应用.
2 String 的池化
方法 intern()的作用就是将 String 池化, 这个池是 String 的常量池. 不同版本的 JDK 有不同的实现.
2.1 不同实现与不同内存空间
JDK 6:intern()方法会把首先遇到的字符串复制一份到永久代中, 然后返回永久中的实例引用; 如果不是首次, 说明常量池中已经有该字符串, 直接返回池中的引用. 常量池在永久代 (PermGen) 中.
JDK 7:intern()方法首次遇到字符串时, 不会复制实例, 而是直接把该字符串的引用记录在常量池中, 并返回该引用; 如果不是首次, 则直接返回池中引用. JDK 7 常量池在堆中.
JDK 8: 功能与 JDK 7 类似. 常量池在元空间 Metaspace 中, 元空间不在虚拟机内存中, 而是使用本地内存.
2.2 常量池大小差异
这个所谓的 String 常量池, 其实就是一张哈希表, 跟 HashMap 类似, 所以也是有大小限制和哈希冲突可能. 常量池越大, 哈希冲突可能性越小.
JDK 6 早期版本, 池大小为常量 1009, 后期变得可配置, 通过参数 - XX:StringTableSize=N 指定. 大小也会受限于永久代的大小, 建议避免使用 intern()方法, 防止造成 PermGen 内存溢出.
JDK 7 将常量池移到堆后, 可以存放更多常量, 也一样通过参数可配置大小. 在 Java 7u40 后, 常量池默认大小增加到了 60013.
JDK 8 默认大小一开始就是 60013, 依旧支持参数配置.
总的来说,-XX:StringTableSize 的默认值在 Java 7u40 以前为 1009,Java 7u40 以后改为 60013.
3 例子分析
通过例子, 来理解一下就更清晰了. JDK 7 和 8 应该表现一致, 本文使用 JDK 8.
3.1 JDK 8
先演示 JDK 8 的情况:
例子 1
- String str1 = new String("pkslow");
- System.out.println(str1.intern() == str1);
结果: false
分析: 因为使用了字面量, 在编译期就会把字符串放到常量池, 当使用 new String()时, 会创建新的对象. 所以常量池中的引用与创建的对象引用不同.
例子 2
- String str1 ="pkslow";
- System.out.println(str1.intern() == str1);
结果: true
分析: 与上个例子对比, 将常量池的地址赋值给了 str1 变量, 所以相等.
例子 3
- String str1 = new StringBuilder("pk").append("slow").toString();
- System.out.println(str1.intern() == str1);
- String str2 = new StringBuilder("pk").append("slow").toString();
- System.out.println(str2.intern() == str2);
结果: true false
分析:
(1)第一句创建了一个新的字符串对象, str1 为其引用, 调用 str1.intern()时会把它的引用放到常量池中并返回, 所以是同一个引用.
(2)在 (1) 中已经放在常量池了, 所以 str2.intern()返回的是 str1, 与 str2 不相等.
例子 4
- String str = new StringBuilder("ja").append("va").toString();
- System.out.println(str.intern() == str);
结果: false
分析: 按理说与上个例子的 (1) 一样, 应该为 true 才对. 问题在于 java 它是一个比较特殊的字符串, 已经在常量池中存在了, 所以不相等. 至于为何会存在, 我的猜想是有两种可能: 其它 JDK 的 Java 代码有该常量; JVM 代码直接把某些特殊字符串放到了常量池. 这个没有深究了.
3.2 JDK 6 的不同
当我们知道了原理后, 不同表现就可以很容易判断出来了. 如下例子:
- String str1 = new StringBuilder("pk").append("slow").toString();
- System.out.println(str1.intern() == str1);
JDK 6 结果: false
JDK 8 结果: true
因为 JDK 6 对于首次遇到的字符串, 会复制一份到常量池并返回其引用, 与 str1 的引用不是同一个, 所以为 false. 而 JDK 8 只是将 str1 的引用在常量池记录然后返回, 还是同一个, 所以为 true.
知道了基本原理, 更多情况就可以具体分析了, 不再一一赘述.
4 一种少见的应用
之前已经说过, String.intern()方法本质就是维持了一个 String 的常量池, 而且池里的 String 应该都是唯一的. 这样, 我们便可以利用这种唯一性, 来做一些文章了. 我们可以利用池里 String 的对象来做锁, 实现对资源的控制. 比如一个城市的某种资源同一时间只能一个线程访问, 那就可以把城市名的 String 对象作为锁, 放到常量池中去, 同一时间只能一个线程获得.
具体代码如下:
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- public class StringInternMultiThread {
- private String city;
- public StringInternMultiThread(String city) {
- this.city = city;
- }
- public void handle() {
- synchronized (this.city.intern()) {
- System.out.println(city + ":Fetched the lock");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(city + ":Release the lock");
- }
- }
- public static void main(String[] args) {
- ExecutorService executorService = Executors.newFixedThreadPool(6);
- StringInternMultiThread guangzhou = new StringInternMultiThread("Guangzhou");
- StringInternMultiThread shenzhen = new StringInternMultiThread("Shenzhen");
- StringInternMultiThread beijing = new StringInternMultiThread("Beijing");
- executorService.execute(guangzhou::handle);
- executorService.execute(guangzhou::handle);
- executorService.execute(guangzhou::handle);
- executorService.execute(shenzhen::handle);
- executorService.execute(shenzhen::handle);
- executorService.execute(beijing::handle);
- executorService.shutdown();
- }
- }
运行结果如下:
- Guangzhou:Fetched the lock
- Shenzhen:Fetched the lock
- Beijing:Fetched the lock
- Beijing:Release the lock
- Shenzhen:Release the lock
- Guangzhou:Release the lock
- Shenzhen:Fetched the lock
- Guangzhou:Fetched the lock
- Shenzhen:Release the lock
- Guangzhou:Release the lock
- Guangzhou:Fetched the lock
- Guangzhou:Release the lock
可以看出, 同一时间同一个城市不会同时获得资源, 而不同城市可以同时获得资源来处理. 这种案例其实有其它更优雅的方案, 这不是本文的重点, 就不赘述了.
5 总结
本文介绍了 String.intern()方法的原理和不同 JDK 版本的表现, 并通过多个例子与一个应用加深理解. 希望对大家理解 String 和 JVM 有帮助.
来源: https://www.cnblogs.com/larrydpk/p/11830208.html