Class常量池
class常量池可以理解为是Class文件中的资源仓库。Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译期生成的各种字面量和符号引用。
一个Class文件的16进制大体结构如下图:
对应的含义如下,细节可以查oracle官方文档:
对于我们java程序员一般不会人工解析这种字节码文件,我们可以通过javap命令生成更可读的JVM字节码指令文件:javap -v Math.class,红色的框就是class常量池信息,常量池中主要存放两大类常量:字面量和符号引用。
字面量:
指由字母、数字等构成的字符串和数值常量,字面量只可以右值出现,
例如:int a=1 这里a为左值,1为右值,1就是字面量。
符号引用:
符号引用是编译原理中的概念,是相对与直接引用来说,主要包括了以下三大类
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
例如:int a=1 ;a就是字段名称,就是一种符号引用。
运行时常量池
只有到运行时被加载到内存后,这些符号才有对应的内存地址,那么这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用会转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接。
字符串常量池
字符串常量池的位置
- jdk1.6及之前,有永久代,运行时常量池在永久代,运行时常量池包含字符串常量池。
- jdk1.7:有永久代,但是已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里。
- jdk1.8及以后:无永久代,运行时常量池在元空间,字符串常量池依然在堆里。
用一个程序证明字符串常量池在哪里:
运行结果:
- jdk7及以上:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- jdk6:Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
字符串常量池设计原理
字符串常量池底层是hotspot的c++实现的,类似一个HashTable,保存的本质上是字符串对象的引用。看下面的代码创建了多少个String对象?
1、在JDK1.6中,调用intern()首先会在字符串池中寻找equal()相等的字符串,例如字符串存在就返回该字符串在字符串池中的引用;假如字符串不存在,虚拟机会重新在永久代创建一个实例,将StringTable的一个表项指向这个新创建的实例:
2、在JDK1.7及以上版本,由于字符串不在永久代了,intern()做了一些修改,更方便的利用堆中的对象。字符串存在与jdk1.6一样,但是字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例。
由上面两个图,可以理解为什么jdk1.6字符串池溢出会抛出OutOfMemoryError:PermGen space,而在jdk1.7及以上版本抛出OutOfMemoryError:Java heap space.
String常量池的几种例子解析:
实例1:
String s0="zhigan";
String s1="zhigan";
String s2="zhi" + "gan";
System.out.println( s0==s1 ); //true
System.out.println( s0==s2 );true
分析:因为例子中的s0和s1中的“zhigan”都是字符串常量,他们在编译期就被确定了,所以s0=s1为true;而“zhi”和“gan”也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2同样在编译期被优化为一个字符串常量“zhigan”,所以s2也是常量池中的“zhigan”的一个引用,所以我们得出s0==s1==s2.
实例2:
String s0="zhigan";
String s1=new String("zhigan");
String s2=“zhi”+new String("gan");
System
.
out
.
println
(
s0
==
s1
);
// false
System
.
out
.
println
(
s0
==
s2
);
// false
System
.
out
.
println
(
s1
==
s2
);
// false
分析:用new String()创建的字符串不是常量,不能在编译期就确定,所用用new String()创建的字符串不放入常量池中,它们有自己的地址空间。s0还是常量池中"zhigna"的引用,s1因为无法在编译期确定,所以是运行时创建的新对象"zhigan"的引用,s2因为有后半部分new String("gan")所以也无法在编译期确定,所以也是一个新创建对象"zhigan"的引用。
实例3:
String a="a1";
String b="a"+1;
System
.
out
.
println
(
a
==
b
);
// true
String a="atrue";
String b="a"+"true";
System
.
out
.
println
(
a
==
b
);
// true
String a="a3.4";
String b="a"+3.4;
System
.
out
.
println
(
a
==
b
);
// true
分析:JVM对于字符串常量的"+"号连接,将在程序编译期,JVM就将常量字符串的"+"连接优化为连接后的值,故上面的程序最后的结果都为true.
实例4:
String a="ab";
String bb="b";
String b="a"+bb;
System
.
out
.
println
(
a
==
b
);
// false
分析:JVM对于字符串引用,由于字符串的"+"连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a"+bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b,所以上面的结果为false.(如果bb是一个方法的返回结果,同样的原因)
实例5:
String a="ab";
final String bb="b";
String b="a"+bb;
System
.
out
.
println
(
a
==
b
);
// true
分析:和例4中唯一不同的是bb字符串被final修饰,对于final修饰的变量,它在编译期被解析为常量值的一个本地拷贝存储到自己的常量池或嵌入到他的字节码流中,所以此时的"a"+bb和"a"+"b"效果是一样的,故结果为true.
实例6:
String s
=
"a"
+
"b"
+
"c"
;
//
就等价于
String s = "abc";
String a
=
"a"
;
String b
=
"b"
;
String c
=
"c"
;
String s1
=
a
+
b
+
c
;
分析:s1这个就不一样,可以通过观察器JVM指令码发现s1的"+"操作会变成如下:
StringBuilder temp=new StringBuilder();
temp.append(a).append(b).append(c);
String s=temp.toString();
八种基本类型的包装类和对象池
java中基本类型的包装类的大部分都实现了常量池技术,严格来说应该叫对象池,再堆上,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外 Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。因为一般这种比较小的数用到的概率相对较大。
例子截图如下:
补充:Integer a=12;底层实际上是执行了Integer.valueOf(12),里面用到了IntegerCache对象池