Class常量池

什么是Class常量池?

  • 我们写的每一个Java类被编译后,就会形成一份Class文件;Class文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用
  • 每一个Class文件中都有一个Class常量池

什么是字面量和符号引用?

  • 字面量包括:
    1. 文本字符串
    2. 声明为final的常量
    3. 八种基本类型的值
  • 符号引用包括:
    1. 类和方法的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符

.class文件都包含哪些内容?

  1. 创建Test.java
public class Test{
    public static void main(String[] args){
    System.out.println("AAA");
    }
}
  1. 将Test.java编译为Test.class
  javac Test.java
  1. 反编译Test.class字节码文件
命令:javap -v Test.class

结果如下:
******************************类的描述信息*****************************************
Classfile /Users/gaotengfei/Desktop/Test.class
Last modified 2022-5-11; size 405 bytes
MD5 checksum f1726f37e72972ae216e51eef3a0d281
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
******************************常量池************************************************
Constant pool:
 #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
 #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
 #3 = String             #18            // AAA
 #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
 #5 = Class              #21            // Test
 #6 = Class              #22            // java/lang/Object
 #7 = Utf8               <init>
 #8 = Utf8               ()V
 #9 = Utf8               Code
#10 = Utf8               LineNumberTable
#11 = Utf8               main
#12 = Utf8               ([Ljava/lang/String;)V
#13 = Utf8               SourceFile
#14 = Utf8               Test.java
#15 = NameAndType        #7:#8          // "<init>":()V
#16 = Class              #23            // java/lang/System
#17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
#18 = Utf8               AAA
#19 = Class              #26            // java/io/PrintStream
#20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
#21 = Utf8               Test
#22 = Utf8               java/lang/Object
#23 = Utf8               java/lang/System
#24 = Utf8               out
#25 = Utf8               Ljava/io/PrintStream;
#26 = Utf8               java/io/PrintStream
#27 = Utf8               println
#28 = Utf8               (Ljava/lang/String;)V
******************************虚拟机中执行编译的方法*****************************************
{
public Test();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=1, locals=1, args_size=1
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 1: 0
//main方法JVM指令
public static void main(java.lang.String[]);
  //main方法描述
  descriptor: ([Ljava/lang/String;)V
  //main方法访问修饰符
  flags: ACC_PUBLIC, ACC_STATIC
  =========================解释器读取JVM指令解释并执行==========================
  Code:
    stack=2, locals=1, args_size=1
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #3                  // String AAA
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
  =========================解释器读取JVM指令解释并执行==========================
    LineNumberTable:
      line 3: 0
      line 4: 8
}
SourceFile: "Test.java"

运行时常量池

什么是运行时常量池?

  • 运行时常量池存在于内存中,是方法区的一部分。它是Class常量池被加载到内存之后的版本。
  • 运行时常量池除了保存Class文件中描述的符号引用外,还会把由符号引用翻译出来的直接引用也存储在运行时常量池中。
  • 运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只在编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量池放入池中。它的字面量是可以动态添加的(String类的intern()方法),符号引用可以被解析为直接引用。
  • JVM在执行某个类的时候,必须经过加载、连接、初始化,而连接又包括验证、准备、解析三个阶段。而当类加载到内存中后,JVM就会将Class常量池中的内容放到运行时常量池中,因此,每个类都有一个运行时常量池。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,也就是StringTable,以保证运行时常量池所引用的字符串与字符串常量池中是一致的。

字符串常量池

字符串常量池在Java内存区域的哪个位置?

  • JDK6.0及之前的版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
  • JDK7.0版本,字符串常量被移到了堆中。

字符串常量池是什么?

  • HotSpot VM里实现的String Pool功能的是一个String Table类,它是一个Hash表,底层是HashSet,默认值大小长度是1009;这个String Table在每个HotSpot VM的实例只有一份,被所有的类共享。字符串常量是由一个个字符组成,放在了StringTable上。
  • JDK6.0中,StringTable的长度是固定的,长度就是1009,因此如果放入String Pool中的String非常多,就会造成hash冲突,导致链表过长,当调用String.intern()时就需要到链表上一个一个找,从而导致性能大幅度下降。
  • JDK7.0中,StringTable的长度可以通过参数指定:-XX:StringTableSize=66666

字符串常量池里放的是什么?

  • JDK6.0之前版本中,Spring Pool里放的都是字符串常量
  • JDK7.0中,由于String.intern()发生了改变,因此String Pool中也可以存放放置在堆内的字符串对象的引用。
  • ⚠️字符串常量池中的字符串只存在一份,且被所有线程共享
      public class Test1 {
      public static void main(String[] args) {
          /**
           * 执行完String s="abc";后,字符串常量池已经存在"abc",因此b不会在常量池中申请新的空间,而是直接把字符串"abc"的内存地址返回给b
           */
          String a="abc";
          String b="abc";
          System.out.println(a==b);
      }
      }
    
  • ⚠️全局字符串池里的内容是在类加载完成,经过验证、准备阶段之后在堆中生成字符串对象实例,然后将该字符串对象实例的引用值存到String Pool中;String Pool中存的是引用值而不是具体的实例对象,具体的实例对象是在堆中开辟的一块空间存放的。

常量池内存位置演化

  1. JDK1.7之前运行时常量池逻辑包含字符串常量池,存放在方法区,此时HotSpot VM对方法区的实现方式为永久代。
    2
  2. JDK1.7字符串常量池和静态变量被从方法区拿到了堆中,运行时常量池剩下的还在方法区,也就是HotSpot的永久代中。
    1
  3. JDK1.8HotSpot废除永久代的概念,用元空间(Metaspace)代替,这时候字符串常量池还在堆中,运行时常量池还在方法区,只不过方法区从永久代变成了元空间(Metaspace)。
    3
    4.关于永久代
    • ⚠️在JDK8以前,许多Java程序员都习惯在HotSpot虚拟机上开发、部署程序,很多人更愿意把方法区称呼为“永久代”,或将两者混为一谈。本质上这两者并不是等价的,因为仅仅是当时的HotSpot虚拟机设计团队把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已,这样使得HotSpot的垃圾收集器能够像管理Java堆一样管理这部分内存,省去专门为方法区编写内存管理代码的工作。对于其他虚拟机,例如BEA、JROCkit、IBM等是不存在永久代的概念的。
    • 永久代这种设计导致Java应用更容易遇到内存溢出的问题(永久代有- XX:MaxPermSize的上限,即使不设置也有默认大小,而J9JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB限制,就不会有问题),而且有极少数方法(例如String::intern())会因永久代的原因而导致不同虚拟机有不同的表现。
    • JDK6的时候HotSpot开发团队就有放弃永久代、逐步改为采用本地内存(Native Memory)来实现方法区的计划。