Java常量池[乐乐独记]
Java常量池中主要存放的信息就是字面量和符号引用,主要分为静态常量池、运行时常量池、字符串常量池和基本类型常量池。
1、字面量和符号引用
1.1、字面量
字面量就是我们的值,类似于数字、字符串之类的,详情如下面代码所示:
// 例如:1就是我们的字面量
int num = 1;
1.2、符号引用
符号引用就是相当于是我们代码中的变量、类的全限定名以及方法名称等等,详情如下面代码所示:
// 例如:num、Compile就是我们的符号引用
int num = 1;
public void Compile(){}
这些常量池现在是静态信息,当运行时被加载到内存之后,这些符号才有对应的内存地址信息,常量池被装在内存之后就变成运行时常量池,这时符号引用就变为了直接引用,也就是我们之前经常说的动态链接,主要通过对象头里的类型指针去转换直接引用。
2、常量池
2.1、静态常量池
在类加载的时候就生成的常量池称之为静态常量池
它存在于我们的.class文件中
我们可通过一条命令去看某一个类的静态常量池
命令:javap -v xxx.class
详情如下图所示:
2.2、运行时常量池
当静态常量池被加载到内存之后就成为了运行时常量池,在hotspot源码中有C++写的一些对象(Constantpool)存储这些常量,以便调用,这些对象就是运行时常量池。
位置:放在了方法区中。
2.3、字符串常量池
2.3.1、字符串常量池的概念
字符串常量池相当于是一个缓存池,当我们程序第一次创建某个字符串字面量对象时,将这个字符串字面量放到字符串常量池中,第二次需要用的时候直接从缓存池里面提取便是。
2.3.2、字符串常量池的位置
Jdk1.6及以前:字符串常量池是运行时常量池的一部分,运行时常量池在永久代中。
Jdk1.7:字符串常量池脱离了永久代,分离到了堆中。
Jdk1.8:永久代改名元空间,字符串常量池在堆中。
2.3.3、字符串常量池的设计思想
由于字符串分配和其他对象分配一样,耗费的代价特别高,如果大量的创建字符串,会影响程序的性能,所以JVM为此做了如下优化:
1、由于字符串常量池的诞生,相当于建立了一个缓存池,已经创建过的可以不用再创建,直接再字符串常量池中取即可。
2、每次创建字符串时,先去常量池里面找,如果有,直接返回,如果没有,先实例化这个字符串常量放入到字符串常量池,然后再返回。
2.3.4、创建字符串的几种操作
(JDK1.7以上版本)
-
直接赋值字符串常量
String s1 = "XiaoLeLe";
JVM底层执行过程:在执行这行代码时,JVM会先去字符串常量池中通过equals(key)的方法去找这个字符串常量是否存在,如果有,直接返回,如果没有,先实例化这个字符串常量放入到字符串常量池,然后再返回。
-
new String();
String s1 = new String("XiaoLeLe");
JVM底层执行过程:首先,我们看到了括号里面的字符串字面量,JVM会执行第一种情况的过程,完成之后,由于new tring()的关系,会在除了字符串常量池的剩下堆空间(也就是上图中的黄色区域)中再创建一个"XiaoLeLe"对象,并将引用返回给s1。
-
使用intern方法
String s1 = new String("XiaoLeLe"); String s2 = s1.intern(); System.out.println(s1 == s2); //false
JVM底层执行过程:第一行的代码在第二种情况已经解释,会创建两个对象,但是s1会指向非字符串常量池中的那个对象,intern方法在执行时也会去字符串常量池中通过equals(key)的方法去找这个字符串常量是否存在,如果存在,直接返回。
如果不存在,将在字符串常量池弄一个内存地址指向堆中,s2指向字符串地址索引,索引再指向非字符串常量池中堆的那个对象,(JDK1.6版本需要将非字符串常量池堆中的复制一份放到字符串常量池中,然后返回索引,所以在JDK1.6版本时会多创建一个对象)详情如下图:
2.3.5、举例
-
栗子1
String s1="XiaoLeLe"; String s2="XiaoLeLe"; // JVM会将下面这行代码优化成String s3="XiaoLeLe"; String s3="Xiao" + "LeLe"; System.out.println( s1==s2 ); //true System.out.println( s1==s3 ); //true
分析:看代码中注释即可知,JVM会对两个字符串常量相加做优化,所以为true。
-
栗子2
String s1 ="XiaoLeLe"; String s2 =new String("XiaoLeLe"); String s3 ="Xiao" + new String("LeLe"); System.out.println( s1 == s2); // false System.out.println( s1 == s3); // false System.out.println( s2 == s3); // false
分析:首先碰到new String()时就会去非字符串常量池里面创建一个属于自己独特的地址,所以三个都不会指向同一个对象。
-
栗子3
String s1 = "XiaoLeLe1"; // JVM会将下面这行代码优化成String s2 = "XiaoLeLe1"; String s2 = "XiaoLeLe" + 1; System.out.println(s1 == s2); // true String s3 = "XiaoLeLe1.6"; // JVM会将下面这行代码优化成String s4 = "XiaoLeLe1.6"; String s4 = "XiaoLeLe" + 1.6; System.out.println(s3 == s4); // true
分析:看代码中注释即可知,JVM会对两个字符串常量相加做优化,所以为true。
-
栗子4
String s1 = "XiaoLeLe"; String s2 = "LeLe"; String s3 = "Xiao" + s2; System.out.println(s1 == s3); // false
分析:由于s2不能像普通的字符串常量一样被JVM优化处理,所以会创建新的对象执行append方法,详情请看辅助知识的+ 号的拓展。
-
栗子5
String s1 = "XiaoLeLe"; final String s2 = "LeLe"; String s3 = "Xiao" + s2; System.out.println(s1 == s3); // true
分析:通过下图红框中的代码可以看出,在我们为s3赋值的时候,是直接将“XiaoLeLe”赋值了过去,所以说,在底层,JVM将final修饰的变量当一个普通的字符串常量去操作的(对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中)。
-
栗子6
public static void main(String[] args) { String s1 = "XiaoLeLe"; final String LeLe = getLeLe(); String s2 = "Xiao" + LeLe; System.out.println(s1 == s2); // false } private static String getLeLe() { return "LeLe"; }
分析:由于LeLe不能像普通的字符串常量一样被JVM优化处理,所以会创建新的对象执行append方法,详情请看辅助知识的+ 号的拓展。
2.3.6、最重要的一个栗子
-
栗子1
String s1 = new StringBuilder("Xiao").append("LeLe").toString(); System.out.println(s1 == s1.intern()); //true String s2 = new StringBuilder("ja").append("va").toString(); System.out.println(s2 == s2.intern()); //false
分析:为什么同样的两行代码,第一个就返回true,第二个返回false,首先,s1、s2创建完成之后,不会在常量池中创建那个"XiaoLeLe"或者"java"对象,当intern时按道理应该都会指向非字符串常量池中堆的那个对象,那么按道理来说两个应该都是true,为什么第二个会是false,原因很简单,因为java是一个关键字,很可能在JVM初始化的时候就已经放到字符串常量池了,所以s2.intern()指向的是字符串常量池的那个对象,而s2指向的是非字符串常量池中堆的那个对象,所以为false。
2.4、八种基本类型的包装类和对象池
Java为包装类型也实现了一个对象池(也可以叫做常量池,目前只实现了Byte,Short,Integer,Long,Character,Boolean),在对象赋值的时候都会执行一个valueOf的方法,这个方法中就会有一个去找这个值是否在常量池中,如果在,就直接返回,不在,就new一个对象并返回。接下来举几个例子
2.4.1、Integer包装类
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
先通过javap命令编译一下.class文件,结果如下:
如上图所示,我们看见程序在赋值的时候执行了valueOf方法,接着我们进入Integer.valueOf()这个方法去看一下,如下图所示:
接着我们进入到图中红框中的缓存池对象看一下会发现它的范围已经写死,所以,当在这个范围时,就可直接在缓存池里面取,如果不是,new一个新对象,然后返回索引,详情如下图:
2.4.2、其它包装类
相比较于Integer包装类,相同办法自己可以去看看其它的包装类,这里我截取两张valueOf的图,参考一下,原理都一样,详情如下图所示:
- Boolean类型的
- Character类型的
10、辅助知识
10.1、+ 号的拓展
我这里以一段程序来举例:
package Test;
public class Test {
public static void main(String[] args) {
String s1 = "ma";
String s2 = "th";
String s3 = s1 + s2;
String s4 = "math";
System.out.println(s3==s4);
}
}
比如说上面这个程序,它输出的就是false,它为什么会输出false呢?
答案:因为java底层对“+”提供了一种叫语法糖的操作,运算符重载和语法糖的区别,会创建StringBuilder对象并调用它的append方法来操作相当于“+”的功能,所以,既然创建了新的对象,“==”之下肯定就是false,接下来,我通过反编译.class文件来给大家解释一下,如下图所示,详情可参考这篇文章的栈和局部变量的操作。
操作指令 | 含义 |
---|---|
invokespecial | 根据编译时类型来调用实例方法 |
aload_1 | 从局部变量1中装载引用类型值 |
aload_2 | 从局部变量2中装载引用类型值 |
注意看我上图中红框中的东西,这几句话就是执行String s3 = s1 + s2;这行代码时的底层操作,我们可以看到:
1、调用了一个StringBuilder对象并执行了init方法。
2、将局部变量1(s1)的值压入操作数栈。
3、调用append方法将s1添加到操作1中的对象上。
4、将局部变量2(s2)的值压入操作数栈
5、调用append方法将s2添加到操作1中的对象上。
6、调用toString()方法将操作1中的StringBuilder对象转换为String赋值给s3,在调用tostring方法时会new一个对象,详情如下图所示: