文章目录
- 一、前言
- 二、常量池的引入:从常量到常量池
- 三、常量池的介绍:JVM三种常量池
- 三、常量池的应用:String类中的常量池技术
- 四、常量池的应用:八种基本类型的常量池技术
- 五、尾声
一、前言
二、常量池的引入:从常量到常量池
2.1 常量
常量即用final修饰的成员变量,其值一旦给定就无法改变!final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。
2.2 Class文件中的常量池
在Class文件结构中,最头的4个字节用于存储魔数Magic Number,用于确定一个文件是否能被JVM接受;
再接着4个字节用于存储版本号,前2个字节存储次版本号,后2个存储主版本号;
再接着是用于存放常量的常量池,由于常量的数量是不固定的,所以常量池的入口放置一个U2类型的数据(constant_pool_count)存储常量池容量计数值。
Class常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等;符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:类和接口的全限定名、字段名称和描述符、方法名称和描述符。
2.3 方法区中的运行时常量池
运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
2.4 常量池的好处
常量池的好处:节约空间和时间
常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:自从有了常量池,常量池中所有相同的字符串常量被合并,只占用一个空间;
(2)节省运行时间:自从有了常量池,比较字符串时,== 比equals()快。对于两个引用变量,只用 双等号 判断引用是否相等,也就可以判断实际值是否相等,节约运行时间。
双等号的含义:基本数据类型之间应用双等号,比较的是他们的数值;复合数据类型(类)之间应用双等号,比较的是他们在内存中的存放地址。
三、常量池的介绍:JVM三种常量池
3.1 全局字符串常量池
全局字符串池里的内容是在类加载完成(类加载七个阶段:加载、验证、准备、解析、初始化、使用、卸载),经过验证阶段和准备阶段之后,在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中,这就是我们说的String字符串常量池,它是全局的,所以又称为全局字符串常量池。
Tip: string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。
在HotSpot虚拟机里,实现的string pool功能的本质上是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(即我们常说的用双引号括起来的的引用,注意是驻留字符串的引用不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份,这个StringTable在每个HotSpot虚拟机的实例只有一份,被所有的类共享。
问题1:JVM和HotSpot虚拟机的关系?
回答1:JVM是规范,而HotSpot是实现方式,并且在openJdk的项目上开发的。
问题2:JVM是什么?
回答2:即一个模拟真机操作的软件机器。像真机一样,它有一个指令集,一个虚拟计算机架构和一个执行模型。它能够运行用这个虚拟指令集编写的代码,就像真机可以运行机器代码一样。
问题3:HotSpot是什么?
回答3:HotSpot 是JVM概念的一个实现。它最初由Sun开发,现在由Oracle拥有。 JVM规范还有其他实现,例如 JRockit , IBM J9 等等。
问题4:OpenJDK是什么?
回答4:OpenJDK 是一个项目在此基础上开发了HotSpot的开源实现(以及JDK的许多其他部分,例如编译器,API,工具等)。实际:我们在命令行输入java -version的时候,会输出其实现方式。如下:
3.2 class文件常量池
3.2.1 方法区(class文件常量池和运行时常量池的基础)
方法区里存放着类的版本,字段,方法,接口和常量池,常量池里存储着字面量和符号引用。符号引用包括:1.类的全限定名,2.字段名和属性,3.方法名和属性。
一图解析方法区,class文件信息,class文件常量池和运行时常量池的关系,如下图:
我们知道,方法区在运行时数据区中,这张图进一步告诉我们,方法区中包含class文件常量池和运行时常量池,而这个class文件常量池又在class文件信息中。
Tip:蓝色框框的class文件是指任意一个.java文件编译生成的.class文件。
3.2.2 class文件信息(class文件常量池的基础)
下面一张图用来表示方法区class文件信息包括哪些内容:
可以看到在方法区里的class文件信息包括:魔数,版本号,常量池,类,父类和接口数组等信息,其中类里面又包括字段和方法的信息。
3.2.3 class文件常量池
Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。
下面用一张图来表示class文件常量池里存储的内容:
从这个图中可以看出,class文件常量池存储的一些字面量和符号引用。
字面量即常说的常量概念,如文本字符串、被声明为final的常量值等。
符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用区分一下,直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。字符引用包括下面三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
常量池的每一项常量都是一个表,一共有如下表所示的11种各不相同的表结构数据,这每个表开始的第一位都是一个字节的标志位(取值1-12),代表当前这个常量属于哪种常量类型。
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
UTF8编码的字符串
字面量:整型字面量、长整型字面量、浮点型字面量、双精度浮点型字面量、字符串类型字面量;
符号引用:类或接口的符号引用、字段的符号引用、类中方法的符号引用、接口中方法的符号引用、字段或方法的部分符号引用。
3.3 运行时常量池
3.3.1 概要:理论:从class文件常量池到运行时常量池
当java文件被编译成class文件之后,也就是会生成上面所说的class常量池,那么运行时常量池又是什么时候产生的呢?
jvm在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。
而当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,所以,运行时常量池也是每个类都有一个。
在上面也说了,class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,也就是把符号引用替换为直接引用,解析的过程会去查询全局字符串池(即上面所说的StringTable),以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。
金手指:字符串常量池是全局的,所有类共用一个字符串常量池,所以称为全局字符串常量池。class文件常量池 和 运行时常量池 都是每个类各自独立的。
3.3.2 理论:从class文件常量池到运行时常量池
编译过程:java文件被编译成class文件,这个新生成的class文件,除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),一个java文件生成一个class文件,但是一个java文件中可以有多个类,为每个类生成一个class文件常量池。
(1)class常量池不是全局的,每一个类有一个对应的class常量池;
(2)既然每个类有一个class常量池,那么这个class常量池中存放着什么东西?这里面存放着编译器编译java文件生成的各种字面量(Literal)和符号引用(Symbolic References)。
运行过程:类加载分为五个阶段:加载、验证、准备、解析、初始化,当类加载到内存中后,jvm就会将class常量池中的内容存放到运行时常量池中,由此可知,运行时常量池也是每个类都有一个。
(1)运行时常量池也是每个类都有一个;
(2)运行时常量池存放的就是直接引用,就是对象的实例。
问题:从 Class文件常量池 到 运行时常量池,全局字符串常量池如何作用的?
回答:Class常量池中存的是字面量和符号引用,也就是说他们存的并不是对象的实例,而是对象的符号引用值。而经过解析(resolve)之后,就把符号引用替换为直接引用,解析的过程会去查询全局字符串池(即上面所说的StringTable),以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的(Tip:全局字符串常量池的东西是放在验证、准备阶段之后的)。
3.3.3 实践:从class文件常量池到运行时常量池
举个实例来说明一下:
public class HelloWorld {
public static void main(String []args) {
String str1 = "abc";
String str2 = new String("def");
String str3 = "abc";
String str4 = str2.intern();
String str5 = "def";
System.out.println(str1 == str3);//true
System.out.println(str2 == str4);//false
System.out.println(str4 == str5);//true
}
}
回到上面的那个程序,现在就很容易解释整个程序的内存分配过程了。
第一句,在堆中会有一个”abc”实例,全局StringTable中存放着”abc”的一个引用值;
第二句,生成两个实例,一个是”def”的实例对象,并且StringTable中存储一个”def”的引用值,还有一个是new出来的一个”def”的实例对象,与之前那个是不同的实例;
第三句,当在解析str3的时候查找StringTable,里面有”abc”的全局驻留字符串引用,所以str3的引用地址与之前的那个已存在的相同;
第四句,str4是在运行的时候调用intern()函数,返回StringTable中”def”的引用值,如果没有就将str2的引用值添加进去,但是在这里,StringTable中已经有了”def”的引用值了,所以返回上面在new str2的时候添加到StringTable中的 “def”引用值;
第五句,str5在解析的时候就也是指向存在于StringTable中的”def”的引用值。
那么这样一分析之后,下面三个打印的值就容易理解了。
3.3.4 小结:从class文件常量池到运行时常量池
金手指:程序的整个过程
第一步,经过编译后生成class文件,在该类的class常量池中存放一些符号引用;
第二步,类加载之后,将class常量池中存放的符号引用转存到运行时常量池中,然后经过验证,准备阶段之后,在堆中生成驻留字符串的实例对象(也就是上例中str1所指向的”abc”实例对象);
第三步,将这个对象的引用存到全局String Pool中,也就是StringTable中;
第四步,在解析阶段,要把运行时常量池中的一部分符号引用替换成直接引用,那么就直接查询StringTable,保证StringTable里的引用值与运行时常量池中的引用值一致,大概整个过程就是这样了。
3.4 小结:三种常量池的统一与辨析
3.4.1 三种常量池的统一
1.对于全局字符串常量池:在每个虚拟机中只有一份,存放的是字符串常量的引用值。
2.对于class常量池:在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
3.对于运行时常量池:在类加载完成之后,将每个class常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致。
3.4.2 辨析:class文件常量池和class文件信息
在刚才上面的图中我们看到,class文件常量池实际上被包含在class文件中,真的是这样的吗?我们来看看编译出的.class文件。
先上一段java代码,如下:
public class TestInt {
private String str = "hello";
void printInt(){
System.out.println(65535);
}
}
经过编译后得到class文件,如下:
public class com.mypackage_base.TestInt
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#18 // java/lang/Object."<init>":()V
#2 = String #19 // hello
#3 = Fieldref #7.#20 // com/mypackage_base/TestInt.str:Ljava/lang/String;
#4 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Integer 65535
#6 = Methodref #23.#24 // java/io/PrintStream.println:(I)V
#7 = Class #25 // com/mypackage_base/TestInt
#8 = Class #26 // java/lang/Object
#9 = Utf8 str
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 printInt
#16 = Utf8 SourceFile
#17 = Utf8 TestInt.java
#18 = NameAndType #11:#12 // "<init>":()V
#19 = Utf8 hello
#20 = NameAndType #9:#10 // str:Ljava/lang/String;
#21 = Class #27 // java/lang/System
#22 = NameAndType #28:#29 // out:Ljava/io/PrintStream;
#23 = Class #30 // java/io/PrintStream
#24 = NameAndType #31:#32 // println:(I)V
#25 = Utf8 com/mypackage_base/TestInt
#26 = Utf8 java/lang/Object
#27 = Utf8 java/lang/System
#28 = Utf8 out
#29 = Utf8 Ljava/io/PrintStream;
#30 = Utf8 java/io/PrintStream
#31 = Utf8 println
#32 = Utf8 (I)V
{
public com.mypackage_base.TestInt();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String hello
7: putfield #3 // Field str:Ljava/lang/String;
10: return
LineNumberTable:
line 7: 0
line 8: 4
void printInt();
descriptor: ()V
flags:
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // int 65535
5: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
8: return
LineNumberTable:
line 10: 0
line 11: 8
}
SourceFile: "TestInt.java"
可以看出被编译的class文件中的内容和上面所说的是能对应上的,这就解答了class文件常量池就是在class文件里面。
3.4.3 辨析:class文件常量池和运行时常量池的关系
在刚才的这个图中,我们搞懂了class文件常量池在class文件信息中,那么class文件常量池和运行时常量池又是什么关系呢?
第一,class文件常量池:class文件常量池存储的是当class文件被java虚拟机加载进来后存放在方法区的一些字面量和符号引用,字面量包括字符串,基本类型的常量。
第二,运行时常量池:运行时常量池是当class文件被加载完成后,java虚拟机会将class文件常量池里的内容转移到运行时常量池里。在class文件常量池的符号引用有一部分是会被转变为直接引用的,比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用;而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。
总结:
第一,class文件常量池:方法区里存储着class文件的信息和运行时常量池,class文件的信息包括类信息和class文件常量池。
第二,运行时常量池:运行时常量池里的内容除了是class文件常量池里的内容外,还将class文件常量池里的符号引用转变为直接引用,而且运行时常量池里的内容是能动态添加的。例如调用String的intern方法就能将string的值添加到String常量池中,这里String常量池是包含在运行时常量池里的,但在jdk1.8后,将String常量池放到了堆中。
Tip1:我们下面说的String的常量池和8种基本类型的常量都是运行时常量池;
Tip2:String常量池和全局字符串常量池是完全不同的两个东西。
三、常量池的应用:String类中的常量池技术
3.1 String常量池的引入
3.1.1 String对象的两种创建方式
String str1 = "abcd";
String str2 = new String("abcd");
System.out.println(str1==str2);//false
两者创建方式区别:只要使用 new 方法,便需要创建新的对象,然后,它们的堆地址是唯一的,与任何的引用 == 比较,都是为false。
3.1.2 String常量池intern()方法
String 类型的常量池比较特殊。它的主要使用方法有两种:
(1)如果是直接使用双引号声明出来的 String 对象,会直接存储在常量池中;
(2)如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern() 方法,如果常量池中没有这个字符串,直接放到里面去,如果常量池中有这个字符串,直接返回。
Tip:String.intern() 是一个 Native 方法,它的作用(在JDK1.6和1.7操作不同)是:
如果运行时常量池中已经包含一个等于此 String 对象内容的字符串,则直接返回常量池中该字符串的引用;
如果运行时常量池中不包含一个等于此 String 对象内容的字符串,在jdk1.6中,将此String对象添加到常量池中,然后返回这个String对象的引用(此时引用的串在常量池)。在jdk1.7中,放入一个引用,指向堆中的String对象的地址,返回这个引用地址(此时引用的串在堆)。’
根据《java虚拟机规范 Java SE 8版》记录,如果某String实例所包含的Unicode码点序列与CONSTANT——String_info结构所给出的序列相同,而之前又曾在该实例上面调用过String.intern方法,那么此次字符串常量获取的结果将是一个指向相同String实例的引用。这是什么意思呢?
String a1 = new String("a");
String a2 = new String("b");
String a3 = new String("c");
System.out.println((a1+a2+a3).intern() == "abc"); // 结果是true
在堆中连接生成了"abc",调用intern()后发现常量池没有"abc",那么就放入堆中的"abc"的引用,之后返回值为这个引用,左边是这个引用,右边直接看常量池的"abc",结果发现是堆中的"abc"的引用,直接返回这个引用,两边相同,都是堆里面连接生成的"abc"的地址,所以打印出true。
Unicode码点序列的直接理解:这玩意不就是字符连起来看equals是否相同不就完了吗!咋这么墨迹~
关于运行时常量池:java虚拟机为每个类型都维护着一个常量池。该常量池是java虚拟机中的运行时数据结构,像传统编程语言实现中的符号表一样有很多用途。当类或接口创建时,它的二进制表示中的常量池表被用来构造运行时常量池,运行时常量池中的所有引用最初都是符号引用,解析后一部分无法被重写变为直接引用,另外后面可以intern()添加引用。
Tip:String常量池是包含在运行时常量池里的,但在jdk1.8后,将String常量池放到了堆中。
3.2 String常量池的应用
代码一:str1.intern() == str,返回false
String str1 = new String("cc"); // 在常量池中创建cc字符串,和jdk6 jdk7无关,常量池中的不是使用str1.intern()创建的
String str2 = str1.intern(); // 常量池中有了,返回接收 和jdk6 jdk7无关
System.out.println(str2 == str1); // 返回false
小结:str2!=str1 str1是堆中,一定不相等,除非str2=str1赋值
这里为什么不返回true,而是返回false呢?
解释:
第一句,当new String(“cc”)后,堆中创建了"cc",也会在常量池创建"cc",创建了2个字符串对象;
第二句,当你String intern = h.intern();其中h.intern()会去常量池检查是否有了"cc",结果发现有了,那么此时返回常量池的引用地址给intern;
第三句,用常量池的引用intern和堆中的h引用去比较肯定不相等。所以返回false。
问题:String s1 = new String(“xyz”); 创建了几个对象?
回答:考虑类加载阶段和实际执行时。
第一,类加载对一个类只会进行一次。"xyz"在类加载时就已经创建并驻留了(如果该类被加载之前已经有"xyz"字符串被驻留过则不需要重复创建用于驻留的"xyz"实例)。驻留的字符串是放在全局共享的字符串常量池中的;
第二,在这段代码后续被运行的时候,"xyz"字面量对应的String实例已经固定了,不会再被重复创建。所以这段代码将常量池中的对象复制一份放到heap中,并且把heap中的这个对象的引用交给s1 持有;
综上,这条语句创建了2个对象。
代码二:一个堆中的"str01"地址和一个常量池中的"str01"地址比较,返回false
String str2 = new String("str") + new String("01");
String str1 = "str01"; // 和jdk6 jdk7无关,常量池中的str01不是使用intern()方法创建的
str2.intern(); // 常量池中已经存在且不接收,屁用没有
System.out.println(str2 == str1); // false
小结:str2!=str1 str2是堆中,一定不相等
解释:
第一句new String(“str”) + new String(“01”);现在在堆中创建了"str",同时常量池也创建"str",创建了"01",同时常量池也创建"01",再进行连接,堆中出现了"str01"。此时常量池中有:“str”,“01”,此时堆中有"str",“01”,“str01”。str2引用指向堆中的"str01"。
第二句String str1 = “str01”;直接在常量池创建了"str01"。此时常量池中有:“str”,“01”,“str01”,此时堆中有"str",“01”,“str01”。str1指向常量池中的"str01"。
第三句str2.intern();检查常量池是否有"str01",结果发现有了,返回常量池"str01"的地址,很可惜,没有变量去接收,所以这一句没什么用,str2指向也不会改变,还是指向堆中"str01"。
第四句去打印str2==str1,一个堆中的"str01"地址和一个常量池中的"str01"地址比较,返回false。
代码三:str3和str1均指向常量池的"str01",返回true
String str2 = new String("str") + new String("01");
String str1 = "str01"; // 和jdk6 jdk7无关,常量池中的str01不是使用intern()方法创建的
String str3 = str2.intern(); // 常量池中已经存在接收下来,表示常量池中的内容
System.out.println(str3 == str1); // true
小结:str2!=str1==str3
解释:str2是堆中的,一定不相等
str1和str3是常量池中的,一定相等,无论常量池中是什么,是jdk6还是jdk7
解释:
比问题二多了一个str3引用保存了常量池"str01",str3和str1均指向常量池的"str01",所以返回true
代码四:str1==str4!=str2!=str3 常量池只有一个,一定相等,堆中涉及比较,一定不相等,第一次intern()没有接收下来
String str2 = new String("str") + new String("01");
str2.intern(); // 使用intern()在常量池创建str01字符串,jdk6为常量str01,jdk7为堆地址
String str1 = "str01"; // 常量池中有了,返回常量的内容
System.out.println(str2 == str1); // 堆内容比较,一定为false
String str3 = new String("str01"); // 堆内容中创建str01
str3.intern(); // 常量池中有了且不接收 屁用没有
String str4 = "str01"; // 返回常量池内容
System.out.println(str3 == str4); // 堆内容比较,一定为false
小结:str1==str4!=str2!=str3
解释:常量池只有一个,一定相等,堆中涉及比较,一定不相等,第一次intern()没有接收下来
解释:
第一句,new String(“str”) + new String(“01”);现在在堆中创建了"str",同时常量池也创建"str",创建了"01",同时常量池也创建"01",再进行连接,堆中出现了"str01"。此时常量池中有:“str”,“01”,此时堆中有"str",“01”,“str01”。str2引用指向堆中的"str01"。
第二句,str2.intern();检查到常量池不存在"str01",如果在jdk1.6,那么就将堆中的"str01"添加到常量池中,如果是jdk1.7,那么就在常量池保存指向堆中"str01"的地址,即保存堆中"str01"的引用。接下来的讲解以jdk1.7为准!!
第三句,String str1 = “str01”;检查到常量池有一个地址保存了这个串,str1就直接指向这个地址,即还是堆中的"str01"。
第四句,接着打印str2==str1是否相等,str2指向堆中的"str01",str1指向常量池的某个地址,这个地址恰好是保存堆中的"str01",所以仍然是true。
第五句,String str3 = new String(“str01”);又在堆中创建了"str01",现在堆中有了2个"str01",而常量池已经有"str01"引用则不在创建。(结论是常量池有相同的串或者引用指向相同的串就不在创建,用equals比较是否相同)
第六句,str3.intern(); 去检查一下常量池到底有没有"str01"呢?检查发现常量池有个引用指向堆中的"str01",检查是用equals比较的,JVM认为常量池是有"str01"的,那么返回指向堆中的"str01"地址,很可惜,没有变量去接收,这一句在这里没有什么用。
第七句,String str4 = “str01”;检查到常量池有个引用指向堆中的"str01",检查是用equals相比,结果为true,那么str4保存这个地址,所以这个"str01"还是堆中的第一个"str01"。
第八句,打印str3 == str4,str3是堆中新建的第二个"str01",str4保存的仍是第一个堆中的"str01",两块堆的地址,所以返回false。
问题:String str2 = new String(“str”) + new String(“01”); 为什么第一行要这么写呢? 为什么不String str2 = new String(“str01”);呢? 区别在哪里呢?
回答:
第一句,new String(“str”)堆中创建"str",同时常量池也创建"str",new String(“01”)在堆中创建"01",同时常量池也创建"01",相加操作只会在堆中创建"str01",所以前者执行以后,内存:堆中有"str",“01”,“str01”,常量池中"str",“01”。str2引用指向堆中的"str01"。
第二句,String str2 = new String(“str01”);这个就是在堆中创建"str01"同时常量池中也创建"str01",str2引用指向堆中的"str01",内存:堆中有"str01",常量池中有"str01"。
综上所述,区别就在于这些串处于不同的位置,前者在常量池是没有"str01"的。
代码五:堆、常量池、intern()
String s = new String("abc");
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s == s1);// 堆内存s和常量池内存s1相比,false
System.out.println(s == s2);// 堆内存s和堆内存s2相比,false
System.out.println(s == s1.intern());// 堆内存地址s和常量池地址s1相比,false
System.out.println(s == s2.intern());// 堆内存地址s和常量池地址s1相比,false
System.out.println(s1 == s2.intern());// 常量池地址s1和常量池地址s1相比,true
System.out.println(s.intern() == s2.intern());// 常量池地址s1和常量池地址s1相比,true
小结:s!=s2!=s1==s1.intern()==s.intern()==s2.intern()
s和s2是堆中,一定不相等,s1是常量池中返回abc字符串,后面常量池比较一定相等
解释:有注释,无需多余解释,上面的问题看懂了这个一看就懂。
金手指:java.lang.String.intern()
值得注意得是,运行时常量池中的常量并不是编译期确定,而是后续可以增加,这是运行时常量池相对于Class文件常量池的一个重要特征,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是String类的intern()方法。
public static void main(String[] args) {
String s1 = new String("计算机"); // 使用new 与任何比较,都是false
String s2 = s1.intern();
String s3 = "计算机";
System.out.println("s1 == s2? " + (s1 == s2));
System.out.println("s3 == s2? " + (s3 == s2));
}
s1 == s2? false
s3 == s2? true
小结:s1!=s2==s3
代码六:字符串拼接
只有左边有变量,右边都是常量的拼接,所以为true
public class Test {
public static void main(String[] args) {
String hello = "Hello", lo = "lo";
System.out.println((hello == "Hello") + " "); // true 第一,只有一边变量,而且不涉及拼接 没有new就没有堆的事
System.out.println((Other.hello == hello) + " "); // true 第二,两边都是变量,但是不涉及拼接,所以为true 没有new就没有堆的事
System.out.println((other.Other.hello == hello) + " "); // true 第二,两边都是变量,但是不涉及拼接,所以为true 没有new就没有堆的事
System.out.println((hello == ("Hel"+"lo")) + " "); // true 第三,只有一边变量 没有new就没有堆的事
System.out.println((hello == ("Hel"+lo)) + " "); // false 第四,两边都有变量,右边有涉及变量的拼接,false
System.out.println(hello == ("Hel"+lo).intern()); // true 第五,两边都有变量,右边有涉及变量的拼接,但是右边返回的常量池中的,所以为true
}
}
class Other { static String hello = "Hello"; }
package other;
public class Other { public static String hello = "Hello"; }
true true true true false true
解释:
在同包同类下,引用自同一String对象.
在同包不同类下,引用自同一String对象.
在不同包不同类下,依然引用自同一String对象.
在编译成.class时能够识别为同一字符串的,自动优化成常量,引用自同一String对象.
在运行时创建的字符串具有独立的内存地址,所以不引用自同一String对象.
两边都有变量,但是没有变量拼接,所以为true
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false 两边都有变量,右边有涉及变量的拼接,所以为false
String str5 = "string";
System.out.println(str3 == str5);//true 解释: "str" + "ing" == "string" 两边都有变量,但是没有变量拼接,所以为true
两边都有变量,右边有涉及变量的拼接,所以为false
String s1 = "abc";
String s2 = "a";
String s3 = "bc";
String s4 = s2 + s3;
System.out.println(s1 == s4);//false,
// 解释:因为s2+s3实际上是使用StringBuilder.append来完成,会生成不同的对象。
// 两边都有变量,右边有涉及变量的拼接,所以为false
// s1指向常量池"abc",s4指向堆中"abc"(append连接而来)
虽然两边都有变量,右边涉及变量的拼接,但是为final,所以为true
final包括两种,定义时初始化的final变量、构造函数中初始化的final变量。
普通final,即定义时初始化,返回为true
String S1 = "abc";
final String S2 = "a";
final String S3 = "bc";
String S4 = S2 + S3; // 两边都有变量,右边涉及变量的拼接,但是为final,所以为true
System.out.println(S1 == S4);//true,因为final变量在编译后会直接替换成对应的值
// 所以实际上等于s4="a"+"bc",而这种情况下,编译器会直接合并为s4="abc",所以最终s1==s4。
普通final,即定义时初始化,返回为s==t
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B; // 将两个常量用+连接对s进行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
s等于t,它们是同一个对象
A和B都是常量,值是固定的,因此s的值也是固定的,它在类被编译时就已经确定了。也就是说:String s=A+B; 等同于:String s=“ab”+“cd”;
空白final,即构造函数中初始化final变量,为false.
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 将两个常量用+连接对s进行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它们是同一个对象");
} else {
System.out.println("s不等于t,它们不是同一个对象");
}
}
运行结果:
s不等于t,它们不是同一个对象
A和B虽然被定义为常量,但是它们都没有马上被赋值。在运算出s的值之前,他们何时被赋值,以及被赋予什么样的值,都是个变数。因此A和B在被赋值之前,性质类似于一个变量。那么s就不能在编译期被确定,而只能在运行时被创建了。
小结:字符串拼接
String str1 = "abcd"; // 常量池创建"abcd"
String str2 = "abcd"; // str2还是上一步的"abcd"
String str3 = "ab" + "cd"; // 常量池创建"ab"和"cd",连接过程编译器直接优化成"abcd",而常量池已经有了"abcd",所以str3和str1都指向"abcd"
String str4 = "ab"; // 常量池已经有了"ab"
str4 += "cd"; // str4+"cd"连接的字符串编译器不能优化,所以是堆中的"abcd"
// 因为"ab"是str4引用的,如果是两个变量s1="ab", s2="cd",s1+s2连接,那么只有用final修饰的指向"ab"的s1和final修饰的指向"cd"的s2相连接才能优化成"abcd"
// 如果只有一个变量s1和常量池的常量连接s1+"cd",这个变量s1也需要final修饰才会优化成"abcd"
System.out.println(str1 == str2); // true 两个变量,但是没有拼接 为true
System.out.println(str1 == str3); // true 两个变量,右边一个只有常量拼接 为true
System.out.println(str1 == str4); // false 两个变量,右边涉及变量拼接 为false
System.out.println("================");
String s1 = "a";
String s2 = "b";
String s3 = "ab";
final String ss1 = "a";
final String ss2 = "b";
System.out.println(s1 + s2 == s3); // false, 有变量引用的字符串是不能优化的,除非变量是final修饰,这一行就是s1+s2生成堆里的"ab"和常量池的"ab"在比较
System.out.println(ss1 + ss2 == s3); // true,原因见上一行,原理和下一行相同,都是常量连接
System.out.println("a" + "b" == s3); // true,常量池的"a"和"b"连接,根据Copy On Write机制, 副本连接生成"ab",发现已存在,直接指向"ab",所以和s3相等
验证一下确实生成了副本才进行连接:
String s = "ab"; // 常量池创建"ab"
String s1 = new String("ab"); // 堆里面创建"ab",因为常量池已有"ab",不在常量池再创建"ab"
String str3 = "ab" + "cd"; // 连接之后常量池是否还有"ab"??在常量池连接成"abcd"后"ab"和"cd"是否还存在?
String s2 = s1.intern(); // 如果常量池还有"ab",s2指向常量池"ab",如果没有,则放入s1地址,s2就指向s1,即s2指向堆里的"ab"
System.out.println(s2 == s1); // 如果true,则s2是堆里的"ab".说明"ab"+"cd"连接后,常量池只有"abcd","ab"和"cd"被回收了
// 结果运行出来是false,说明"ab"+"cd"连接之后,不仅存在"ab","cd"还存在"abcd"
关于Java的String类这种在修改享元对象时,先将原有对象复制一份,然后在新对象上再实施修改操作的机制称为“Copy On Write”,大家可以自行查询相关资料来进一步了解和学习“Copy On Write”机制,在此不作详细说明。
比如(“a”+“b”+“c”).intern() == “abc”,结果返回true,而"a"+“b”+“c” == “abc"也是返回true。都是副本连接生成"abc”。
3.3 String常量池的应用
第一,String对象的两种创建方式
如果是直接使用双引号声明出来的 String 对象,会直接存储在常量池中,一定不在堆中新建一份。
如果不是用双引号声明的 String 对象,可以使用 String 提供的 intern 方法。不管任何情况下,只要使用new方法,一定会在堆种创建新的对象,如 new String()
它们的堆地址是唯一的,与任何的引用 == 比较,一定都是为false。
第二,常量池中一份,堆中多份
相同内容的字符串,在常量池中永远只会保留一份;但是,相同内容的字符串,在堆中可以保留多份。这就是String的常量池技术。
第三,str2.intern(); 和 str1=str2.intern(); 的区别
代码 str.intern();
没有变量接收这个返回值
如果常量池中没有这个字符串,就将将string的值(jdk7是堆地址)添加到String常量池中;
如果常量池中有这个字符串,啥都不干。
代码 str1=str.intern();
有变量接收这个返回值
如果常量池中没有这个字符串,就将将string的值(jdk7是堆地址)添加到String常量池中,将常量池内容赋值给str1;
如果常量池中有这个字符串,啥都不干,但是,还是将常量池内容(jdk7是堆地址)赋值给str1。
第四,string不可变,需要改变就是拼接
String是final修饰是不可变对象,字符串拼接操作,常量拼接或final修改的变量拼接不会生成新的字符串,只要涉及一个非final变量,底层一定会使用StringBuilder生成新的String变量。
四、常量池的应用:八种基本类型的常量池技术
基本类型的常量池和String字符串一样,任何相同数值的整型数值,在常量池中只有一份,但是,在堆中可以有多份,这就是常量池技术。
Java 基本类型的包装类的大部分都实现了常量池技术,即 Byte、Short、Integer、Long、Character、Boolean;这5种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。
两种浮点数类型的包装类 Float、Double 并没有实现常量池技术。
Tip:只有包装类声明才有常量池,且如果使用new Integer(40)还是会到堆中新建对象。
Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 输出true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 输出false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出false
在[-128,127]区间内的利用cache数组的值,否则new一个新的Integer对象。这里2个333不等因为是2块不同的堆内存。2个33相等是因为利用了同一个cache数组,是值的比较,这里i1==33,打印出来也是true。
Integer 缓存源代码:
public static Integer valueOf(int i) { // 自动装包代码 int变为Interger
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
应用场景:
Integer i1=40;Java 在编译的时候会直接将代码封装成 Integer i1=Integer.valueOf(40); 从而使用常量池中的对象。
Integer i2 = new Integer(40) ;这种情况下一定会创建新的对象。
System.out.println(i1==i2); //输出false
Integer 比较(==)更丰富的一个例子:
Integer i1 = 40; // 不创建
Integer i2 = 40; // 不创建
Integer i3 = 0; // 不创建
Integer i4 = new Integer(40); // 一定创建
Integer i5 = new Integer(40); // 一定创建
Integer i6 = new Integer(0); // 一定创建
System.out.println("i1=i2 " + (i1 == i2)); // true
System.out.println("i1=i2+i3 " + (i1 == i2 + i3)); // true
System.out.println("i1=i4 " + (i1 == i4)); // 涉及i4 一定为false
System.out.println("i4=i5 " + (i4 == i5)); // 涉及i4 i5 一定为false
System.out.println("i4=i5+i6 " + (i4 == i5 + i6)); // 拆包 i4==40
System.out.println("40=i5+i6 " + (40 == i5 + i6)); // 拆包 40==40
结果:
i1=i2 true
i1=i2+i3 true
i1=i4 false
i4=i5 false
i4=i5+i6 true
40=i5+i6 true
解释:
语句 i4 == i5 + i6,因为 + 这个操作符不适用于 Integer 对象,首先 i5 和 i6 进行自动拆箱操作,进行数值相加,即 i4 == 40。然后Integer对象无法与数值进行直接比较,所以i4自动拆箱转为int值40,最终这条语句转为40 == 40进行数值比较。
五、尾声
由浅入深,虚拟机常量池底层解析,完成了。
天天打码,天天进步!!!