美文网首页
Java:常量池

Java:常量池

作者: 康小曹 | 来源:发表于2021-09-24 16:36 被阅读0次

一、概念

1. 常量池的概念

  • 基础概念

常量池在 Java 用于保存在编译期已确定的,已编译的 .class 文件中的一份数据。

类似于 oc 中编译 .m 文件之后生成的单个 mach-O 文件中的 __DATA、__DATA_CONST 段;

它包括了关于类,方法,接口等中的常量,也包括字符串常量,如String s = "java"这种申明方式;当然也可扩充,执行器产生的常量也会放入常量池,故认为常量池是 JVM 的一块特殊的内存空间。

  • 每个 Java 文件都有自己的静态常量池

每个 Java 文件编译为 .class 文件后,都将产生当前类独有的常量池,我们称之为静态常量池。

class 文件中的常量池包含两部分:字面值(literal)和符号引用(Symbolic Reference)。

其中字面值可以理解为 Java 中定义的字符串常量、final 常量、整数型常量等等。

符号引用指的是一些字符串,这些字符串表示当前类引用的外部类、方法、变量等的引用地址的抽象表示形式,在类被 jvm 装载并第一次使用这些符号引用时,这些符号引用将会解析为直接引用。符号常量包含:

  1. 类和接口的全限定名(包名+类型)
  2. 字段的名称和描述符
  3. 方法的名称和描述符

类似于 mach-O 文件中的符号表等信息;

  • JVM 装载类时,加载 .class 文件的静态常量池到方法区

JVM 在进行类装载时,将 .class 文件中常量池部分的常量加载到方法区中,此时方法区中的保存常量的逻辑区域称之为运行时常量区。

示例:

package com.company;

public class XKStringTest {
    static void Test(String param) {
        Integer a = 200;
        Integer b = 200;
        Integer c = 100;

        String s1 = "123";
    }
}

使用 javap -verbose 命令可以查看 class 字节码的详细信息,其中包含了编译期确定的静态常量池,提取出 constant pool 部分:

静态常量池

2. 常量池总结

几个重点:

  1. Java 类单独拥有静态常量池;
  2. 常量池包含两部分:字面量和符号引用;
  3. 在 JVM 进行类加载时,类中的静态常量池统一被复制到方法区,程序运行过程中使用到的也正是方法区的常量池,类中的常量池是编译之后产生的,是静态的,方法区的常量池是动态的,属于运行时;

二、包装类常量池的实现

先说结论:

8 种基础数据类型的包装类型中,除了 Float 和 Double ,其他 6 种都实现了常量池技术;

1. 5种基础类型的实现

Short/Long 都是 -128~127:

private static class ShortCache {
        private ShortCache(){}

        static final Short cache[] = new Short[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Short((short)(i - 128));
        }
    }

Character 为 0~127:

    private static class CharacterCache {
        private CharacterCache(){}

        static final Character cache[] = new Character[127 + 1];

        static {
            for (int i = 0; i < cache.length; i++)
                cache[i] = new Character((char)i);
        }
    }

Integer 默认为 -128~127,但是可配置,核心代码:

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

根据代码:

  1. 取出 VM 的配置 high;
  2. 取 high 和 127 较大者 i;
  3. 取 i 和 (MaxInteger + 127)的较小者为 h;
  4. 创建 low ~ h 的 Integer 对象;

验证:

public static void main(String[] args) {
    Integer i1 = 127;
    Integer i2 = 127;
    System.out.println(i1 == i2); // true

    Integer i3 = 128;
    Integer i4 = 128;
    System.out.println(i3 == i4); // false

    Character c1 = '2';
    Character c2 = '2';

    System.out.println(c1 == c2); // true
}

从代码上可以看出来,这些类型的的常量池实现方式说白了就是缓存。使用 final 将对应的缓存存放到常量区了,常量池中的缓存不可改变,而 String 则是可以不断新增的;

2. Bool类型也有常量池

Bool 就不多说了吧,就 2 个值;

Boolean b1 = true;
Boolean b2 = true;
Boolean b3 = false;
Boolean b4 = false;

// 内存地址相同
System.out.println(System.identityHashCode(b1));
System.out.println(System.identityHashCode(b2));

// 内存地址相同
System.out.println(System.identityHashCode(b3));
System.out.println(System.identityHashCode(b4));

System.out.println(b1 == b2); // ture
System.out.println(b3 == b4); // true

3. 总结

包装类型常量池

三、字符串常量池

之所以把字符串常量池单独列出,是因为:

  1. 字符串常量使用太过于频繁;
  2. 字符串常量相对包装类而言,实现的常量池功能更加丰富;
  3. 字符串常量池可以新增,而上文的 5 中包装类型的常量池不能新增;

在 C/C++ 中,编译器将多个编译期编译的文件链接成一个可执行文件或者 dll 文件,在链接阶段,符号引用被解析为实际地址,即相对文件首行的偏移。java 中这种链接是在程序运行时动态进行的。所以整个过程会产生很多常量,尤其是字符串常量,比如类名、方法名、包名等。这也是为什么字符串常量相对于包装类而言,具备更强大的功能;

1. 字符串常量地址创建过程

先说结论:

  1. 双引号的字符串如果在字符串常量池不存在则新创建;
  2. 新创建的字符串存在于堆中;
  3. 字符串常量池中存储的是堆中 String 对象的内存地址;
  4. 下次使用这个字符串时,检索到该字符串存在,则直接返回常量池中存储的引用(内存地址);

举个例子🌰:

String s = "123";
String s2 = new String("123");

这句代码做了几件事:

  1. 在字符串常量池检索是否存在 "123" 这个字符串;
  2. 第一次肯定不存在,此时使用字符串 "123" 在堆中创建一个字符串对象 mallocString;
  3. 在字符串常量池中添加 "123" 的索引,并将引用指向 mallocString;
  4. 将 mallocString 在堆中地址赋值给变量 s;
  5. new String("123") 实际调用 String(String original) 方法,需要传递一个字符串对象;
  6. 在字符串常量池中检索字符串 "123",这次能够找到,于是取出引用,赋值给 original 作为参数传递;
  7. 构造方法在堆中新生成一个字符串对象 mallocString2,并分配内存;
  8. 将 original 指向的对象,即 mallocString 的 value、hash 属性赋值给 mallocString2;
  9. 返回 mallocString2 的内存地址,赋值给 s2

因此,整个过程中,创建了 2 个 String 对象;

如果只有这一行代码:

String s = new String("123");

那么就这一句代码,就创建了两个 String 对象;

2. + 号的几种情况

    1. String对象 + String对象

创建一个 StringBuilder 对象,调用 append 方法,最后调用 toString 方法在堆中创建一个新的字符串对象;

    1. "xxx" + "xxx"

两边都是双引号时,直接进行字符串拼接然后存入静态常量区,常量区中只有最终的字符串。相当于编译器识别到这种情况直接进行了字符串替换;

    1. String + "xxx"

此时也是调用 StringBuilder 进行拼接后返回新的 String 对象。唯一区别就是 "xxx" 这里的对象是从常量池中取到的;

如下代码:

String s1 = "123";
String s2 = "456";
String s3 = s1 + s2;
String s4 = "123" + "456";

使用 javap -c xxx.class 反编译的结果中可以得到验证:

反编译结果
  1. 第一和第二句代码都是直接读取出了 .class 的静态常量区中的字符串常量;
  2. 第三句代码则进行了4个步骤;
  3. 第四局代码依然是直接读取字符串常量;

如果只有最后一句代码:

String s4 = "123" + "456";

反编译:

反编译g

如上,+ 的第二种情况会直接在静态常量区生成 1 个字符串常量,最终赋值给 s1;

使用 javap -verbose 查看 .class 中的静态常量区:

常量区

如上图所示,只有 "123456" 这个常量,没有 "123" 和 "456",因为编译器直接给替换成了 String s1 = "123456";

这里也可以通过 intern (后文会讲该方法)方法来验证:

代码1:

String s1 = "123" + "456";
String s2 = new String("1");
String s3 = new String("23");
String s4 = s2 + s3;
String s5 = s4.intern();

System.out.println(System.identityHashCode(s4));
System.out.println(System.identityHashCode(s5));

输出:

460141958
460141958

解释:
s4 和 s5 相等,证明 intern 返回的是 s4 的地址,所以 intern 走的是常量区无 "123" 的逻辑,即先在字符串常量区创建“123”的索引,然后将这个索引指向 s4 的内存地址,最后返回 s4 的内存地址;

代码2:

String s1 = "123" + "456";
String s2 = new String("1");
String s3 = new String("23");
String s4 = s2 + s3;
String s = "123";
String s5 = s4.intern();

System.out.println(System.identityHashCode(s4));
System.out.println(System.identityHashCode(s5));
System.out.println(System.identityHashCode(s));

输出:

460141958
1163157884
1163157884

解释:
此时 s5 和 s4 不相等,而 s5 和 s 相等。证明 intern 走的是字符串常量区存在 "123" 的逻辑,即直接返回字符串"123" 指向的地址,也就是 s 指向的地址;

打印时不能使用 string.hashCode 来判断内存地址是否相等,而应该使用 System.identityHashCode() ;因为 String 类重写了 Object 的 hashCode 方法,内部是对字符串进行 hash 算法并返回,所以字符串内容相等则 hashCode() 方法的返回值就都相等;而 System.identityHashCode() 无论对象是否重写了 hashCode 方法,都会调用 Object 中的 hashCode 方法;

3. + 号的效率问题

根源:

  1. 除了上述的第二种情况,"+" 号每次都会创建 StringBuilder 对象;
  2. StringBuilder 调用 append 方法最终调用 toString 方法,每次都会创建一个 String 对象;

就以上两点成了 “+” 效率问题的核心,一旦涉及到大数据量的字符串拼接,则会生成很多 StringBuilder 和 String 对象;

解决方案:

  • 使用 StringBuilder 的 append 方法,拼接完成后一次性生成新字符串对象;

另外还有一个 StringBuffer,这个对象只是相对于 StringBuilder 添加了 synchronized 关键字,即:同步执行。所以:

  • 多线程下使用 StringBuffer 更安全,单线程下使用 StringBuilder 更加有效;

具体源码和反编译源码自行查阅,不详细展开了;

4. internal() 方法

只有字符串有这个方法,8 种包装类都不具备此方法,这也是字符串优化的体现。5 种基础数据类型只会预先创建 256 个 Integer 对象存储在常量区,其余的不会存储进常量池,需要开发者自行管理堆内存(其实这个活也都被 GC 系统干了)。 而 String 需要动态添加的原因在于字符串使用实在太过于频繁;

该方法的意义(流程)在于:

  1. 判断常量池是否有这个 String 对象对应的字符串;
  2. 如果有,则返回常量池中对应字符串的引用地址,而不是 String 自身的地址;
  3. 如果没有,则在常量池中建立该字符串的索引,并在常量池中引用 String 对象(方法调用者)自身的内存地址,然后返回;

看一段代码:

public static void main(String[] args) {
        String s1 = new String("caoxk");
        String s2 = new String("_test");
        // 这里不是通过双引号"caoxk_test"生成
        // 这里是通过append生成 char[] 之后调用 new String方法生成的
        // 所以常量池中只有"caoxk"和"_test",并没有"caoxk_tests"
        String s3 = s1 + s2;
        
        // 此时常量池中没有"caoxk_test"
        String s4 = s3.intern();
        String s5 = "caoxk_test";

        System.out.println(System.identityHashCode(s3));
        System.out.println(System.identityHashCode(s4));
        System.out.println(System.identityHashCode(s5));
        System.out.println("s3 == s4:" + (s3 == s4));
        System.out.println("s4 == s5:" + (s4 == s5));
}

输出结果:

1846274136
1846274136
1846274136
s3 == s4:true
s4 == s5:true

上文中,当调用 s3.internal() 时,因为常量池中不存在字符串 "caoxk_test" 的索引,所以此时建立索引并引用了 s3 的地址,即:

  • 常量池"caoxk_test"索引中存储的地址即为 s3 在堆中的地址;

所以,当 s5 = "caoxk_test" 时,已经存在该字符串索引,于是将 s3 的地址返回,所以 s3 == s4 == s5;

再来看一段代码:

// 创建了两个 String 对象
String s1 = new String("caoxk");
// 创建了两个 String 对象
String s2 = new String("_test");

// 返回的不是 s1 的内存地址
// 因为 "caoxk" 这里已经生成了一个对象并被常量池引用
// 返回的是常量池中的引用,而不是 s1 的内存地址
String s3 = s1.intern();
// 返回的是常量池中的引用
String s4 = "caoxk";

System.out.println(System.identityHashCode(s1));
System.out.println(System.identityHashCode(s3));
System.out.println(System.identityHashCode(s4));
System.out.println("s1 == s3:" + (s1 == s3));
System.out.println("s3 == s4:" + (s3 == s4));

解释看注释,就不详细解释了;

四、总结

  1. 只要不是 "xxx" + "xxx" 这种形式,双引号修饰的字符串,都会存在于常量池中,因为没有时,会在堆中新创建一个对象并添加引用到常量池中;
  2. "xxx" + "xxx" 编译器识别后直接转换成了 "xxxxxx",所以最终只会生成一个字符串常量;
    • 号的本质是字符串拼接,只要左右任意一处有 String 对象,则会创建 StringBuilder 和 String 对象,这也是使用 + 号循环拼接字符串时效率低下的本质;
  3. 字符串循环拼接时可以先创建一个 StringBuilder 对象循环进行 append,完成后一次性生成一个 String 对象,如此只需要一个 StringBuild 和 一个 String 对象;
  4. 8 种举出数据类型中的 6 种都实现了运行时不可变的常量池,本质是常量区内存缓存(final);

相关文章

  • java基础类型、String类理解、版本对比、1.8新特性

    1、java基本数据类型及长度 2、jvm的常量池: JVM常量池浅析Java常量池理解与总结 Java中的常量池...

  • 细说Java常量池

      Java中的常量池有:class常量池、运行时常量池、String常量池。 为什么要使用常量池?   避免频繁...

  • java__常量池

    java的常量池分为两种型态:静态常量池和运行常量池 静态常量池: 即class文件中的常量池,这种常量池主要用于...

  • 常量池、运行时常量池、字符串常量池

    常量池、运行时常量池、字符串常量池 Java里包含各种常量池,经常傻傻分不清楚,下面就简单梳理下Java中的池们。...

  • JVM(六)JVM常量池

    1.常量池类型 Java中的常量池分为三种: 类文件常量池(静态常量池)(The Constant Pool)运行...

  • Java 内存—常量池

    Java中的常量池分为两种型态: 静态常量池 运行时常量池 静态常量池 所谓静态常量池是指class文件中的常量池...

  • 你对常量池够了解吗

    在 java 中,常量池分为以下三种: class 常量池 字符串常量池 运行时常量池 一、class 常量池 1...

  • 常量池

    Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。 静态常量池 : *.class文件中的常量池,...

  • Java中的常量池

    Java中的常量池分为三类:字符串常量池、class常量池、运行时常量池 字符串常量池 从1.7及其之后,字符串常...

  • String常量池

    java中有几种不同的常量池,以下的内容是对java中几种常量池的介绍以及重点研究一下字符串常量池。 class常...

网友评论

      本文标题:Java:常量池

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