常量池

Class常量池与运行时常量池

Class常量池可以理解为Class文件中的资源仓库,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池Constant pool,用于存放编译期生成的各种字面量符号引用。一个class文件的16进制大体结构如下:

class文件16进制结构

对应的含义如下:

class文件16进制含义

一般不会去人工解析这种16进制的字节码文件,一般可以通过javap命令或一些字节码工具生成更可读的JVM字节码指令文件,如下所示,Constant pool即是class常量池信息,常量池中主要存放字面量符号引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
Classfile /E:/SourceCode/JVM-Learn/target/classes/com/eleven/icode/jvm/unit/Math.class
Last modified 2021-8-21; size 999 bytes
MD5 checksum 333e6820383da465bfe69370b8b00ae4
Compiled from "Math.java"
public class com.eleven.icode.jvm.unit.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#39 // java/lang/Object."<init>":()V
#2 = Class #40 // com/eleven/icode/jvm/unit/Math
#3 = Methodref #2.#39 // com/eleven/icode/jvm/unit/Math."<init>":()V
#4 = Methodref #2.#41 // com/eleven/icode/jvm/unit/Math.compute:()I
#5 = Fieldref #42.#43 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #44 // test
#7 = Methodref #45.#46 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #47 // com/eleven/icode/jvm/entity/User
#9 = Methodref #8.#39 // com/eleven/icode/jvm/entity/User."<init>":()V
#10 = Fieldref #2.#48 // com/eleven/icode/jvm/unit/Math.user:Lcom/eleven/icode/jvm/entity/User;
#11 = Class #49 // java/lang/Object
#12 = Utf8 initData
#13 = Utf8 I
#14 = Utf8 ConstantValue
#15 = Integer 666
#16 = Utf8 user
#17 = Utf8 Lcom/eleven/icode/jvm/entity/User;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/eleven/icode/jvm/unit/Math;
#25 = Utf8 compute
#26 = Utf8 ()I
#27 = Utf8 a
#28 = Utf8 b
#29 = Utf8 c
#30 = Utf8 main
#31 = Utf8 ([Ljava/lang/String;)V
#32 = Utf8 args
#33 = Utf8 [Ljava/lang/String;
#34 = Utf8 math
#35 = Utf8 MethodParameters
#36 = Utf8 <clinit>
#37 = Utf8 SourceFile
#38 = Utf8 Math.java
#39 = NameAndType #18:#19 // "<init>":()V
#40 = Utf8 com/eleven/icode/jvm/unit/Math
#41 = NameAndType #25:#26 // compute:()I
#42 = Class #50 // java/lang/System
#43 = NameAndType #51:#52 // out:Ljava/io/PrintStream;
#44 = Utf8 test
#45 = Class #53 // java/io/PrintStream
#46 = NameAndType #54:#55 // println:(Ljava/lang/String;)V
#47 = Utf8 com/eleven/icode/jvm/entity/User
#48 = NameAndType #16:#17 // user:Lcom/eleven/icode/jvm/entity/User;
#49 = Utf8 java/lang/Object
#50 = Utf8 java/lang/System
#51 = Utf8 out
#52 = Utf8 Ljava/io/PrintStream;
#53 = Utf8 java/io/PrintStream
#54 = Utf8 println
#55 = Utf8 (Ljava/lang/String;)V

字面量

字面量就是指由字母、数字等构成的字符串或者数值常量,字面量只可以右值出现,所谓右值是指等号右边的值,如int a=1这里的a为左值,1为右值即字面量。

符号引用

符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括类和接口的全限定名字段的名称和描述符方法的名称和描述符三类常量;如int a=1中a就是字段名称,就是一种符号引用,还有Math类常量池里#24对应的Lcom/eleven/icode/jvm/unit/Math;就是类的全限定名maincompute方法名称()是一种UTF8格式的描述符这些都是符号引用

运行时常量池

这些常量池现在是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成运行时常量池,对应的符号引用在程序加载运行时会被转变为被加载到内存区域的代码的直接引用,也就是我们说的动态链接,如compute()这个符号引用在运行时会被转变为compute()方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用

字符串常量池

字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型大量频繁的创建字符串,极大程度地影响程序的性能,JVM为了提高性能减少内存开销,在实例化字符串常量的时候进行了一些优化为字符串开辟一个字符串常量池,类似于缓存区,创建字符串常量时,首先查询字符串常量池是否存在该字符串,存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

直接赋值字符串

String param = "eleven"; 代码中,param指向常量池中的引用,这种方式创建的字符串对象,只会在常量池中,因为"eleven"这个字面量,创建对象param时,JVM会先去常量池中通过equals(key)方法,判断是否有相同的对象若有,则直接返回该对象在常量池中的引用;若没有则会在常量池中创建一个新对象,再返回引用

new String() 方式

String name = new String("eleven");代码中,name指向内存中的对象引用,这种方式会保证字符串常量池堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。因为有"eleven"这个字面量,故会先检查字符串常量池中是否存在字符串"eleven",若不存在,先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象"eleven";存在的话,就直接去堆内存中创建一个字符串对象"eleven";最后将内存中的引用返回

intern方法

1
2
3
String s1 = new String("zhuge");
String s2 = s1.intern();
System.out.println(s1 == s2); //false

String中的intern方法是一个native方法,当调用intern方法时,equals(oject)方法确定字符串常量池是否已经包含一个等于此String对象的字符串,若有则返回池中的字符串,否则将intern返回的引用指向当前字符串s1jdk1.6需要将s1复制到字符串常量池里

字符串常量池位置

