美文网首页Java
细说Java常量池

细说Java常量池

作者: 小胡_鸭 | 来源:发表于2020-07-14 23:14 被阅读0次

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

    为什么要使用常量池?

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

    一、class常量池

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

    1、字面量

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

    • 八种基本类型的值,eg: 1、1.0、true、'a'
    • 文本字符串,eg: "hello world"
    • 被声明为final的常量

    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常量池中包含的常量应该有:

    字面量

    • 字符串:“Hello World”
    • 被final修饰的基本类型值:100

    符号引用

    • 类和接口的全限定名:basic/ConstantsTestObject
    • 字段的名称和描述符:basic/ConstantsTest.name:Ljava/lang/String;basic/ConstantsTest.num:I
    • 方法名称和描述符:java/lang/Object."<init>":()V(构造方法)、infojava/io/PrintStream.println:(Ljava/lang/String;)V(第一个print)、java/io/PrintStream.println:(I)V(第二个print)等

      将类编译出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、字符串常量池的存储位置

    • JDK6.0及之前版本中,String Pool里放的都是字符串常量,这些常量都放在Perm Gen区(也就是方法区)中;
    • JDK7.0中,String Pool里放的实际上是字符串对象的引用,对象的实体存储被转移到堆内存中,这样做是因为方法区存储空间有限,一旦常量池过大会导致OOM。

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

    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));
    

      上面代码执行结果如下:



      分析:

    • s1 == s2:都指向常量池中的字符串,所以true。
    • s1 == s3:虽然用了+号做字符串连接,但是这个操作对编译器来说是可预测的,所以会进行优化,自动生成Hello赋值给s3,s3同样指向常量池中的字符串,所以true。
    • s1 == s4:s4是分别用了常量池中的字符串和存放对象的堆中的字符串,做+的时候会进行动态调用,最后生成的仍然是一个String对象存放在堆中,所以false。
    • s1 == s5:s5使用new创建的对象,会在堆内存中分配一个新的内存空间,所以false。
    • s4 == s5:每次使用new创建的对象都是新分配内存空间,不会相等,所以false。
    • s1 == s6:s5是使用String#intern()生成的,方法首先在常量池中查找是否存在一份equal相等的字符串如果有的话就返回该字符串的引用,没有的话就将它加入到字符串常量池中,所以存在于class中的常量池并非固定不变的,可以用intern方法加入新的,这里很明显在常量池中找到equal的字符串,所以为true。
    • s5 == s6:不管常量池中是否存在跟s5字符串值equal的常量,s6最终都是指向常量池中的常量,所以结果肯定是false。
    • s1 == s9:虽然s7、s8都是指向常量池中的常量,但是s9的生成用的是动态调用,所以返回的是一个新的String对象,所以结果是false。
    • s5 == s9:false,分析同上。

        除此之外,还有一些特例:

    (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(),所以会在堆内存创建一个新的对象。

    相关文章

      网友评论

        本文标题:细说Java常量池

        本文链接:https://www.haomeiwen.com/subject/otovqktx.html