常量池、运行时常量池、字符串常量池
文章目录常量池运行时常量池字符串常量池(StringTable)第一个例子第二个例子JDK8常量池存在于字节码文件中二进制字节码中有:类基本信息、常量池、类方法定义(其中包含虚拟机指令)常量池用于存放编译期生成的各种字面量和符号引用字面量:字面量类似与我们平常说的常量,主要包括:文本字符串:就是我们在代码中能够看到的字符串,例如String a = “aa”。其中”aa”...
JDK8
常量池
存在于字节码文件中
二进制字节码中有:类基本信息、常量池、类方法定义(其中包含虚拟机指令)
常量池用于存放编译期生成的各种字面量和符号引用
字面量:
字面量类似与我们平常说的常量,主要包括:
- 文本字符串:就是我们在代码中能够看到的字符串,例如String a = “aa”。其中”aa”就是字面量。
- 被final修饰的变量。
符号引用:
主要包括以下常量:
- 类或接口的全限定名:例如对于String这个类,它的全限定名就是java/lang/String。
- 字段的名称和描述符:所谓字段就是类或者接口中声明的变量,包括类级别变量(static)和实例级的变量。
- 方法的名称和描述符。所谓描述符就相当于方法的参数类型+返回值类型。
反编译javap -v Main.class
这段代码的二进制字节码:
public class Main {
public static void main(String[] args) {
System.out.println("hello world");
}
}
说明:
注释部分是javap帮我们加上的
虚拟机指令解释执行依靠指令后边的符号地址:#数字
,比如#2
表示去常量池找前边标号为#2
的那一行
所以,常量池就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
运行时常量池
运行时常量池位于方法区的元数据区中,方法区是线程共享的,因此运行时常量池也是线程共享的
运行时常量池:常量池在*.class文件,当类加载到方法区时,它的常量池信息就会放入运行时常量池,并把里边的符号地址变为真实地址。应该注意的是,“文本字符串”是放在字符串常量池中的。
字符串常量池(StringTable)
字符串常量池:StringTable又称String pool,jdk6的时候,位于方法区的运行时常量池中,jdk7及其之后,位于堆中。因为方法区永久代的垃圾回收要等到FullGC,而FullGC的触发不是很频繁,导致StringTable中的对象迟迟不被回收,而Java程序中用到的字符串又很多,很容易导致永久代内存不足,所以放到了堆中。
StringTable是一个哈希表结构。堆空间是线程共享的,所有字符串常量池是线程共享的。
当类加载到方法区时,常量池中的文本字符串会加载进入字符串常量池中。
字符串常量池实际存放的堆地址,这块堆地址空间存储的是文本字符(这里网上很多地方说:直接把文本字符串放在字符串常量池中,应该是不对的。此处还未找到权威证明,只在这篇文章中看到和我的想法一样的文章:https://www.cnblogs.com/justcooooode/p/7603381.html。
欢迎指正)
下面为了表述方便,说的是 把字符串“ab”放到字符串常量池中(或称StringTable),实际表明的是 常量池中存放的是“ab”的引用
第一个例子
有这样一道题:
public class Main {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
String s6 = s4.intern();
System.out.println(s3 == s4);
System.out.println(s3 == s5);
System.out.println(s3 == s6);
}
}
分析:
第一步:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
反编译javap -v Main.class
这段代码的二进制字节码:
astore_1
表示把变量s1存储到局部变量表Slot为1的位置。
常量池中的信息如上,当常量池信息被加载到运行时常量池中时,这时候a、b、ab都是常量池中的符号,还没有变为java字符串。当执行到指令ldc #2
时,才会把符号a变为“a”字符串对象(这叫字符串的延迟加载),然后去StringTable中找,如果不存在这个字符串,就把这个字符串放入StringTable中,如果存在了,就直接使用StringTable中的这个字符串。
第二步,修改代码如下:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);
aload_1
表示去局部变量表中Slot为1位置的数据。
astore 4
表示把变量s4存储到局部变量表Slot为4的位置。
执行s1 + s2
的过程实际操作是new StringBuilder().append("a").append("b").toString()
(这属于编译器优化)。StringBuilder中的toString()源码如下,因此s1 + s2
就相当于new String(“ab”),重新new了一个String实例,是在堆上开辟了一块内存存储这个字符串,因此s3和s4不相等。
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
第三步,修改代码如下:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
从反编译结果可以看出,"a" + "b"
是直接去StringTable中找字符串“ab”,而不是先找“a”,再找“b”,最后再拼接,可以看到s5变量的取值,和s3变量的取值是一样的过程。因此s3和s5值相等。
第四步,修改代码如下:
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
String s6 = s4.intern();
先去局部变量表中Slot为4位置的数据(“ab”,这个“ab”字符串时堆上的,因为取的是s4的值),然后调用intern()方法尝试将这个字符串放入StringTable中,如果存在,则不放入,直接拿来用,否则放入,并返回StringtTable中的这个对象(关于intern()具体知识看下边的例子)。
最终结果:
false
true
true
第二个例子
public class Main {
public static void main(String[] args) {
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
// s1和s2是final修饰的,s1+s2相当于把“ab”给s4
// 如果s1或者s2不是final修饰的,则还是用的StringBuilder
String s4 = s1 + s2;
System.out.println(s3 == s4); //true
}
}
public class Main {
public static void main(String[] args) {
String s1 = "ab";
final String s2 = "a";
String s3 = s2 + "b";
System.out.println(s1 == s2); //true
}
}
第三个例子
public class Main {
public static void main(String[] args) {
// 此时堆上有三块空间:new String("a") new String("b") new String("ab")
// StringTable中有:["a",“b”]
String s = new String("a") + new String("b");
// 此时StringTable中还没有"ab",所以会在常量池中创建一个"ab"
// s变为指向常量池中的那个"ab"
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // true
}
}
如果将代码改成这样:
public class Main {
public static void main(String[] args) {
String x = "ab";
// StringTable中有:["ab","a",“b”],下面这行代码得到的“ab”和StringTable中的“ab”不是同一个字符串
String s = new String("a") + new String("b");
// 由于StringTable中已经存在了“ab”,因此s2直接使用这个“ab”
// s并没有变化
String s2 = s.intern();
System.out.println(s2 == x); // true
System.out.println(s == x); // false
}
}
上边是JDK7及其之后的情况,JDK6的时候,执行s.intern(),如果常量池中没有和s相同值的字符串,则会重新创建一个字符串,把这个字符串放到常量池中,并没有使用s指向的那块空间,所以常量池中的"ab"和s并不相等。
在JDK6中执行:
public class Main {
public static void main(String[] args) {
String s = new String("a") + new String("b");
String s2 = s.intern();
System.out.println(s2 == "ab"); // true
System.out.println(s == "ab"); // false
}
}
更多推荐
所有评论(0)