JVM - 常量池

175 阅读5分钟

1. 常量池概述

JVM 中有以下常量池:class 文件常量池、运行时常量池、字符串常量池。

常量池主要目的就是避免重复创建。

2. class 文件常量池

class 文件常量池,指的是 .class 文件 Constant pool。每个 .class 文件都会有自己的常量池。

2.1 查看 .class 文件内容

一般,我们通过 javac 先把 .java 文件编译成 .class 文件, 再通过 javap 反编译 .class 文件, 反编译后就可看到 Constant pool。

javap 可选命令如下

javap -help

用法: javap <options> <classes>

其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置

常用的命令为: javap -c -l -v -p xx.class

下面反编译该 java 类

public class RuntimeConstantPool {
    private static int age = 11;
    public static double money = 12.12;
    public final int total = 13;
    protected static String name = "csp";
    public String k3 = "k3" + "k4";
    
    private void hello() {}
    protected String hello2() {
        String k1 = "k1";
        final String k2 = "k2";
        return k2;
    }
    void hello4() {}
    public void hello3() {}

}

输入 javap -c -l -v -p RuntimeConstantPool.class

public class com.csp.boot.jvm.RuntimeConstantPool
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #13.#33        // java/lang/Object."<init>":()V
   #2 = String             #34            // k3k4
   #3 = Fieldref           #12.#35        // com/csp/boot/jvm/RuntimeConstantPool.k3:Ljava/lang/String;
   #4 = String             #36            // k1
   #5 = String             #37            // k2
   #6 = Fieldref           #12.#38        // com/csp/boot/jvm/RuntimeConstantPool.age:I
   #7 = Double             12.12d
   #9 = Fieldref           #12.#39        // com/csp/boot/jvm/RuntimeConstantPool.money:D
  #10 = String             #40            // csp
  #11 = Fieldref           #12.#41        // com/csp/boot/jvm/RuntimeConstantPool.name:Ljava/lang/String;
  #12 = Class              #42            // com/csp/boot/jvm/RuntimeConstantPool
  #13 = Class              #43            // java/lang/Object
  #14 = Utf8               age
  #15 = Utf8               I
  #16 = Utf8               money
  #17 = Utf8               D
  #18 = Utf8               name
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               k3
  #21 = Utf8               <init>
  #22 = Utf8               ()V
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               hello
  #26 = Utf8               hello2
  #27 = Utf8               ()Ljava/lang/String;
  #28 = Utf8               hello4
  #29 = Utf8               hello3
  #30 = Utf8               <clinit>
  #31 = Utf8               SourceFile
  #32 = Utf8               RuntimeConstantPool.java
  #33 = NameAndType        #21:#22        // "<init>":()V
  #34 = Utf8               k3k4
  #35 = NameAndType        #20:#19        // k3:Ljava/lang/String;
  #36 = Utf8               k1
  #37 = Utf8               k2
  #38 = NameAndType        #14:#15        // age:I
  #39 = NameAndType        #16:#17        // money:D
  #40 = Utf8               csp
  #41 = NameAndType        #18:#19        // name:Ljava/lang/String;
  #42 = Utf8               com/csp/boot/jvm/RuntimeConstantPool
  #43 = Utf8               java/lang/Object

2.2 constant pool 内容介绍

constant pool 在编译后,即可从 .class 文件中得知。

constant pool 存储的是 字面量和符号引号。

字面量: 文本字符串、final 修饰的常量、其他

符号引号: 类与接口的全限定名、方法名和描述符、字段名和描述符

2.2.1 字面量

文本字符串

例如:"k3k4"、"k1"、"k2"、"csp"

final 修饰的常量

例如:final int total = 13

2.2.2 符号引用

类与接口的全限定名

#12 = Class #42 // com/csp/boot/jvm/RuntimeConstantPool

方法名和描述符

#26 = Utf8 hello2

字段名和描述符

#9 = Fieldref #12.#39 // com/csp/boot/jvm/RuntimeConstantPool.money:D

3. 运行时常量池

jdk1.7 后,运行时常量池放在元数据区。元数据区使用的是直接内存(堆外)。

运行时常量池存储的是 字面量、符号引用和直接引用。

字面量、符号引用: 来源于 class 被 JVM 加载后,class 文件常量池的内容会被放入运行时常量池中

直接引用: Ljava/lang/String 这种叫符号引用,因为还不是对象,所以没办法知道其地址,当类加载时, 有部分符号引用会直接被翻译成直接引用。可以简单理解为引用地址。

注:运行时常量池只有 1 个,有多少个 class 文件,就有多少个 class 文件常量池。

4. 字符串常量池

上面提到 运行时常量池存储的还是 文本字符串 而非对象。字符串常量池,就是用来存储字符串常量对象。

除了在编译期就能确定的,哪些文本字符串会被放入字符串常量池中。

String.intern() 方法还提供在运行时动态的将字符串引用放入常量池中。

4.1 String.intern() 动态加入字符串常量池

在 JDK1.7 及之后。当调用 String.intern() 会先在字符串常量池中判断文本字符串是否存在,如果存在则返回池中的引用。如果不存在,则创建一个对象,并将引用放到常量池中。

文本字符串为 key, 引用为 value。

画个图描述一下

image.png

Show me code

String s1 = "hello";
String s2 = new String("hello");
String s3 = s2.intern();
System.out.println(s1 == s2); // false
System.out.println(s3 == s1); // true

String s4 = new String("world!");
String s5 = s4.intern();
System.out.println(s4 == s5); // false

4.2 存储区域变化

JDK1.7 及之后,字符串常量池,从方法区迁移至了 JAVA 堆中。

从方法区迁移至 JAVA 堆,其实是可以理解的设计。

方法区在启动的时候,我们会通过 JVM 设置大小,而且一般不会给太大。

但是 JAVA 程序可以在运行时动态的往字符串常量池追加字符串对象。

这样,就很难估算给方法区多大的内存合适。

总结

image.png

参考

  1. JVM知识梳理之二_JVM的常量池

  2. 通过反编译深入理解Java String及intern + JDK1.8关于运行时常量池, 字符串常量池的要点

  3. JVM详解之:运行时常量池