jvm

JVM 系列 内存管理之内存区域

开启JVM探索新篇章

Posted by lichao modified on November 7, 2019

class 文件常量池

class 文件常量池是指编译生成的 class 字节码文件结构中的一个常量池(Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载后,存放于方法区的运行时常量池。

字面量: 字符串字面量和声明为 final 的常量值(基本数据类型)

  • 字符串字面量除了类中所有双引号括起来的字符串(包括方法体内的),还包括所有用到的类名、方法名和这些类与方法的字符串描述、字段(成员变量)的名称和描述符
  • 声明为 final 的常量值指的是成员变量,不包含本地变量,本地变量是属于方法的。

符号引用包括类和接口的全限定名(包括包路径的完整名)、字段的名称和描述符、方法的名称和描述。只不过是以一组符号来描述所引用的目标,和内存并无关,所以称为符号引用,直接指向内存中某一地址的引用称为直接引用

基本数据类型常量在常量池存储

通过下面的例子来讲解下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Test {  
      
    private final int a = 10;  
    private final int b = 10;  
    private float c = 11f;  
    private float d = 11f;  
    private float e = 11f;  

    private String s1 = "JVM原理";  
    private String s2 = "JVM原理";  
      
}

创建完上面的 java 文件后,执行 javac Test.java ,再执行 javap -v Test 查看 class 文件。 存储概览

从上面的结果可以看到在常量池中,只有一个常量 10 、一个常量 11f、一个常量 “JVM原理”。代码中所有用到 int 类型 10 的地方,会使用指向常量池的指针值#16 定位到第#16个常量池项(cp_info),即值为 10 的结构体 CONSTANT_Integer_info,而用到 float 类型的 11f 时,也会指向常量池的指针值#4来定位到 第#4个 常量池项(cp_info) 即值为 11f 的结构体 CONSTANT_Float_info。我们可以看到 CONSTANT_String_info 结构体位于常量池的第#8个索引位置,且该结构体直接存储了指针值#37,用来指向第 #37 个常量池项。而存放 ”JVM原理” 字符串的 UTF-8 编码格式的字节数组被放到 CONSTANT_Utf8_info 结构体中,该结构体位于常量池的第#37个索引位置。

类文件中定义的类名和类中使用到的类常量在常量池存储

CONSTANT_Class_info 结构体中,存在一个索引值,用于指向存储了[类二进制形式的完全限定名称]字符串的 CONSTANT_Uft8_info 结构体。该索引值占两个字节大小,所以它能表示的最大索引是 65535(2 的 16 次方 -1),也就是说常量池中最多能容纳 65535 个常量项。所在在定义类时要注意类的大小。

假设定义了一个 ClassTest 的类,并把它放到com.focus.jvm 包下,则 ClassTest类的完全限定名为com.focus.jvm.ClassTest,将JVM编译器将类编译成class文件后,此完全限定名在class文件中,是以二进制形式的完全限定名存储的,即它会把完全限定符的”.”换成”/“ ,即在class文件中存储的 ClassTest类的完全限定名称是”com/focus/jvm/ClassTest”。因为这种形式的完全限定名是放在了class二进制形式的字节码文件中,所以就称之为二进制形式的完全限定名。

1
2
3
4
5
6
7
8
9
10
11
package com.focus.jvm;  
import  java.util.Date;  
public class ClassTest {  
    private Date date1 =new Date();  
    private Date date2;  
    private Date date3;

    public ClassTest(){  
        date2  = new Date();  
    }  
}

存储概览 由上图可以看到,在 ClassTest.class 文件的常量池中,共有 3 个 CONSTANT_Class_info 结构体,表示 ClassTest 中用到的 Class 信息。一个 java/util/Date 的 CONSTANT_Class_info 结构体,它在常量池中的位置是 #2,存储的指针值为#19,它指向了常量池的第 19 个常量池项;一个 com/focus/ClassTest 的 CONSTANT_Class_info 结构体,它在常量池中的位置是 #6,存储的指针值为 #22,它指向了常量池的第 22 个常量池项;另外一个 java/lang/Object 的CONSTANT_Class_info结构体,它在常量池中的位置是 #7,存储的指针值为 #23,它指向了常量池的第 23 个常量池项。

Java 中规定所有的类都要继承 java.lang.Object 类,即所有类都是 java.lang.Object 的子类。JVM在编译类的时候,即使我们没有显示地继承 Object,JVM编译器在编译的时候会自动帮我们加上去。所以对于某个类而言,其class文件中至少会有两个CONSTANT_Class_info常量池项,用来表示自己的类信息和其父类信息。如果类声明实现了某些接口,那么接口的信息也会生成对应的CONSTANT_Class_info常量池项。

如果在类中使用到了其他的类,只有真正使用到了相应的类,JDK编译器才会将类的信息组成CONSTANT_Class_info常量池项放置到常量池中。如下面的代码:

1
2
3
4
5
6
7
8
package com.focus.jvm;  
import  java.util.Date;  
public class ClassTest {  
     
    private Date date3;

   
}

编译后结果: 存储概览 在JDK将其编译成class文件时,常量池中并没有java.util.Date对应的CONSTANT_Class_info常量池项。它认为你只是声明了“Ljava/util/Date”类型的变量,并没有实际使用到Ljava/util/Date类。

总结

  • 对于某个类或接口而言,其自身、父类和继承或实现的接口的信息会被直接组装成CONSTANT_Class_info常量池项放置到常量池中;
  • 类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有对应的CONSTANT_Class_info常量池项;
  • 类中或接口中仅仅定义某种类型的变量,JDK只会将变量的类型描述信息以UTF-8字符串组成CONSTANT_Utf8_info常量池项放置到常量池中。

运行时常量池

运行时常量池是方法区的一部分,是一块内存区域。class文件常量池将在类加载后进入方法区的运行时常量池中存放。一个类加载到JVM中后对应一个运行时常量池,运行时常量池相对于class文件常量池来说具备动态性,即字面量可以动态的添加。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量(基本类型包装类和String),这些常量被放在运行时常量池中。class文件常量池只是一个静态存储结构,里面的引用都是符号引用。而运行时常量池可以在运行期间将符号引用解析为直接引用。可以说运行时常量池就是用来索引、查找字段和方法名称和描述符的。给定任意一个字段或方法的索引,最终可得到该字段或方法所属的类型信息和名称及描述符信息。

基础类型

java中基本类型的包装类的大部分都实现了常量池技术,即Byte,Short,Integer,Long,Character,Boolean。这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。两种浮点数类型的包装类Float,Double并没有实现缓存池技术。

字符串常量池

字符串常量池是全局的,JVM中独此一份,因此也称为全局字符串常量池。运行时常量池中的字符串字面量若是成员的,则在类加载初始化阶段就使用到了字符串常量池;若是本地的,则在使用到的时候才会使用字符串常量池。其实,“使用常量池”对应的字节码是一个ldc指令,在给String类型的引用赋值的时候会先执行这个指令,看常量池中是否存在这个字符串对象的引用,若有就直接返回这个引用,若没有,就在堆里创建这个字符串对象并在字符串常量池中记录下这个引用。String类的intern()方法还可以在运行期间把字符串放到字符串常量池中。

在 jdk1.6(含)之前也是方法区的一部分,并且其中存放的是字符串的实例; 在 jdk1.7(含)之后是在堆内存之中,存储的是字符串对象的引用,字符串实例是在堆中; jdk1.8 已移除永久代,字符串常量池是在本地内存当中,存储的也只是引用

JVM中除了字符串常量池,8种基本数据类型中除了两种浮点类型外,其它的6种基本数据类型的包装类都使用了缓冲池,但是Byte、Short、Integer、Long、Character这几个包装类只有对应值在[-128,127]时才会使用缓冲池,超出此范围仍会去创建新的对象。