Jdk1.6及之前:有永久代,运行时常量池在永久代运行时常量池包含字符串常量池
Jdk1.7:有永久代,但已经逐步去永久代,字符串常量池从永久代里的运行时常量池分离到堆里
Jdk1.8及之后: 无永久代,运行时常量池在元空间字符串常量池里依然在堆里

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* jdk6:-Xms6M -Xmx6M -XX:PermSize=6M -XX:MaxPermSize=6M
* jdk8:-Xms6M -Xmx6M -XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 100000000; i++) {
String str = String.valueOf(i).intern();
list.add(str);
}
}
}

运行结果:

1
2
3
4
jdk7及以上:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
jdk6:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

字符串常量池设计原理

字符串常量池底层是hotspot的C++实现的,底层类似一个HashTable,保存的本质上是字符串对象的引用。如下代码创建了多少个对象:

1
2
3
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();
System.out.println(s1 == s2);

JDK 1.6下输出是false,创建了 6 个对象,在JDK 1.7及以上的版本输出是true,创建了 5 个对象,这里没有考虑GC,但这些对象确实存在或存在过。产生这种变化的原因主要是,字符串常量池从永久代中脱离移入堆区的原因,intern()方法也相应发生了变化:

JDK 1.6中,调用intern()首先会在字符串池中寻找 equal() 相等的字符串,若字符串存在则返回该字符串在字符串常量池中的引用;若字符串不存在,虚拟机会重新在永久代上创建一个实例,将StringTable的一个表项指向这个新创建的实例

jdk1.6字符串常量池原理

JDK 1.7及以上版本中,由于字符串常量池不在永久代了,intern() 做了一些修改,更方便地利用堆中的对象。字符串存在时和JDK 1.6一样,字符串不存在时不再需要重新创建实例,可以直接指向堆上的实例

jdk1.7字符串常量次原理

实例分析

1
2
3
4
5
6
String s0 = "eleven";
String s1 = new String("eleven");
String s2 = "ele" + new String("ven");
System.out.println( s0==s1 ); // false
System.out.println( s0==s2 ); // false
System.out.println( s1==s2 ); // false

new String()创建的字符串不是常量不能在编译期就确定,故new String()创建的字符串不放入常量池中,它们有自己的地址空间。s0还是常量池中”eleven”的引用,s1因为无法在编译期确定,所以是运行时创建的新对象”eleven”的引用,s2因为有后半部分new String(”ven”)故也无法在编译期确定,故也是一个新创建对象”eleven”的引用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

String a = "ab";
String bb = "b";
String b = "a" + bb;
System.out.println(a == b); // false

String a = "ab";
final String bb = getBB();
String b = "a" + bb;
System.out.println(a == b); // false
private static String getBB() {
return "b";
}

JVM对于字符串常量+号连接,将在程序编译期,JVM就将常量字符串的+连接优化为连接后的值,拿"a" +1来说,经编译器优化后在class中就已经是a1。

JVM对于字符串引用,由于在字符串的+连接中,有字符串引用存在,而引用的值在程序编译期是无法确定的,即"a" + bb无法被编译器优化,只有在程序运行期来动态分配并将连接后的新地址赋给b

JVM对于字符串引用bb,它的值在编译期无法确定,只有在程序运行期调用方法后,将方法的返回值和”a”来动态连接并分配地址为b。

String是不可变的

1
2
3
4
5
String s = "a" + "b" + "c";
String a = "a";
String b = "b";
String c = "c";
String s1 = a + b + c;

由上面的例子可知s就等价于String s = "abc",s1就不一样了,通过观察其JVM指令码发现s1的”+”操作会变成如下操作:

1
String s1 = (new StringBuilder()).append(a).append(b).append(c).toString()

字符串常量池中会存在计算机技术,堆内存中存储的是str引用的对象计算机技术,由于StringBuildertoString方法返回的是一个new String()对象,该对象才是真正返回的对象引用,没有出现计算机技术字面量,在常量池中不会生成计算机技术对象,故str.intern()返回的也是对象在堆中的引用。

1
2
String str = new StringBuilder("计算机").append("技术").toString();
System.out.println(str == str.intern()); // true

下面的例子和上面的类似,但唯一区别在于java是关键字,在JVM初始化相关类里肯定早就放入字符串常量池中,故str.intern()返回的是常量池中对象。

1
2
String str = new StringBuilder("ja").append("va").toString();
System.out.println(str == str.intern()); // false

下面的例子和上面的类似,但唯一区别在于这里的test是字面量,故会在常量池中创建该对象,故str.intern()返回的是常量池中对象。

1
2
String str = new String("test");
System.out.println(str == str.intern()); // false

八种基本类型的包装类和对象池

java基本类型包装类的大部分在堆上都实现了对象池技术,这些类是Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外Byte,Short,Integer,Long,Character这5种整型的包装类也只是在对应值大于等于-128小于等于127时才可使用对象池,也即对象不负责创建和管理大于127的这些类的对象。因为一般这种比较小的数用到的概率相对较大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {
public static void main(String[] args) {
//5种整形的包装类Byte,Short,Integer,Long,Character的对象,
//在值小于127时可以使用对象池
Integer i1 = 127; //这种调用底层实际是执行的Integer.valueOf(127),里面用到了IntegerCache对象池
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 bool1 = true;
Boolean bool2 = true;
System.out.println(bool1 == bool2);//输出true
//浮点类型的包装类没有实现对象池技术
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1 == d2);//输出false
}
}

如一下源码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private static class LongCache {
private LongCache(){}

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

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

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

private static class ByteCache {
private ByteCache(){}

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

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

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));
}
}

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);
}
}