符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用
- 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
- 一个能间接定位到目标的句柄
- 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。
常量定义
“常量”在程序运行时,不会被修改的量。换言之,常量虽然是为了硬件、软件、编程语言服务,但是它并不是因为硬件、软件、编程语言而引入。
字面常量(直接常量):整型常量、字符常量、实型常量等。
在java中一般是指由java修饰的成员变量:静态变量、实例变量和局部变量。
javap
-c -verbose -classpath ${workspace_loc}/${project_name}/bin ${java_type_name}
在eclipse首先配置javap方便后续使用,参考下图:
package com.sunld;
/**
* @Title: Test.java
* @Package com.sunld
* <p>Description:</p>
* @author sunld
* @version V1.0.0
* <p>CreateDate:2017年5月12日 上午11:45:14</p>
*/
public class Test {
public final int a = 1;
public static int b;
public final static int c=2;
}
使用javap查看编译后的class文件
编译后的class文件信息如下:
Classfile /D:/Workspaces/eclipse_neon/TestJvm/bin/com/sunld/Test.class
Last modified 2017-6-7; size 362 bytes
MD5 checksum 3b07cb0016432f841b26eb9015c06c20
Compiled from "Test.java"
public class com.sunld.Test
minor version: 0
major version: 50
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // com/sunld/Test
#2 = Utf8 com/sunld/Test
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 ConstantValue
#8 = Integer 1
#9 = Utf8 b
#10 = Utf8 c
#11 = Integer 2
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Methodref #3.#16 // java/lang/Object."<init>":()V
#16 = NameAndType #12:#13 // "<init>":()V
#17 = Fieldref #1.#18 // com/sunld/Test.a:I
#18 = NameAndType #5:#6 // a:I
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 this
#22 = Utf8 Lcom/sunld/Test;
#23 = Utf8 SourceFile
#24 = Utf8 Test.java
{
public final int a;
descriptor: I
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: int 1
public static int b;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static final int c;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 2
public com.sunld.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #15 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #17 // Field a:I
9: return
LineNumberTable:
line 11: 0
line 12: 4
line 11: 9
LocalVariableTable:
Start Length Slot Name Signature
0 。。。
10 0 this Lcom/sunld/Test;
}
SourceFile: "Test.java"
可以看到对之前声明的属性编译后的显示,以及class文件常量池中存储的内容。
Class文件常量池
源码文件在编译之后生成class文件,虚拟机对于class文件的结构有严格的要求,class文件中的数据项参考下图:
常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References)
关于Class文件常量池会在后续的文章中详细介绍。
Class文件常量表,来源于网络。
运行时常量池
当java文件被编译成class文件之后,就会生成Class文件常量池;jvm在执行某个类的时候,必须经过加载、连接、初始化 ,而连接又包括验证、准备、解析三个阶段。(在JVM中,类的生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段)而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用。
运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
常量池的优点
可以理解为缓存的概念。常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,==比equals()快。对于两个引用变量,只用==判断引用是否相等,也就可以判断实际值是否相等。
字符串常量池
字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。 )。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
代码1
public class TestString2 {
private String str = "sun" + "ld";
private static String str1 = "sun1" + "ld1";
private static final String str2 = "sun2" + "ld2";
private final String str3 = "sun3" + "ld3";
}
Constant pool:
#1 = Class #2 // com/sunld/TestString2
#2 = Utf8 com/sunld/TestString2
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 str
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 str1
#8 = Utf8 str2
#9 = Utf8 ConstantValue
#10 = String #11 // sun2ld2
#11 = Utf8 sun2ld2
#12 = Utf8 str3
#13 = String #14 // sun3ld3
#14 = Utf8 sun3ld3
#15 = Utf8 <clinit>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = String #19 // sun1ld1
#19 = Utf8 sun1ld1
#20 = Fieldref #1.#21 // com/sunld/TestString2.str1:Ljava/lang/String;
#21 = NameAndType #7:#6 // str1:Ljava/lang/String;
#22 = Utf8 LineNumberTable
#23 = Utf8 LocalVariableTable
#24 = Utf8 <init>
#25 = Methodref #3.#26 // java/lang/Object."<init>":()V
#26 = NameAndType #24:#16 // "<init>":()V
#27 = String #28 // sunld
#28 = Utf8 sunld
#29 = Fieldref #1.#30 // com/sunld/TestString2.str:Ljava/lang/String;
#30 = NameAndType #5:#6 // str:Ljava/lang/String;
#31 = Fieldref #1.#32 // com/sunld/TestString2.str3:Ljava/lang/String;
#32 = NameAndType #12:#6 // str3:Ljava/lang/String;
#33 = Utf8 this
#34 = Utf8 Lcom/sunld/TestString2;
#35 = Utf8 SourceFile
#36 = Utf8 TestString2.java
在代码中分别声明字符串str:实例变量、类变量、类常量、实例常量;通过字节码解析发现字符串字面量直接拼接编译期会自动组合成一个放入到字符串常量池。实例变量、类变量的属性索引是在init、cinit(运行时)方法中指向字面量;类常量和实例常量是直接指向字面量(编译时)。
代码2
public class TestString3 {
private String str1 = "a";
private String str2 = "b";
private String str3 = str1 + str2;
private String str4 = str1 + "c";
private String str5 = "d" + "d1";
// public static void main(String[] args){
// String str1 = "a";
// String str2 = "b";
// String str3 = str1 + str2;
// String str4 = str1 + "c";
// String str5 = "a" + "b";
// }
}
Constant pool:
#1 = Class #2 // com/sunld/TestString3
#2 = Utf8 com/sunld/TestString3
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 str1
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 str2
#8 = Utf8 str3
#9 = Utf8 str4
#10 = Utf8 str5
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Methodref #3.#15 // java/lang/Object."<init>":()V
#15 = NameAndType #11:#12 // "<init>":()V
#16 = String #17 // a
#17 = Utf8 a
#18 = Fieldref #1.#19 // com/sunld/TestString3.str1:Ljava/lang/String;
#19 = NameAndType #5:#6 // str1:Ljava/lang/String;
#20 = String #21 // b
#21 = Utf8 b
#22 = Fieldref #1.#23 // com/sunld/TestString3.str2:Ljava/lang/String;
#23 = NameAndType #7:#6 // str2:Ljava/lang/String;
#24 = Class #25 // java/lang/StringBuilder
#25 = Utf8 java/lang/StringBuilder
#26 = Methodref #27.#29 // java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#27 = Class #28 // java/lang/String
#28 = Utf8 java/lang/String
#29 = NameAndType #30:#31 // valueOf:(Ljava/lang/Object;)Ljava/lang/String;
#30 = Utf8 valueOf
#31 = Utf8 (Ljava/lang/Object;)Ljava/lang/String;
#32 = Methodref #24.#33 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#33 = NameAndType #11:#34 // "<init>":(Ljava/lang/String;)V
#34 = Utf8 (Ljava/lang/String;)V
#35 = Methodref #24.#36 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#36 = NameAndType #37:#38 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#37 = Utf8 append
#38 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#39 = Methodref #24.#40 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#40 = NameAndType #41:#42 // toString:()Ljava/lang/String;
#41 = Utf8 toString
#42 = Utf8 ()Ljava/lang/String;
#43 = Fieldref #1.#44 // com/sunld/TestString3.str3:Ljava/lang/String;
#44 = NameAndType #8:#6 // str3:Ljava/lang/String;
#45 = String #46 // c
#46 = Utf8 c
#47 = Fieldref #1.#48 // com/sunld/TestString3.str4:Ljava/lang/String;
#48 = NameAndType #9:#6 // str4:Ljava/lang/String;
#49 = String #50 // dd1
#50 = Utf8 dd1
#51 = Fieldref #1.#52 // com/sunld/TestString3.str5:Ljava/lang/String;
#52 = NameAndType #10:#6 // str5:Ljava/lang/String;
#53 = Utf8 LineNumberTable
#54 = Utf8 LocalVariableTable
#55 = Utf8 this
#56 = Utf8 Lcom/sunld/TestString3;
#57 = Utf8 SourceFile
#58 = Utf8 TestString3.java
- 对于直接做+运算的两个字符串(字面量)常量,并不会放入String常量池中,而是直接把运算后的结果放入常量池中
- 对于先声明的字符串字面量常量,会放入常量池,但是若使用字面量的引用进行运算就不会把运算后的结果放入常量池中了 ,除非在编译期确认是常量
- 总结一下就是JVM会对String常量的运算进行优化,未声明的,只放结果;已经声明的,只放声明
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false
String str5 = "string";
System.out.println(str3 == str5);//true
//#######################################
public static final String a="a";
public static final String b="b";
String c = a + b;
String d = "ab";
System.out.println(d == c);//true
//#######################################
public static final String a;
public static final String b;
static{
a="a";
b="b";
}
String c = a + b;
String d = "ab";
System.out.println(d == c);//false
对象的创建与对比
public class TestString4 {
public static void main(String[] args) {
String str1 = new String("a");
String str2 = new String("b");
String str3 = "a";
// System.out.println(str1 == str3);//false
}
}
Constant pool:
#1 = Class #2 // com/sunld/TestString4
#2 = Utf8 com/sunld/TestString4
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/sunld/TestString4;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Class #17 // java/lang/String
#17 = Utf8 java/lang/String
#18 = String #19 // a
#19 = Utf8 a
#20 = Methodref #16.#21 // java/lang/String."<init>":(Ljava/lang/String;)V
#21 = NameAndType #5:#22 // "<init>":(Ljava/lang/String;)V
#22 = Utf8 (Ljava/lang/String;)V
#23 = String #24 // b
#24 = Utf8 b
#25 = Utf8 args
#26 = Utf8 [Ljava/lang/String;
#27 = Utf8 str1
#28 = Utf8 Ljava/lang/String;
#29 = Utf8 str2
#30 = Utf8 str3
#31 = Utf8 SourceFile
#32 = Utf8 TestString4.java
执行过程:把java文件编译成class文件之后,在该类中的常量池中存放一些符号引用,然后类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,通过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(字面值a,b在堆中的实例对象),然后将这个对象的引用存放到字符串常量池中(StringTable),最后解析阶段,就是把运行时常量池中的符号引用替换成直接引用。
针对上述代码分析如下:首先在队中有4个实例a,b,str1实例,str2实例,在字符串常量池中存放着a,b的引用值,解析str3的时候str3的引用地址返回的是驻留在常量池a的引用地址。所以返回的false
intern(),运行时调用
String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。
另外一点值得注意的是,虽然String.intern()的返回值永远等于字符串常量。但这并不代表在系统的每时每刻,相同的字符串的intern()返回都会是一样的(虽然在95%以上的情况下,都是相同的)。因为存在这么一种可能:在一次intern()调用之后,该字符串在某一个时刻被回收,之后,再进行一次intern()调用,那么字面量相同的字符串重新被加入常量池,但是引用位置已经不同。
public class TestString5 {
public static void main(String[] args) {
String s1 = new String("a");
String s2 = s1.intern();
String s3 = "a";
System.out.println(s1 == s2);//false
System.out.println(s3 == s2);//true
}
}
Constant pool:
#1 = Class #2 // com/sunld/TestString5
#2 = Utf8 com/sunld/TestString5
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/sunld/TestString5;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Class #17 // java/lang/String
#17 = Utf8 java/lang/String
#18 = String #19 // a
#19 = Utf8 a
#20 = Methodref #16.#21 // java/lang/String."<init>":(Ljava/lang/String;)V
#21 = NameAndType #5:#22 // "<init>":(Ljava/lang/String;)V
#22 = Utf8 (Ljava/lang/String;)V
#23 = Methodref #16.#24 // java/lang/String.intern:()Ljava/lang/String;
#24 = NameAndType #25:#26 // intern:()Ljava/lang/String;
#25 = Utf8 intern
#26 = Utf8 ()Ljava/lang/String;
#27 = Fieldref #28.#30 // java/lang/System.out:Ljava/io/PrintStream;
#28 = Class #29 // java/lang/System
#29 = Utf8 java/lang/System
#30 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Methodref #34.#36 // java/io/PrintStream.println:(Z)V
#34 = Class #35 // java/io/PrintStream
#35 = Utf8 java/io/PrintStream
#36 = NameAndType #37:#38 // println:(Z)V
#37 = Utf8 println
#38 = Utf8 (Z)V
#39 = Utf8 args
#40 = Utf8 [Ljava/lang/String;
#41 = Utf8 s1
#42 = Utf8 Ljava/lang/String;
#43 = Utf8 s2
#44 = Utf8 s3
#45 = Utf8 StackMapTable
#46 = Class #40 // "[Ljava/lang/String;"
#47 = Utf8 SourceFile
#48 = Utf8 TestString5.java
创建了几个对象:参考上述的执行过程
String s1 = new String(“xyz”);
考虑类加载阶段和实际执行时。
(1)类加载对一个类只会进行一次。”xyz”在类加载时就已经创建并驻留了(如果该类被加载之前已经有”xyz”字符串被驻留过则不需要重复创建用于驻留的”xyz”实例)。驻留的字符串是放在全局共享的字符串常量池中的。
(2)在这段代码后续被运行的时候,”xyz”字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s1 持有。
这条语句创建了2个对象。
基本数据类型常量池
基本数据类型的使用量是很大的,java常量池,在节省内存方面是一个很好的机制,相同的数据,在常量池中只要保留一份即可。
Java的8种基本类型(Byte, Short, Integer, Long, Character, Boolean, Float, Double), 除Float和Double以外, 其它六种都实现了常量池, 但是它们只在大于等于-128并且小于等于127时才使用常量池。
package com.sunld;
/**
* @Title: TestInteger.java
* @Package com.sunld
* <p>Description:</p>
* @author sunld
* @version V1.0.0
* <p>CreateDate:2017年6月9日 上午9:28:45</p>
*/
public class TestInteger {
public static void main(String[] args) {
Integer a = 10;
Integer b = 10;
System.out.println(a == b);//true
a = 127;
b = 127;
System.out.println(a == b);//true
a = 128;
b = 128;
System.out.println(a == b);//false
a = -128;
b = -128;
System.out.println(a == b);//true
a = -129;
b = -120;
System.out.println(a == b);//false
}
}
Constant pool:
#1 = Class #2 // com/sunld/TestInteger
#2 = Utf8 com/sunld/TestInteger
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/sunld/TestInteger;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Methodref #17.#19 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#17 = Class #18 // java/lang/Integer
#18 = Utf8 java/lang/Integer
#19 = NameAndType #20:#21 // valueOf:(I)Ljava/lang/Integer;
#20 = Utf8 valueOf
#21 = Utf8 (I)Ljava/lang/Integer;
#22 = Fieldref #23.#25 // java/lang/System.out:Ljava/io/PrintStream;
#23 = Class #24 // java/lang/System
#24 = Utf8 java/lang/System
#25 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Methodref #29.#31 // java/io/PrintStream.println:(Z)V
#29 = Class #30 // java/io/PrintStream
#30 = Utf8 java/io/PrintStream
#31 = NameAndType #32:#33 // println:(Z)V
#32 = Utf8 println
#33 = Utf8 (Z)V
#34 = Utf8 args
#35 = Utf8 [Ljava/lang/String;
#36 = Utf8 a
#37 = Utf8 Ljava/lang/Integer;
#38 = Utf8 b
#39 = Utf8 StackMapTable
#40 = Class #35 // "[Ljava/lang/String;"
#41 = Utf8 SourceFile
#42 = Utf8 TestInteger.java
通过javap查看Integer赋值使用的方法是#16 = Methodref #17.#19 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
查看源代码如下:
public static Integer valueOf(int i) {//[-128,127]数据来源于IntegerCache中,不在该范围内则new对象
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
//缓存是使用Integer类型的静态数组,[-128,127]范围内的数据都是只new了一次然后多次使用,也是放在堆中的。存在一定的风险性,对象可能被回收。
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
总结
- 全局常量池在每个VM中只有一份,存放的是字符串常量的引用值。
- class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
- 运行时常量池是在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。