深刻理解运行时常量池、字符串常量池

3,873 阅读5分钟

「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

一、静态常量池和运行时常量池

Class常量池可以理解为是Class文件的资料库。常量池用于存放编译期生成的各种 "字面量""符号引用"

类文件中除了包含类的版本、字段、方法、接口等描述信息,还包含的就是常量池。如下图所示:

我们可以通过javap命令生成更可读的JVM字节码指令文 件:

image

1. 字面量

字面量指由字母、数字等构成的字符串或者数字常量。

字面量只可以右值出现,所谓的右值就是指等号右边的值,比如:int a=1 这里的a为左值,1为右值,在这个例子中1就是字面量

int a = 1;
int b = 2;
String c = "abcdefg"
package com.lxl.jvm;
public class Math {
    public static int initData = 666;
    public static User user = new User();
    public User user1;
​
    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }
​
    public static void main(String[] args) {
        System.out.println("aaa");
        Math math = new Math();
        while(true){
            math.compute();
​
        }
    }
}

2、符号引用

符号引用是编译原理中的概念,是相对于直接引用来说的,主要包括以下三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

上面的例子中,

  • a、b是字段名称,是一种符号引用

  • 在Math类中,

    • com/lxl/jvm/Math是类的全限定名,也是符号引用
    • main和compute是方法名,也是符号引用
    • ()是UTF8格式的描述符,这些都是符号引用。

最开始这些字面量和符号引用都是保存在常量池中,他们都是静态信息

当程序运行时被加载到内存后,这些符号才有对应的内存地址信息。这些常量一旦被转入内存就会变成运行时常量池。运行时常量池在方法区中。

再说的明白一些,到底什么是常量池,什么是运行时常量池?

Math类,生成的对应的class文件,class文件中定义了一个常量池集合,这个集合用来存储一系列的常量。这时候的常量池是静态常量池。

当程序运行起来,会将类信息加载到方法区中,并为这些常量分配内存地址,这时原来的静态常量池就转变成了运行时常量池。

符号引用在程序运行以后被加载到内存中,原来的代码就会被分配内存地址,引用这个对象的地方就会变成直接引用,也就是我们说的动态链接了。

例如,上面Math类中的compute()方法,compute()这个符号引用在运行时就会被转变为compute()方法具体代码在内存中的地址,主要通过对象头里的类型指针去转换直接引用。

这句话的意思是:compute()方法被加载到内存以后,就有了自己的地址,原来调用computer()方法的符号引用,现在就变成对compute()地址的直接引用,这个直接引用是存在对象头里的,通过指针来指向直接饮用

二、字符串常量池

String c = "abcdefg"

字符串常量就保存在字符串常量池中

1、 字符串常量池的设计思想

1)字符串的分配,和其他的对象分配一样,耗费高昂的时间与空间代价,作为最基础的数据类型,大量频繁的创建字符串,极大程度地影响程序的性能

2)JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化

  • 为字符串开辟一个字符串常量池,类似于缓存区
  • 创建字符串常量时,首先查询字符串常量池是否存在该字符串
  • 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中

比如,下面的案例

String c = "abcdefg"
String d = "abcdefg"
  • 首先在字符串常量池中开辟一块空间,用来保存字符串“abcdefg”,然后在让符号引用c指向“abcdefg”的地址。
  • 执行到第二句代码时,会发现内存中已经有“abcdefg”了,那么就不会再创建了,而是直接让符号引用d指向“abcdefg”的地址。

2、字符串常量池保存的位置

Jdk1.6及之前: 有永久代, 运行时常量池在永久代,运行时常量池包含字符串常量池

Jdk1.7:有永久代,但已经逐步“去永久代”,字符串常量池从永久代里的运行时常量池分离到堆里

Jdk1.8及之后: 无永久代,运行时常量池在元空间,字符串常量池里依然在堆里

用一段代码来验证字符串常量到底是存在哪里的?

package com.lxl.jvm;
​
import java.util.ArrayList;
​
/**
 * jdk6:‐Xms6M ‐Xmx6M ‐XX:PermSize=6M ‐XX:MaxPermSize=6M
 * jdk8:-Xms6M -Xmx6M -XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000000; i++) {
            //String str = String.valueOf(i).intern();
            //String.valueOf(i);
            list.add("123");
        }
    }
}

Jdk7及以上:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

jdk6及以下

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

三、运行时常量池,字符串常量池,堆,方法区的位置关系

关于运行时常量池和字符串常量池以及和方法区,堆得关系,看下图:

image

jdk1.6:有永久代,运行时常量池保存在永久代中,字符串常量池是运行时常量池的一部分

image

jdk1.8及以后:有元空间,运行时常量池保存在元空间中,字符串常量池保存在堆中

\