聊聊各种常量池

学习JVM的时候经常会遇到各种常量池,不同版本的JDK它们的存储位置也不同,这篇随笔就整理下几种常见的常量池,以JDK1.8为主。先看一张存储示意图,里面涉及1.8和1.6。

 

常量池是存储在方法区中的,比如我们有这样一段代码:

public class Demo {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

我们对Demo.class“进行反汇编得到具体字节码信息,前面这一段是类的元信息,包括类名、修改时间、访问修饰关键字等。

接着我们可以看到 Constant pool:,这就是常量池,每一个符号后面引用了其他符号或者表示具体的信息。其实常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。

运行时常量池:是方法区的一部分,JDK1.8方法区位于系统内存中。当类被加载到内存时,那么原先的常量池信息就会放入运行时常量池中,并且将 #1 这些符号地址变为直接引用。

字符串常量池:也就是StringTable,1.8是存储在堆中。用一个简单的例子来说明字符串对象的各种关系:

public class Demo {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2;
        System.out.println(s3 == s4); // false
        String s5 = "a" + "b";
        System.out.println(s3 == s5); // true
    }
}

编译这段程序的时候a、b、ab这些都是运行时常量池中的符号,还没变成Java 字符串对象。当执行到“String s1 = “a”; ”这行代码时,ldc会将a符号变为 “a” 字符串对象,如果串池中没有”a”对象,“a”对象就会被放入StringTable里。s2、s3逻辑类似,最终串池里面的对象为[“a”, “b”, “ab”]。

s4将s1、s2变量拼接,底层字节码的执行逻辑是在第9行创建了 StringBuilder 对象,13行是调用无参构造方法初始化类。17、21行调用了 StringBuilder.append 方法进行拼接,24行调用StringBuilder.toString方法,这个方法底层是生成一个新的字符串即将用拼接的值作为新字符串对象的值。s3是在串池中,s4是一个新的对象存储在堆中,比较s3 == s4 输出值应该是false。

再看s5将两个字符串常量进行拼接,这时候JVM并不会像拼接变量那样创建对象,而是直接到串池中找到对象 “ab”,所以 “System.out.println(s3 == s5);” 输出值为true。这是因为 javac 在编译期间认为 “a” 、”b”是定值不会再改变, 所以直接得到结果”ab”。

 

其实我们可以使用 intern方法,主动将串池中还没有的字符串对象放入串池。

但是如果在程序开始时就将“ab”放入串池,再比较 s == “ab“ 就会返回false。

 

Tags: