Class 常量池
Class 常量池可以理解为 Class 文件中的资源仓库, Class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是 常量池(constant pool table), 用于存放编译期生成的各种 字面量(Literal)和符号引用(Symbolic References)
我们一般会通过 javap
命令生成可读的 JVM 字节码指令文件:
Classfile /Users/zhengsh/cqvie.edu.cn/ssm-salt/ssm-salt-jvm/src/main/java/cn/edu/cqvie/jvm/Math.class Last modified 2021年2月4日; size 499 bytes MD5 checksum 037cef02562301117a47bc10b250c770 Compiled from "Math.java" public class cn.edu.cqvie.jvm.Math minor version: 0 major version: 52 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #2 // cn/edu/cqvie/jvm/Math super_class: #7 // java/lang/Object interfaces: 0, fields: 0, methods: 3, attributes: 1 Constant pool: #1 = Methodref #7.#18 // java/lang/Object."<init>":()V #2 = Class #19 // cn/edu/cqvie/jvm/Math #3 = Methodref #2.#18 // cn/edu/cqvie/jvm/Math."<init>":()V #4 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream; #5 = Methodref #2.#22 // cn/edu/cqvie/jvm/Math.compute:()I #6 = Methodref #23.#24 // java/io/PrintStream.println:(I)V #7 = Class #25 // java/lang/Object #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 compute #13 = Utf8 ()I #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 SourceFile #17 = Utf8 Math.java #18 = NameAndType #8:#9 // "<init>":()V #19 = Utf8 cn/edu/cqvie/jvm/Math #20 = Class #26 // java/lang/System #21 = NameAndType #27:#28 // out:Ljava/io/PrintStream; #22 = NameAndType #12:#13 // compute:()I #23 = Class #29 // java/io/PrintStream #24 = NameAndType #30:#31 // println:(I)V #25 = Utf8 java/lang/Object #26 = Utf8 java/lang/System #27 = Utf8 out #28 = Utf8 Ljava/io/PrintStream; #29 = Utf8 java/io/PrintStream #30 = Utf8 println #31 = Utf8 (I)V
Constant pool:
下面的内容就是常量池,常量池中主要存放的是字面量和符号引用。
字面量
字面量就是指字母、数字等构成的字符串或者数值常量。
字面量只可以以右值出现,所谓右值是指等号右边的值,如 int a = 1, 这里的 a 是左值, 1 为右值。在这个例子中 1 就是一个字面量。
int a = 1; int b = 2; String str = "Hello !";
符号引用
符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括以下三类常量
- 类和接口的全限定名
- 字段名称和描述符
- 方法名称和描述符
对于上面的 a, b 是字段名词, 就是一个符号引用,还有类似常量池中的 Ljava/io/PrintStream
是类的权限定名, main
和 compute
是方法名称,这些都是符号引用。这些常量池现在都是静态信息,只有到运行时被加载到内存后,这些符号才有对应的内存地址信息,这些常量池一旦被装入内存就变成了 运行时常量池, 对应的符号引用在程序加载或运行时就会被转变为被加载内存区的代码的直接引用,也就是我们说的动态连接了, 例如,compute()
这个符号引用在运行时就被转换成了 compute()
方法具体代码在内存中的地址,主要是通过对象头里的类型指针去转换为直接引用。
字符串常量池
字符串常量池的设计思想
- 字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的常见字符串,极大程度的影响程序的性能。
- JVM 为了提高性能和减少开销,在实例化字符串常量的时候做了一些优化
- 为字符串开辟了一个字符串常量池,类似缓冲区
- 创建字符串常量时,首先查询字符串常量池是否存在该字符串
- 存在该字符串,返回引用实例,不存在,实例化该字符串并放入常量池中
三种字符串操作(Jdk1.7 以上版本)
- 直接赋值操作
String a = "abc"; // 指向常量
这个方式创建的字符串对象,只会在常量池中。
因为有 “abc” 这个字面量,在创建的时候, JVM 会先去常量池中通过 equals(key) 方法,判断是否有相同的对象如果有,则直接返返回该对象在常量池中的引用;
如果没有,则在常量池中创建一个新的对象,再返回引用。
- new 创建对象
String s1 = new String("abc"); // s1 指向内存中对象的引用
这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。
步骤如下:
因为有 “abc” 这个字面量,所以先回去常量池中检查字符串常量池中是否存在字符串 “abc” 。
不存在,先在字符串常量池里创建一个字符串对象;再去内存中创建一个字符串对象 “abc”;
如果存在的话,直接去堆内存中创建一个字符串对象 “abc”。
最后将内存中的引用返回。
- intern 方法
String s1 = new String("abc"); String s2 = s1.intern(); System.out.println(s1 == s2) //false
String 中的 intern 方法是一个 native 的方法, 当调用 intern方法时, 如果字符串常量池已经包含一个等于此 String 对象的字符串 (用equals(object)方法确定),则返回池中的字符串。否则,将intern返回的引用指向当前字符串 s1(jdk1.6版本需要将 s1 复制到字符串常量池里)。