文章目录
1. Class文件常量池
class文件是一组二进制字节流,class文件常量池在编译阶段就已经确定。class文件常量池主要存放两大常量:字面量和符号引用。
class JavaBean{
private int value = 1;
public String s = "abc";
public final static int f = 0x101;
public void setValue(int v){
final int temp = 3;
this.value = temp + v;
}
public int getValue(){
return value;
}
}
通过 javac
命令编译之后,用 javap -v
命令查看编译后的文件:
class JavaBasicKnowledge.JavaBean
minor version: 0
major version: 52
flags: ACC_SUPER
Constant pool:
#1 = Methodref #6.#29 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#30 // JavaBasicKnowledge/JavaBean.value:I
#3 = String #31 // abc
#4 = Fieldref #5.#32 // JavaBasicKnowledge/JavaBean.s:Ljava/lang/String;
#5 = Class #33 // JavaBasicKnowledge/JavaBean
#6 = Class #34 // java/lang/Object
#7 = Utf8 value
#8 = Utf8 I
#9 = Utf8 s
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 f
#12 = Utf8 ConstantValue
#13 = Integer 257
#14 = Utf8 <init>
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 LJavaBasicKnowledge/JavaBean;
#21 = Utf8 setValue
#22 = Utf8 (I)V
#23 = Utf8 v
#24 = Utf8 temp
#25 = Utf8 getValue
#26 = Utf8 ()I
#27 = Utf8 SourceFile
#28 = Utf8 StringConstantPool.java
#29 = NameAndType #14:#15 // "<init>":()V
#30 = NameAndType #7:#8 // value:I
#31 = Utf8 abc
#32 = NameAndType #9:#10 // s:Ljava/lang/String;
#33 = Utf8 JavaBasicKnowledge/JavaBean
#34 = Utf8 java/lang/Object
1.1. 字面量
字面量接近 java 语言层面的常量概念。
【字符串】
字符串的值。
// public String s = "abc"; 中的 "abc"。
#9 = Utf8 s
#3 = String #31
#31 = Utf8 abc
【final 修饰变量】
包括成员变量、静态变量、实例变量和局部变量。
// public final static int f = 0x101; 中的0x101。
#11 = Utf8 f
#12 = Utf8 ConstantValue
#13 = Integer 257
【注意】
- 字面量指的是数据的值,class文件常量池中一般指字符串的值和常量的值。
- 对于基本类型数据和局部变量,他们的字面量不会存在于class文件常量池。
// private int value = 1; 常量池中只保留了字段描述符 I 和字段名称 value
#7 = Utf8 value
#8 = Utf8 I
1.2. 符号引用
符号引用主要设涉及编译原理方面的概念。
【类和接口的全限定名】
将类名中原来的 “.” 替换为 “/” 得到的,主要用于在运行时解析得到类的直接引用。
#5 = Class #33
#33 = Utf8 JavaBasicKnowledge/JavaBean
【字段名称和描述符】
类或者接口中声明的变量名称和类型,包括类变量和实例变量。
#4 = Fieldref #5.#32
#5 = Class #33
#32 = NameAndType #7:#8
// 基本数据类型的变量,常量池只保留字段名称和字段描述符
// private int value = 1;
#7 = Utf8 value // 变量名
#8 = Utf8 I // 变量类型
// 形参变量和局部变量,常量池只保留字段名称
// public void setValue(int v) {}
#23 = Utf8 v // 形参变量名
// final int temp = 3;
#24 = Utf8 temp // 局部变量名
【方法名称和描述符】
方法的名称和参数类型、返回值类型
// public void setValue(int v) {}
#21 = Utf8 setValue // 方法名
#22 = Utf8 (I)V // 参数类型 I 返回值类型 V
#25 = Utf8 getValue // 方法名
#26 = Utf8 ()I // 参数类型空 返回值类型 I
2. 运行时常量池
运行时常量池是方法区的一部分,不同的类共用一个运行时常量池。
- Class 加载过程,class 文件常量池会进入运行时常量池。
- 多个 class 文件常量池中相同的字符串在运行时常量池只会存在一份。
- class 文件常量池中的符号引用,会在 Class 的解析阶段翻译成直接引用,一起存储在运行时常量池中。
- 运行时常量池具有动态性,其中的内容并不全部来自 class 常量池,在运行时可以通过代码生成常量并将其放入,这种特性被用的最多的就是
String.intern()
。
3. 字符串常量池
字符串常量池和运行时常量池不是一个概念。
- JDK 1.7 以前字符串常量池和运行时常量池都在方法区中。
- JDK 1.7 及以后运行时常量池在方法区中,字符串常量池在堆中。
3.1. 创建字符串对象的方式
3.1.1. 字面量创建
String s = "abc";
- JVM 先去字符串常量池中查找
equals("abc")
的对象。 - 如果有则不用创建,直接将字符串常量池中的
"abc"
对象的引用赋值给s
。 - 如果没有则在堆中创建一个新的
"abc"
对象,并将堆中的"abc"
对象的引用驻留一份在字符串常量池中,再将字符串常量池中的"abc"
对象的引用赋值给s
。
s => 字符串常量池中(同时也是堆中)的 “abc” 对象。且无论创建多少次,只要字符串的值相同,它们都指向堆中的同一个对象。
3.1.2. 构造方法创建
String s = new String("abc");
- JVM 先去字符串常量池中查找
equals("abc")
的对象。 - 如果有则在堆中创建一个新的
"abc"
对象。 - 如果没有则在堆和字符串常量池中各创建一个新的
"abc"
对象。 - 将字符串常量池中的
"abc"
对象的引用赋值给s
。
s => 堆中的 “abc” 对象。
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = new String("Hello");
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
// true。 s1和s2都指向字符串常量池中的同一个"Hello"对象
System.out.println(s1 == s2);
// true。 s3会在编译时合并成"Hello"对象,因此s1和s3都指向字符串常量池中的同一个"Hello"对象
System.out.println(s1 == s3);
// false。s4只会创建"Hel"对象,"lo"对象会在堆中创建,因此s1和s4指向的对象不同
System.out.println(s1 == s4);
// false。s1指向字符串常量池中的"Hello"对象,s5指向堆中的"Hello"对象
System.out.println(s1 == s5);
// true。 s1指向字符串常量池中的"Hello"对象,s5.intern()会返回字符串常量池中的"Hello"对象
System.out.println(s1 == s5.intern());
// false。s9会在运行时动态赋值,因此s1和s9指向的对象不同
System.out.println(s1 == s9);
// false。s5和s6指向堆中不同的"Hello"对象
System.out.println(s5 == s6);
// true。 s5.intern()和s6.intern()都会返回字符串常量池中的同一个"Hello"对象
System.out.println(s5.intern() == s6.intern());
3.2. intern 方法
将对应的字面量进行特殊处理。
- JDK 1.6及以前:检查字面量对象是否在字符串常量池中,如果存在则直接返回字符串常量池的对象,如果不存在,则在字符串常量池创建并返回该字面量对象。
- JDK 1.7及以后:检查字面量对象是否在字符串常量池中,如果存在则直接返回字符串常量池的对象,如果不存在,则把堆中的字面量对象的引用添加到字符串常量池中,并返回该字面量对象。
4. 包装类对象常量池
java 基本类型的包装类 Byte,Short,Integer,Long,Character,Boolean 实现了常量池技术,而 Float 和 Double 则没有实现。
另外上面这 6 种整型的包装类也只是在对应值在 [-128,127] 区间时才使用缓存数据, 超出此范围仍然会去创建新的对象。
Integer i1 = 127;
Integer i2 = 127;
System.out.println(i1 == i2); // true。 s1和s2都指向常量池中的同一个对象
i2 = 200;
System.out.println(i1 == i2); // false。s2重新创建了一个新的对象,s1和s2指向不同对象
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // false。128超出范围,s1和s2都指向不同对象
Integer i5 = -128;
Integer i6 = -128;
System.out.println(i5 == i6); // true。 s1和s2都指向常量池中的同一个对象
Boolean bool1 = false;
Boolean bool2 = false;
System.out.println(bool1 == bool2); // true。 s1和s2都指向常量池中的同一个对象
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1 == d2); // false。Double类没有使用常量池。s1和s2指向不同对象