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
    }
}
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