三种常量池
1、常量池版本变化:
- jdk1.6及以前,运行时常量池在方法区中
- jdk1.7,运行时常量池移到堆中去了
- jdk1.8,运行时常量池移到元空间中,取消了方法区(永久代)
2、下面是jdk1.8的三种常量池
类文件中的常量池:
存在class文件中,所处区域堆中,内容概要符号引用和字面量,class常量池是在编译的时候每个class都有的,在编译阶段,存放的是常量的符号引用。
运行时常量池:
存在于内层元空间中,JVM运行时,类信息、常量、静态变量、即时编译期编译后的代码
class类文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域
运行时常量池是在类加载完成之后,将每个class类常量池中的符号引用值转存到运行时常量池中,也就是说,每个class都有一个运行时常量池,类在解析之后,将符号引用替换成直接引用,与全局常量池中的引用值保持一致
字符串常量池:
- jdk1.7之前存在方法区中。(jdk1.8方法区直接没了,取而代之的是元空间)
- jdk1.7/8及后面,存在堆中,下面intern研究的就是该区
可以使用代码验证(jdk1.7以前版本这里就不演示了,ps:OutOfMemoryError: PermGenspace)
下面是jdk1.8,反复创建字符串会oom,堆内存溢出,证明字符串常量词是在堆中
3、各种字面量
对于String s="2",int i=3; 2、3这两个值就是字面量
4、符号引用
当一个A类引用了B类的时候,A类加载编译的时候,不知道B类是否被加载,在内存等,就在A类中符号引用记录B类,当A运行时,当虚拟机将B加载到实际内存的时候,就将B类的符号引用替换成实际地址(每一个类在编译的时候都会存在大量符号引用,以供虚拟机在具体执行类的时候将符号地址替换成实际地址)
ps:文章后面提到如果没有写明都是指字符串常量词
补充知识
- 有一张表string pool维护着字符串常量词的引用。
- 对于字符串创建,不管是直接创建还是new,只要是遇到""引号就会去常量池检查引号里面的字符,没有则创建。
- jdk1.7以前,intern方法在调用后会在常量池找,如果没有找到会将String插入到常量池中,如果找到,则返回String常量池的引用(此时常量词在方法区,不在堆中)。
- jdk1.7及其后,intern方法在调用后首先是在string pool中的常量池找,没有则在堆里找,找到则返回堆中引用到常量池,在返回常量池的引用,实际上是间接返回堆中的引用。都没有找到则在字符串常量池创建一个,并返回。(重点)
字符串池里的内容是在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到string pool中(记住:string pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的)。 在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。
看下面例子前,了解两个创建对象语句(这里假设常量词是没有字符1)
1、String s = new String("1");
对于这个语句,它创建了两个对象,一个是new 一个是字符串常量池中的对象(遇到引号,先在字符串常量池创建"1",然后在堆中new对象)
2、String s = new String("1") + new String("1");
对于这个语句,它共创建了五个对象
-
两个new匿名对象(堆中)
-
一个1的字符串常量(new String("1")的时候,就创建了1的字符串常量,第二个new因为已经存在,所以不再创建)
-
一个其内部实现是先new一个StringBuilder对象,然后 append(new String("1")),append(new String("1")
- 最后一个让s引用toString()返回的对象“11”,相当于new出来的一个对象,但是“11”并没有加入字符串常量池。
例子讲解
下面都是在jdk1.8的情况下
1、未使用intern
String s = new String("1");
String s2 = "1";
System.out.println(s == s2);//false
输出false
其中s是引用了new的对象,但是它还在字符串常量池创建了对象。s2引用的是字符串常量池的对象,因为s已经创建好了。两个引用不一样,所以输出false。
2、使用了intern
String s = new String("1").intern();
String s2 = "1";
System.out.println(s == s2);//true
输出true
其中s是引用了new的对象,但是它还在字符串常量池创建了对象,并且调用intern后,返回了字符串常量池的引用,最后s实际是引用了字符串常量池的引用。s2引用的是字符串常量池的对象,因为s已经创建好了。两个引用都是字符串常量池的引用,所以输出true。
3、使用了intern
String s = new String("1");
String s2 = s.intern();
String s3 = "1";
System.out.println(s == s2);//false
System.out.println(s == s3);//false
System.out.println(s2 == s3);//true
输出false false true
其中s是引用了new的对象,但是它还在字符串常量池创建了对象。s2对象调用了intern后,字符串常量池已经有"1",返回了"1"字符串常量池的引用。s3引用的是字符串常量池的对象,因为s已经创建好了。s2与s3引用都是字符串常量池的引用,所以输出true。s引用的是new的引用,与它比较输出false
4、未使用intern
String s3 = new String("1") + new String("1");
String s4 = "11";
System.out.println(s3 == s4);//false
输出false
引号内容只有1,字符串常量池只创建了"1",没有创建"11",s3返回的是引用了toString产生的对象,并且它不在字符串常量池。s4未在字符串常量池找到"11",所以在字符串常量池创建了"11"并返回引用。最终两者对象引用不一样,输出false。
5、使用了intern(常量池在jdk1.7移到了堆中)
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//true
输出true
其中s3是引用了toString产生的对象,相当于new在堆中的对象s3,并且它不在字符串常量池。调用s3.intren()后,在字符串常量池引用了new的对象s3的引用,s4在字符串常量池找到"11"并直接返回其引用,其实就是间接引用了在堆中的对象s3。最终两者对象引用一样,输出true。(多读几遍)
如下图看:
是不是认为自己懂了?那就测试一下吧
package com.xue.test;
/**
* 彻底吃透String--常量池--intern
* @author
*
*/
public class Test3 {
public static void main(String[] args) {
//一次放开一个多行注释运行
/* String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);//false
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);//true
*/
/*String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);//false
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);//false
*/
/*//+连接但编译器不优化
String s1 = new String("xy") + "z";
String s2 = s1.intern();
System.out.println( s1 == s1.intern() );//true
System.out.println( s1 + " " + s2 );
System.out.println( s2 == s1.intern() );//true
*/
/*// 一般情况
String s1 = new String("xyz") ;
String s2 = s1.intern();
System.out.println( s1 == s1.intern() );//false
System.out.println( s1 + " " + s2 );
System.out.println( s2 == s1.intern() );//true
*/
/*//编译器优化,会拼成 "xyz"在编译
String s1 = "xy" + "z";
String s2 = s1.intern();
System.out.println( s1 == s1.intern() );//true
System.out.println( s1 + " " + s2 );
System.out.println( s2 == s1.intern() );//true
*/
}
}
还不懂?
看参考博客:String的Intern方法详解