Featured image of post 再谈常量池

再谈常量池

深入理解 intern() 方法,从几个例子入手分析理解常量池的设计思想

感觉上一篇文章没有说过瘾,所以我决定今天再补充一下之前没有讲清楚的String.intern()方法,希望这次能够一下子讲解得明明白白。

本篇文章主要围绕着一个例子进行展开说明,目的就是想让大家理解java中关于String这样的一个对象的创建原理,话不多说,咱们进入正题。

常见的一道面试题

请判断下面例子的打印结果:

  • 例1

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    public class Main {
        public static void main(String[] args)  {
              String s1 = new String("abc");
              String s2 = "abc";
              System.out.println(s1 == s2);//false
    
              s1.intern();
            System.out.println(s1 == s2);//false
        }
    }
    
  • 例2

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    public class Main {
        public static void main(String[] args)  {
              String s3 = new String("def");
            String s4 = new String("def");
            System.out.println(s3 == s4);//false
    
            String s33 = s3.intern();
            String s44 = s3.intern();
            System.out.println(s33 == s44);//true
            System.out.println(s3 == s4);//false
        }
    }
    
  • 例3

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    public class Main {
        public static void main(String[] args)  {
              String s5 = new String("hi") + new String("jk");
            String s6 = "hijk";
            s5.intern();
            System.out.println(s5 == s6);//false
    
            String s7 = new String("lm") + new String("no");
            s7.intern();
            String s8 = "lmno";
            System.out.println(s7 == s8);//true
        }
    }
    
  • 例4

    1
    2
    3
    4
    5
    6
    7
    8
    
    public class Main {
        public static void main(String[] args)  {
              String s9 = new String("ja") + new String("va");
            s9.intern();
            String s10 = "java";
            System.out.println(s9 == s10);//false
        }
    }
    

上面的结果我已经写在了注释里面,不知道你是否回答正确了,下面我们来依次解析这些题目的结果。

解答

在解答问题之前,我们需要先来说一说String.intern()方法到底做了什么,这个十分重要,是能够解答上面题目的基础。

首先,String.intern()方法的返回值一定全局字符串常量池中记录的对应字面量在堆内存中的地址。当字符串对象调用intern()方法的时候,它会先去全局字符串常量池中查找对应的字符串字面量是否存在,如果字符串已经存在,那么将直接返回该字符串字面量所对应的堆内存中的地址;如果全局常量池中没有找到对应的字符串字面量值,那么便会去堆内存中进行操作。

注意:由于全局字符串常量池在jdk1.7之后移动到了堆中,所以上面的操作其实都是在堆中进行的,这一点十分重要,因为这涉及到了堆内存的复用。

我们继续往下讲,由于在全局字符串常量池中没有找到,所以线程转而会去查看堆中是否存在已经创建好的字面量对应的对象实例,如果存在对应的实例,那么会将该实例的地址放到全局字符串常量池对应字面量下,然后返回;如果不存在对应的实例,那么将会去创建该字面量对应的对象实例,然后将对应的实例地址放到全局字符串常量池对应字面量下,然后返回。

所以这里存在了一个我们容易忽略的地方,如果全局字符串常量池中不存在,会继续去堆中查找有没有创建过对应字面量的实例,如果没有创建,那么才会去创建对应的实例。而不是直接就创建了相关的实例。

下面我们来试着解答上面的四道例题:

  • 例1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Main {
    public static void main(String[] args)  {
      	String s1 = new String("abc");
      	String s2 = "abc";
      	System.out.println(s1 == s2);//false
      
      	s1.intern();
        System.out.println(s1 == s2);//false
    }
}

1这道题目主要执行逻辑其实就变成了下面这样:

在执行第一行代码String s1 = new String("abc")的时候,此时由于存在字面量abc,所以会先去全局字符串常量池中查询是否存在字面量abc对应的地址,发现没有该字面量对应的地址,所以去堆空间的特定区域(应该是新生代)中查询是否存在字面量abc对应的实例,发现也没有,因此创建了实例对象abc,其地址为String@467,然后将该地址放到了全局字符串常量池中。之后再执行new String("abc"),所以又创建了一个字符串地址为String@468的同字面量实例。

