JVM系列-深究Java中的常量池

1,954 阅读8分钟

1、基本概念:

1.1、常量:

  • 常量是用final修饰的变量或者是在编译时期定义好的字符串。
  • 常量在类编译时期载入类的常量池中。

1.2、什么是字面量和符号引用:

  • 字面量包括:1.文本字符串 2.八种基本类型的值 3.被声明为final的常量等;
  • 符号引用包括:
    • 用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
    • 它与直接引用区分一下,直接引用一般是指向方法区的本地指针。
    • 一般包括三类常量:1.类和方法的全限定名 2.字段的名称和描述符 3.方法的名称和描述符。
    • 当虚拟机运行时,需要从常量池获得对应的符号引号,再在类创建或运行时解析、翻译到具体的内存地址之中(直接引用)。

1.3、常量池的好处:

  • 常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
  • 例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中:
    • 节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
    • 节省运行时间:比较字符串时,==equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。

2、静态常量池(也称class常量池,Class Constant Pool):

  • *.class文件中的常量池,我们写的每一个Java类被编译后,就会形成一份class文件;class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是静态常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References),占用了class文件绝大部分空间。(编译时期)
  • 每个class文件都有一个class常量池。

3、运行时常量池(Runtime Constant Pool):

  • jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。(运行时期)
  • 运行时常量池存在于JVM内存的方法区中,用来存储编译期间生成的字面量和符号引用。
  • 当类加载到JVM内存中后,jvm就会将class常量池中的内容存放到方法区的运行时常量池中,由此可知,运行时常量池也是每个类都有一个。
  • Java语言并不要求常量一定只有编译期才能生成,也就是并非置入Class文件中常量池的内容才能进入方法区运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String类的intern()方法。
  • 在类解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是我们上面所说的StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。
  • 当常量池无法再申请到内存时也会抛出OutofMemoryError异常。

4、字符串常量池:【属于运行时常量池,只存储字符串】

4.1、字符串常量池在Java内存区域的哪个位置:

  • JDK7之前版本字符串常量池是放在方法区中的,不过自从JDK7之后,Hotspot虚拟机便将原本放在永久代的字符串常量池移至堆中。

4.2、字符串常量池是什么:

  • 在HotSpot里实现的string pool功能的是一个固定大小的HashTable,它是一个Hash表,默认值大小长度是1009;这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量由一个一个字符组成,放在了StringTable上,StringTable里面存的是key(字面量“abc”, 即驻留字符串)-value(字符串"abc"实例对象在堆中的引用)键值对。
  • 在JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String#intern()时会需要到链表上一个一个找,从而导致性能大幅度下降;
  • 在JDK7.0中,StringTable的长度可以通过参数指定: -XX:StringTableSize=66666

4.3、字符串常量池里放的是什么:

  • 在JDK6.0及之前版本中,String Pool里放的都是字符串常量;
  • 在JDK7.0中,由于String#intern()发生了改变,因此String Pool中也可以存放放于堆内的字符串对象的引用地址。
  • 需要说明的是:字符串常量池中的字符串只存在一份!
    String s1 = "hello,world!";
    String s2 = "hello,world!";
	//即执行完第一行代码后,常量池中已存在 “hello,world!”,那么 s2不会在常量池中申请新的空间,而是直接把已存在的字符串内存地址返回给s2。

4.4、String a= "ha" 与 String a = new String("ha")区别:

  • 直接定义的"ha"是储存在字符串常量池中;new String(“ha”)是String存储在堆中,“ha”在字符串常量池中;
  • 常量池中相同的字符串只会有一个,但是new String(“ha”)每new一个对象就会在堆中新建一个对象,不管这个值是否相同;
  • String a = “ha”与String b = “ha”:a b都指向字符串常量池中的“ha”,所以 a==b ;
  • String a = new String(“ha”)与String b = new String(“ha”):是会在堆中创建两个对象,这两个对象的值都为ha,所以a!=b;a.equals(b)返回true;
  • String a =“ha”在编译阶段就会在内存中创建,String a = new String(“ha”)是在运行时才会在堆中创建对象。

4.5、字符串拼接情况:

        使用 ” ” 双引号创建 : String s1 = “first”;
	使用字符串连接符拼接 : String s2=”se”+”cond”;
	使用字符串加引用拼接 : String s12=”first”+s2;
	使用new String(“”)创建 : String s3 = new String(“three”);
	使用new String(“”)拼接 : String s4 = new String(“fo”)+”ur”;
	使用new String(“”)拼接 : String s5 = new String(“fo”)+new String(“ur”);
	s1 : 中的”first” 是字符串常量,在编译期就被确定了,先检查字符串常量池中是否含有”first”字符串,若没有则添加”first”到字符串常量池中,并且直接指向它。所以s1直接指向字符串常量池的”first”对象。
	s2:“se”和”cond”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被解析为一个字符串常量,并且s2是常量池中”second”的一个引用。
	s12 : JVM对于字符串引用,由于在字符串的”+”连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即("first"+s2)无法被编译器优化,只有在程序运行期来动态分配使用StringBuilder连接后的新String对象赋给s12。
	(编译器创建一个StringBuilder对象,并调用append()方法,最后调用toString()创建新String对象,以包含修改后的字符串内容)
	s3 : 用new String() 创建的字符串不是常量,不能在编译期就确定,所以new String() 创建的字符串不放入常量池中,它们有自己的地址空间。但是”three”字符串常量在编译期也会被加入到字符串常量池(如果不存在的话)
	s4 : 同样不能在编译期确定,但是”fo”和”ur”这两个字符串常量也会添加到字符串常量池中,并且在堆中创建String对象。(字符串常量池并不会存放”four”这个字符串,会存放“fo”这个字符串)

4.6、final引用拼接:

	public class StringConcat {
	    final String a = "hello";
	    final String b = "moto";
	    String result = a + b + "2018";
	}
	• 对于final修饰的局部变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中。
	• 所以此时的(a + b + “2018”)和(“hello” + “moto” + “2018”)效果是一样的。

4.7、intern()方法:

  • String.intern( )是一个Native方法,它的作用是:如果字符串常量池已经包含一个等于此String对象的字符串,则返回代表字符串常量池中这个字符串的String对象。【并不是堆中的字符串对象,而是字符串常量池中的字符串对象】;否则将此String对象的引用地址(堆中)添加到字符串常量池中,因为字符串常量池在堆中,所以没必要在字符串常量池中再多创建一份String对象。
  • jdk 1.7 后的字符串常量池存在于堆中。
String aa=new String("abcdef");//"abcdef"字符串对象,创建在字符串常量池
String aaIntern=aa.intern();//aaIntern为字符串常量池的"abcdef"对象
System.out.println("aa==aaIntern    "+(aa==aaIntern));//false   
String aaStr="abcdef";
System.out.println("aaIntern==aaStr "+(aaIntern==aaStr));//true
System.out.println("aa==aaStr   "+(aa==aaStr));//false  aa在堆,aaStr在字符串常量池	       ——————————————  ——————————————  ——————————————  ——————————
String bb = new String("123") + "456";
String bbIntern = bb.intern();
System.out.println("bb==bbIntern    " + (bb == bbIntern));// true,字符串常量池没有"123456"
String bbStr = "123456";
System.out.println("bb==bbStr   " + (bb == bbStr));// true
System.out.println("bbIntern==bbStr " + (bbIntern == bbStr));// true
String cc = new String("1") + "23";
String ccIntern = cc.intern();// 字符串常量池已经有"123"
System.out.println("cc==ccIntern    " + (cc == ccIntern));// false

参考:tech.meituan.com/2014/03/06/…