细说Java常量池

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336