1

JDK 6及以前,字符串常量池位于永久代(方法区)中;JDK 7开始,字符串常量池从永久代移动到了堆中。事实上, JDK 6和JDK 7及以后的版本在String.intern()的具体实现上有所差异,因此对于同样的代码可能会导致不同的结果。

在详细分析intern()的原理之前,先引入一个“小工具”,它能帮我们打印对象的内存地址,这个工具的代码参考了[2],代码如下:

public class Test {
    private static Unsafe unsafe;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static long addressOf(Object o)
            throws Exception {
        Object[] array = new Object[]{o};

        long baseOffset = unsafe.arrayBaseOffset(Object[].class);
        int addressSize = unsafe.addressSize();
        long objectAddress;
        switch (addressSize) {
            case 4:
                objectAddress = unsafe.getInt(array, baseOffset);
                break;
            case 8:
                objectAddress = unsafe.getLong(array, baseOffset);
                break;
            default:
                throw new Error("unsupported address size: " + addressSize);
        }

        return (objectAddress);
    }
}

提出问题

考虑《深入理解Java虚拟机》中的一个例子:

String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);

书中说对于JDK 6,上述代码会打印出false, false;对于JDK 7,上述代码会打印出true, false,这是为什么呢?别急,我们先了解一下intern()是怎么工作的。

JDK 6和 JDK 7的intern()原理分析

考虑下面一段代码:

String str = new String("Hel") + new String("lo"); // 1
System.out.println(addressOf(str)); // 2
System.out.println(addressOf(str.intern())); // 3

对于JDK 6,上述代码会打印出两个不同的地址;对于JDK 7 上述代码会打印出两个相同的地址。这实际上是因为,JDK 6和JDK 7底层在实现intern()时有所不同。

JDK 6

在JDK 6中,上述程序的内存情况如下图所示:

image.png

  • 第1行代码"Hel""lo"是字面量,会被放出永久代的字符串常量池,然后str指向一个堆中的String类型对象
  • 第3行代码: 调用intern()时,首先会检查常量池中有没有值等于"Hello"的对象,发现没有,那么会在常量池中创建一个值为"Hello"的对象,并返回该对象的指针。

JDK 7

在JDK 7中,上述程序的内存情况如下图所示:

image.png

  • 第1行代码:同上
  • 第3行代码:调用intern()时,首先会检查常量池中有没有值等于"Hello"的对象,发现没有,那么会在常量池中创建一个值为"Hello"的对象,但是返回的是堆中的字符串对象的地址。所以上述程序打印出来的两个地址是相同的。
根据参考的资料[1]和[3],JDK 6和JDK 7的intern()的大致原理就是如上面所说的那样,要想了解具体是怎么实现的,还是要阅读JDK底层的源码。

解决问题

了解了intern()的大致工作原理之后,再回到一开始的问题:

String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);

下面仅仅用JDK 7做一下分析,JDK 6为什么得到那样的输出请读者自行研究一下:

  • 第1行代码"计算机""软件"会被放入字符串常量池,在堆中创建一个String对象
  • 第2行代码: 执行intern(),发现字符串常量池没有值为"计算机软件"的对象,于是在字符串常量池创建String对象,然后返回对str1的引用,因此str1.intern() == str1的值为true。(事实上,上一小节我们也验证了,str1.intern()str1指向的内存地址是相同的)
  • 第3行代码"ja""va"会被放入字符串常量池,在堆中创建一个String对象
  • 第4行代码: 执行intern(),发现字符串常量池中有值为"java"的字符串对象,直接返回对常量池中这个对象的引用,因此str2.intern()指向的是字符串常量池中的对象,而str2指向的是堆中的对象,所以不相等。

至于为什么执行第4行代码时常量池中已经存在字符串"java"了, 这涉及类加载的细节,下面是《深入理解Java虚拟机》中的解释:

它是在加载sun.misc.Version这个类的时候进入常量池的。 本书第2版
并未解释java这个字符串此前是哪里出现的, 所以被批评“挖坑不填
了”(无奈地摊手) 。 如读者感兴趣是如何找出来的, 可参考RednaxelaFX的知乎回答
https://www.zhihu.com/questio...

参考

[1] https://stackoverflow.com/que...
[2] https://stackoverflow.com/que...
[3] https://stackoverflow.com/a/7...


ponnylv
3 声望0 粉丝

下一篇 »
CS学习资料