而第二行代码String s2 = "abc"则先去全局字符串常量池中查询是否存在字面量为abc的地址,很明显是存在的,所以返回的s2实例对应的地址便是String@467,所以比较这两个对象的地址很明显是不相等的;之后调用String.intern()方法时,按照我上面介绍的String.intern()方法的执行逻辑,这里如果有接受返回值的话,那么应该是String@467,但是由于没有接受返回值,所以这里的s1s2地址不变,因此还是输出false

  • 例2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Main {
    public static void main(String[] args)  {
      	String s3 = new String("def");
        String s4 = new String("def");
        System.out.println(s3 == s4);//false

        String s33 = s3.intern();
        String s44 = s3.intern();
        System.out.println(s33 == s44);//true
        System.out.println(s3 == s4);//false
    }
}

经过例1的分析,我们可以立马回答出来这里在堆空间中其实是创建了三个字面量为def的实例,除了s3s4两个显示创建的之外,还有一个隐性创建的实例,所以s3s4地址是不相等的,而s33s44获取的便是全局字符串常量池中的地址,也就是那个隐性创建的字面量实例地址,所以s33s44地址相等;同理最后一行输出的依然是false

  • 例3
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Main {
    public static void main(String[] args)  {
      	String s5 = new String("hi") + new String("jk");
        String s6 = "hijk";
        s5.intern();
        System.out.println(s5 == s6);//false

        String s7 = new String("lm") + new String("no");
        s7.intern();
        String s8 = "lmno";
        System.out.println(s7 == s8);//true
    }
}

这一题其实比较难以理解的就是String s5 = new String("hi") + new String("jk");会不会产生一个字面量值为hijk的实例,同时将实例地址放到全局字符串常量池中,这里我给出答案是:不会,因为依据上一节我提到的将字面量对应实例地址放到全局字符串常量池中的方式,其实只有两种,一种是类似String s = "abc"的方式,另外一种就是调用String.intern()方法的方式,而这里两样都不存在,所以这里不会和全局字符串常量池相关。因此例3中第一行代码,其实仅仅会在堆空间中创建一个字面量为hijk的实例(当然这里没有考虑hijk),此时创建的实例地址还没有放到全局字符串常量池中,而在执行String s6 = "hijk"的时候,会在堆中创建一个字面量也为hijk的实例,同时将该实例的地址放到全局字符串常量池中,所以之后无论是否调用String.intern()方法,最后s5s6都是不会相等的。

接下来再看s7s8,这里在创建s7对象之后,此时堆中之存在一个lmno的实例,而全局字符串常量池中还不存在字面量为lmno的实例地址,此时调用s7.intern()方法,按照我上面对intern()方法的介绍,这里会将s7对应的实例地址放到全局字符串常量池中,所以后面创建s8的时候获取的实例地址就是s7对应的实例地址。因此这里输出的结果是true

注意:如果你使用的是jdk1.7以下的版本,那么最后输出的是false,具体原因是因为jdk1.7以下版本全局字符串常量池在堆外,所以无法进行复用地址。

  • 例4
1
2
3
4
5
6
7
8
public class Main {
    public static void main(String[] args)  {
      	String s9 = new String("ja") + new String("va");
        s9.intern();
        String s10 = "java";
        System.out.println(s9 == s10);//false
    }
}

最后一道题目其实挺有迷惑性的,如果按照我上一题的分析,这里应该输出true才对,但是却偏偏输出了false,这是因为你要看看这里的字面量是什么,这里的字面量是java,所以这个java字面量地址很有可能在执行main方法之前就已经被放到了全局字符串常量池中,具体放的位置在sun.misc.Version类中launcher_name字段,如图:

具体调用执行该类在java.lang.SysteminitializeSystemClass()方法会调用:

i

所以java标准库在JVM启动过程中会调用sun.misc.Versioninit()方法,因而sun.misc.Version会进行类加载的操作,而类加载的初始化阶段时,会对静态常量字段进行真正的赋值操作,但是由于sun.misc.Versionlauncher_name字段是final修饰的,因此引用的字符串java在准备阶段就被intern到了字符串常量池里面了。

总结

本文主要针对之前探讨的intern()方法进行深入探究,总的过程比较曲折,但是还是比较具有实际意义,让我对常量池的概念又有了更加深入的理解,也让我对jvm越来越有兴趣,感觉是时候去深入理解理解了~

资料

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus