Java

细说Java常量池

2020-07-14  本文已影响0人  小胡_鸭

  Java中的常量池有:class常量池、运行时常量池、String常量池。

为什么要使用常量池?

  避免频繁地创建和销毁对象而影响系统性能,实现对象的共享(字符串常量池);对于类共用的元数据信息,使用常量池可以共享使用,而不是不同线程、对象都创建一个副本,节省内存开销(class常量池、运行时常量池)。

一、class常量池

  一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References),以及其他类的元信息。每个class文件都有一个class常量池。

1、字面量

  字面量相当于Java语言层面常量的概念,包括:

2、符号引用

  符号引用则属于编译原理方面的概念,比如代码中定义了一个int a,变量名是a,这就是一个常量。包括:

package basic;

public class ConstantsTest {
    public String name = "Hello World";
    public final int num = 100;

    public ConstantsTest(String name) {
        this.name = name;
    }

    public void info() {
        System.out.println(name);
        System.out.println(num);
    }
}

  按照上面说的规则,该类的class常量池中包含的常量应该有:

字面量

符号引用

  将类编译出class文件,再用 javap -v ConstantsTest 可以看到完整的常量池信息:




二、运行时常量池

  当加载一个类时,势必要将其class常量池中的信息加载到内存中,这就是运行时常量池,通常存储类元信息的内存叫方法区,被该类的所有实例对象所共享引用。

  JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用。

  运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。

关于String#intern()

  在代码中,字符串字面量会被放入一个字符串常量池中,使用String类的intern方法时,首先在字符串常量池中查找是否存在一份equal相等的字符串如果有的话就返回该字符串的引用,没有的话就将它加入到字符串常量池中,所以存在于class中的常量池并非固定不变的,可以用intern方法加入新的。

三、字符串常量池

1、字符串常量池的实现与本质

  在HotSpot VM里是通过 StringTable 类来实现常量池的,它是一个hash表,即通过计算String对象的hashcode,决定要将其存储在表中的哪个位置,,默认大小为1009。StringTable 在JVM中只有一个实例,被所有的类共享。

  在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;

  在JDK7.0中,StringTable的长度可以通过参数指定:

-XX:StringTableSize=66666

  如果在类的定义中使用了字符串的字面量,直接赋值拼接,则对应的字面量会被放到字符串常量池中,如下面的代码所示:

public class StringPool {
    public static void main(String[] args) {
        String i = "hello";
        String j = "World";
        String k = "hello" + "World";
        String l = new String("hello");
    }
}

  类的字节码文件内容如下:


  从编译器就可以确定值的变量有i、j、k,而l需要调用虚拟方法,所以是运行期决定的,生成的对象不在常量池里,所以程序执行的结果是false。



2、字符串常量池的存储位置

  字符串常量池中的字符串只存在一份

String s1 = "hello,world!";
String s2 = "hello,world!";

  执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。



3、使用案例和坑

        String a = "hello";
        String b = new String("hello");
        System.out.println(a == b);

  上面的代码,执行结果为 false,因为a是常量池中的一个常量,而b是一个普通的位于堆内存中的对象,如下图所示(JDK6.0标准):


  使用 new String 创建的对象都是存储在堆内存中的,而a作为字面量,一开始就存储在class文件中,之后运行期,转存至方法区中,所以a和b指向的对象不一样。
        String s1 = "Hello";
        String s2 = "Hello";
        String s3 = "Hel" + "lo";
        String s4 = "Hel" + new String("lo");
        String s5 = new String("Hello");
        String s6 = s5.intern();
        // 拼接是动态调用,所以拼接后的String对象存在堆内存中
        String s7 = "H";
        String s8 = "ello";
        String s9 = s7 + s8;

        System.out.println("s1 == s2? " + (s1 == s2));
        System.out.println("s1 == s3? " + (s1 == s3));
        System.out.println("s1 == s4? " + (s1 == s4));
        System.out.println("s1 == s5? " + (s1 == s5));
        System.out.println("s4 == s5? " + (s4 == s5));
        System.out.println("s1 == s6? " + (s1 == s6));
        System.out.println("s5 == s6? " + (s5 == s6));
        System.out.println("s1 == s9? " + (s1 == s9));
        System.out.println("s5 == s9? " + (s5 == s9));

  上面代码执行结果如下:



  分析:

(1)常量拼接

    public static void main(String[] args) {
        final String a = "hello";
        final String b = "world";
        String c = a + b;
        String d = "helloworld";
        System.out.println(c == d);
    }

  a、b、c类似于上面的s7、s8、s9,但是a、b被final修饰,表示在编译时就可以确定它的值,将其拼接起来的值c也是可以确定的,所以c指向常量池中的字符串常量,执行结果为true。


(2)static静态代码块
    public static final String a;
    public static final String b;

    static {
        a = "hello";
        b = "world";
    }

    public static void main(String[] args) {
        String c = "helloworld";
        String d = a + b;
        System.out.println(c == d);
    }

  虽然a、b用final修饰,也是常量,但是拼接成的d却不是常量,因为在编译器初始化a、b的static代码块是不执行的,因此是未知的,初始化属于类加载的一部分,属于运行期,从反编译的字节码来看,d是先通过 StringBuilder 拼接,再调用其 toString() 方法生成的。


  看看StringBuilder的源码,toString()方法调用了 new String(),所以会在堆内存创建一个新的对象。
上一篇 下一篇

猜你喜欢

热点阅读