Java中的常量池,实际上分为两种形态:静态常量池和运行时常量池。
静态常量池就是你编译生成的.class文件里面存放的数据,静态常量池主要存放:字面量和符号引用,所谓字面量就是一些文本字符串,被声明为final类型的final常量值等。而符号引用主要是编译原理方面相关的概念。主要包括下面几类常量:
- 类和接口的名字(全限定名)
- 字段的名称和描述符
- 方法的名称和描述符
- 方法类型和方法句柄
虚拟机指令根据这种表找到要执行的类名,方法名,参数类型等信息。
而运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,把class文件里的死的数据变成一个个有真实内存地址的数据。
我们来反编译一段class文件使用javap -v XXX(不带.class扩展名),查看它的常量池部分
如下,可以看到一些类名、方法名、字段名和一些源代码中出现的字符串。
分析StringTable
String table又称为String pool,字符串常量池,其存在于堆中(jdk1.7之后改的)。最重要的一点,String table中存储的并不是String类型的对象,存储的是指向String对象的索引,真实对象还是存储在堆中。-XX: StringTableSize设置StringTable的长度在,在jdk8中,1009是 可设置的最小值。
String s1 = "a";
String s2 = "b";
String s3 = "ab";
准备上面的代码,反编译后对应的指令如下
#这是反编译的结果
0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 args [Ljava/lang/String;
3 27 1 s1 Ljava/lang/String;
6 24 2 s2 Ljava/lang/String;
9 21 3 s3 Ljava/lang/String;
Constant pool:
#1 = Methodref #10.#29 // java/lang/Object."<init>":()V
#2 = String #30 // a
#3 = String #31 // b
#4 = String #32 // ab
………………………………………………………………
指令部分的解读:
-
先从常量池中加载a这个字符串。再存取到1号变量s1……
-
这些个字符串常量信息都会被加载到运行时常量池中。但像a这些字符串仅仅是一些符号,还不是真正的java对象。
-
当我们具体执行到 0: ldc #2 时他就会找a这个符号,他要把a变成字符串对象
-
先从StringTable[] 找有没有“a“,把“a”当key找 (StringTable[]是一个hash表,长度是固定的可以通过命令改变大小),第一次找找不到,他就会把生成的”a“对象放入StringTable[]。
-
每一个字符串对象都不是事先放在串池里的,而是执行到对应指令时才开始创建对象的,用不到不会创建。是一个懒惰的。
字符串变量相加
看图可知,两个字符串变量相加其实创建了一个新的StringBuilder对象,加号也变成了append方法,这种动态拼接的字符串结果不会存放在池中。如果出现一个s4="ab" 问这个s3==s4吗?答案是false,因为s4的引用为“ab”,它存放在串池中,而s3是new出来的,虽然值是一样的,但地址是不同的。
字符串常量相加
可以看但,s5的初始化指令就是直接给他赋值了ab并没有拼接,这其实是编译器的一种优化。因为前一个变量的拼接在编译器不可见,只可能会有变化,随意编译器不能给变量进行优化。
此时s3和s5是相等的,因为执行到s3前,串池中并没有"ab",执行到s3后,才把"ab"加入到串池,串池"ab"的地址就是s3的值;当执行的s5的时候,s5发现串池中有"ab",所以它就找到这个"ab"的引用地址,赋给s5。
再来测试一下StringTable的延迟加载
需要借助idea调试工具的memory功能查看运行时的内存。每次执行完一行,他才会在StringTable中加入一个字符串
Intern()
主动将串池中还没有的字符串放入串池,如果存在则返回这个字符串的引用,否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。
下图执行完第一行字符串"ab"并不会放在串池中,因为他是动态拼接的,执行了intern方法后,把s1的地址引用放进串池,并返回地址给s2,所以都相等。
String s1=new String("a")+new String("b");
//执行完上面的代码后StringTable为["a","b"]
//串池并没有字符串"ab",我们可以手动的把"ab"添加到串池中
String s2 = s1.intern();//注意这里返回的是s1的地址
//true
System.out.println(s1==s2);
//true
System.out.println(s2=="ab");
如下图执行intern发现串池中已经有"ab"了,所以返回了s3的地址赋给s2
String s1=new String("a")+new String("b");//["a","b"]
String s3 = "ab"; //["a","b","ab"]
String s2 = s1.intern(); //发现"ab"在串池中有,返回s3的地址
System.out.println(s1==s2);//false
System.out.println(s1==s3);//false
System.out.println(s2==s3);//true
相信下图应该对你来说很简单了
String s1="a";
String s2="b";
String s3="a"+"b";
String s4=s1+s2;
String s5="ab";
String s6=s4.intern();
System.out.println(s3==s4);//false
System.out.println(s3==s5);//true
System.out.println(s3==s6);//true
String x2=new String("c")+new String("d");//x2是堆内存的,x1引用了常量池的。
String x1="cd";
x2.intern();
System.out.println(x2==x1);//false
StringTable优化
StringTable是会发生垃圾回收的,会发生内存溢出OOM
1.添加参数可以增加hash桶的个数(最小值可以设置为1009):-XX:StringTableSize=200000
must be between 1009 and 2305843009213693951
通过减少桶的冲突,可以提高jvm的效率。hash分布。桶个数较少的话,效率低,个数大则浪费空间。
2.运用intern方法将字符串入池,保证相同的字符串只存储一份(在串池中如果已经有相同的字符串对象就不会再创建该字符串对象了)