Java-常量池

概述

  • 分类:Class常量池(又叫静态常量池)和运行时常量池,一般来说常量池指运行时常量池
  • Class文件中包含:类的版本、字段、方法、接口等描述信息,以及常量池
  • 常量池中存放编译期生成的各种字面量和符号引用
    • 字面量,由字母、数字等构成的字符串或数值常量
    • 符号引用,相对于直接引用,主要包括三类常量
      • 类与接口的全限定名
      • 字段的名称与描述符
      • 方法的名称与描述符
    • 例子:int a = 1; // a就是符号引用, 1就是一个字面量
    • 静态常量池装载到内存后变成运行时常量池,对应的符号引用转变为被加载到内存区域代码的直接引用(动态链接)
  • 常量池可以理解为一个缓存池,没有必要每次都创建
    • 字符串是比较基础的数据类型,大量频繁创建字符串,非常影响程序的性能
    • 思想:创建字符串常量时,先查询常量池是否存在该字符串,存在该字符串就直接返回该字符串的引用,不存在,就实例化该字符串放入池中。不就相当于一个缓存池?

字符串常量池

位置

  • JDK1.6及之前:有永久代,运行时常量池在永久代,运行时常量池包含字符串常量池
  • JDK1.7:有永久代,但是在逐步"去永久代",字符串常量池从永久代的运行时常量池分离到堆中
  • JDK1.8:无永久代,运行时常量池在元空间,字符串常量池依然在堆中
  • 如下图:
    常量池位置
  • 拓展(供参考):
    • 方法区是JVM的规范,是逻辑上的东西。JVM所有线程共享,用于存储类信息、常量池
    • 永久代和元空间是方法区的不同实现
    • 永久代容易OOM,所以JDK1.8以后就不再使用永久代实现方法区了
  • 因为现在大多使用JDK1.8及以上的版本,所以以下重点关注1.8以后常量池的特点

测试

/**
 * 测试常量池的位置
 *
 * 设置虚拟机参数(将内存大小调小点), VM Args: -Xms10M -Xmx10M
 */
public class TestStrLocation {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100000000; i++) {
            for (int j = 0; j < 1000000; j++) {
                // 生成大量的字面量
                list.add(String.valueOf((i + j) / 1000000).intern());
            }
        }
        System.out.println(list);

        // 内存溢出
        // 1.6以前: Exception in thread "main" java.lang.OutOfMemoryError: PermGen space(永久代)
        // 1.7及以上: Exception in thread "main" java.lang.OutOfMemoryError: Java heap space(堆)
    }
}

设计思想

  • 直接赋值
    • String s = "hello"; // s指向常量池中的引用
    • 说明:"hello"是一个字面量,会放在常量池中。先通过equals(key)方法,判断该字符串是否存在。存在:直接返回该对象在常量池中的引用,不存在:先创建,再返回引用
  • new对象
    • String s1 = new String("hello"); // s1指向内存中(堆)的对象引用
    • 说明:该方式会保证常量池与堆中都有该对象,但是返回的还是堆中的引用。检查常量池中是否有该对象,存在:直接在堆中创建对象,并返回该对象的引用。不存在:先在常量池中创建一个,再去内存中创建一个,然后返回内存中该对象的引用
  • intern方法
    • 示例
      • String s1 = new String("hello");
      • String s2 = s1.intern();
      • System.out.println(s1 == s2); // false
    • 说明:intern是一个native方法。调用intern方法,实际返回的是常量池中该对象的引用。存在:直接返回该字符串在常量池中的地址。不存在:则返回该字符串在堆中的地址
  • 关于"+"连接字符串
    • 示例
      • String s1 = a + b + c; // 三个变量相加(如果是常量相加会优化为一个字符串常量)
      • 等价于
      • StringBuilder temp = new StringBuilder();
      • temp.append(a).append(b).append(c);
      • String s2 = temp.toString(); // 只有堆中的一个对象, 常量池不会有abc变量连接后的字符串常量
    • 说明:即"+"连接字符串,实际就是调用append方法以及toString
  • 补充:字符串常量池底层是hotspot的C++实现的,底层类似于Hashtable这样的key-value结构,保存的本质上还是字符串对象的引用(也就是说常量池其实就是一个key-value的表,key类似于指针,指向内存中的某区域,该区域放着对象)

案例分析

示例1

以下代码输出结果?以及创建的对象个数?

String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2); // JDK1.6: false, JDK1.7及以上: true
// JDK1.6: 创建了6个对象, JDK1.7及以上: 创建了5个对象
  • 说明:区别在于intern方法在不同版本的实现方式不同
  • 相同:new对象的方式,会创建两个对象,两个new对象创建4个对象。连接字符串会在堆中生成一个"hello"对象(注意:hello并不是字面量,而是拼接得到的,不会在常量池创建一个hello,编译期不能确定这个s1是不是hello),这里一共创建了5个对象。
  • 不同:JDK1.6,调用intern会在复制一份s1到常量池,然后key指向常量池中"复制s1",此时"复制s1"的地址自然和s1的地址不同,结果为false。JDK1.7,不会进行复制,也就不会再创建一个对象,key直接指向s1,二者的地址自然相同,结果为true
  • 见下图
    JDK1.6
    JDK1.7

示例2

下面的结果是否相等(==比较地址,equals比较字面量)

String a = "a1";
String b = "a" + 1; // 1也是字面量
System.out.println(a == b); // true

String s = "a" + "b" + "c"; // 编译期优化为"abc", 因为三个常量都是不会发生变化
String s1 = "abc";
System.out.println(s == s1); // true

示例3

下面的结果是否相等

String abc = "abc";
String abc1 = "ab" + new String("c"); // 会在堆上开辟新空间存放abc
System.out.println(abc == abc1); // false
        
String ab = "ab";
String b = "b";
String ab2 = "a" + b; // b是变量, 编译期无法确定是否不变,同样开辟新空间
System.out.println(ab == ab2); // false

示例4

下面的结果是否相等

public static void test() {
    String ab = "ab";
    String b = "b";
    String ab2 = "a" + b;
    System.out.println(ab == ab2); // false

    String ab3 = "ab";
    final String b2 = "b"; // final修饰的变量不可被修改, 常量
    String ab4 = "a" + b2; // 编译期可以确定b2不会被修改
    System.out.println(ab3 == ab4); // true

    String ab5 = "ab";
    // final修饰变量不可变,是指b3引用的值不变,此时b3的值也是一个引用,这个引用所指向的内存空间的数据还是能变化的
    final String b3 = getB(); // 只有调用方法后,才能确定该值,编译期无法确定是否变化
    String ab6 = "a" + b3;
    System.out.println(ab5 == ab6); // false
}

public static String getB() {
    return "b";
}

示例5

下面的结果是否相等

// 等价于 new String("计算机") + new String("技术")
// 堆中创建一个"计算机技术", 一个"计算机", 一个"技术"
// 常量池中创建一个"计算机", 一个"技术"
String a = new StringBuilder("计算机").append("技术").toString();
System.out.println(a == a.intern()); // true

// 等价于 new String("ja") + new String("va")
// 堆中创建一个"java", 一个"ja", 一个"va"
// 常量池中创建一个"ja", 一个"va"
// 常量池中还有一个"java", 因为java是关键字,JVM初始化时,其他相关类早就放进去了
String b = new StringBuilder("ja").append("va").toString();
System.out.println(b == b.intern()); // false

// 等价于 String c = new String("abc");
// 堆中创建一个"abc"
// 常量池创建一个"abc"
String c = new StringBuilder("abc").toString();
System.out.println(c == c.intern()); // false

代码中出现了字面量,一定会放到常量池中。如果使用new方式,还会在堆中创建,连接字符串变量所产生的字符串会创建在堆中,而不会在常量池中创建

对象池

  • 基本类型的包装类大部分都实现了常量池技术(或者说"对象池",在堆上),包括:Byte、Short、Integer、Long、Character、Boolean,两种浮点型没有实现
  • 只有在对应值小于等于127时才可以使用对象池,因为一般只有较小的数用到的概率较大

设计思想

Integer.valueOf 方法,其他包装类类似

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

案例分析

示例1

下面的结果是否相等

public class TestCache {
    public static void main(String[] args) {

        // 小于等于127, IntegerCache对象池中有
        Integer i1 = 127; // 实际上调用:Integer.valueOf(127)
        Integer i2 = 127;
        System.out.println(i1 == i2); // true

        // 大于127, 对象池中没有
        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4); // false

        // 使用new的方式创建, 不使用对象池
        Integer i5 = new Integer(127);
        Integer i6 = new Integer(127);
        System.out.println(i5 == i6); // false

        // Boolean也使用了对象池技术
        Boolean b1 = true;
        Boolean b2 = true;
        System.out.println(b1 == b2); // true

        // 浮点型没有使用对象池技术
        Double d1 = 1.0;
        Double d2 = 1.0;
        System.out.println(d1 == d2); // false
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值