【JVM】class文件解析

class文件解析

  • 在我们编译后的class文件内部都是存的16进制的数据

  • 里面到底储存了什么数据,又有什作用呢,下面我们一起来学习学习

Class文件结构示意图

下面的图是一字排开形式,都是平级关系,由于图片排开太长,所以我画成了下面这样【容易误会,特此说明】

Class常量池如何储存数据

看上面的组织结构图,第一层的常量池数据区,那是我们这一小节的攻克目标

常量池里面又是什么结构组织呢?

  • 首先我们看到常量池数据区的左边是常量池计数器

    • 常量池计数器:记录的是常量池数据区中的常量池项cp_info的数量

      • 从1开始计数,第一个有用常量池项为1,所以常量池项的索引也是从1开始

      • 至于索引为0的数据空间,class文件规范是如此定义的

常量池项 (cp_info) 的结构

cp_info{
    tag:xx
    info[]:xx
}
  • tag:类型,用标记下面info数组的数据类型

  • info[]:若干个字节构成数组

细化了的常量池的结构会是类似下图所示的样子:

int和float数据类型在常量池中的储存?

比如我们用这个代码做测试:

  • 我们声明了五个变量,但是取值就两种int类型的10 和Float类型的11f

public class IntAndFloat{
    private final int a = 10;
    private final int b = 10;
    private float c = 11f;
    private float d = 11f;
    private float e = 11f;
}

javac编译为字节码.class文件,然后我们使用:javap -v IntAndFloat.class观察其常量池中的信息

  • 编译器会将10和11f分别包装成consant_integer_info和consant_float_info结构体

    • 然后放置搭到常量池中去

  • 可以看到虽然我们在代码中写了两次10 和三次11f,但是常量池中,就只有一个常量10 和一个常量11f

    • 第四个常量池项为 :11.0f

    • 第十三个常量池项:10

    • 其他的常量池项的意义我们后面会说道

  • 代码中所有用到 int 类型 10 的地方,会使用指向常量池的指针值#13 定位到第#13 个常量池项(cp_info),即值为 10的结构体CONSTANT_Integer_info

  • 而用到float类型的11f时,也会指向常量池的指针值#4来定位到第#4个常量池项(cp_info) 即值为11f的结构体CONSTANT_Float_info

long和double数据类型在常量池中的储存?

我们都知道long和double都是占用8个字节的数据类型,那么他妈的储存和上面四个字节有何区别呢?

如int和float的演示代码一致,我们也会发现,无论在代码中声明了几个变量,只要取值相同,那么该值只会在常量池中存有一份

String 类型的字符串在常量池中如何储存?

  • 对于字符串而言,JVM会将字符串类型的字面量以UTF-8 编码格式存储到在class字节码文件中

  • 编译时会将这些字符串转换成CONSTANT_String_info结构体,然后放置于常量池中

用代码演示一下:

public class StringTest {
    private String s1 = "JVM原理";
    private String s2 = "JVM原理";
    private String s3 = "JVM原理";
    private String s4 = "JVM原理";
}

操作手法一致,先变异后查看常量词信息:javap -v StringTest.class

  • 好像和上面有一点不同了,我们首先先把储存细节说一下,然后就明白哪里不同了

CONSTANT_String_info {
    u1 tag=8;
    u2 string_index;   //常量池索引
}
  • 这就是String在常量池中的储存的前一部分,可以发现不同的是

    • 本来为数据的 u4 bytes 变成了 u2的 string_index

  • String类型的储存和其他类型的储存还是有很大的区别,他们是直接储存,而String不是

    • string_index储存的并不是直接的字节数组,而储存的是真实数据的一个索引值

    • 被指定的索引值得对应的数据才是真实的数据所在

    • 拿上图说话,#2 后面没有任何数据,指向的是#21

    • #21后面的数据才是我们的数据所在,而#21的结构体如下所示

    • CONSTANT_String_info {
          u1 tag=1;         //为 utf8类型,而不是8
          u2 length;        //该字节数组的长度
          u1 bytes[length]; //使用utf8编码后的字节数组数据
      }

字符串储存的细节为:【联合上图的代码测试】

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

说了上面的String类型的储存,下面我们在说引用类型的储存就简单多了 JVM会将某个Java 类中所有使用到了的类的完全限定名 以二进制形式的完全限定名 封装成 CONSTANT_Class_info结构体中,然后将其放置到常量池里,其结构如下所示:

CONSTANT_String_info {
    u1 tag=7;        
    u2 name_index
}
  • name_index的值是某个constant_utf8_info结构体在常量池中的索引

  • 对应的constant_utf8_info结构体储存了对应的二进制形式的完全限定名称的字符串

  • name_index是占有两个字节,也就是说它最大能表示65535(2的16次方 - 1)

    • 所以常量池中最大容量65535个常量项,在定义Java类时应该注意类的大小,不能太大

我们还是简单定义一个测试,来说明一下

import java.util.Date;
public class ClassTest {
    private Date date =new Date();
}

老规矩,先编译后查看常量池信息:javap -v ClassTest.class

 

 

  • 我们一个发现了三个CONSTANT_Class_info类型的结构体

  • #2 的name_index指向 #16

    • #16:承载的是Data类的二进制形式的完全限定名

  • #5 的name_index指向 #18

    • #18:承载的是ClassTest类的二进制形式的完全限定名

  • #6 的name_index指向 #19

    • #19:承载的是Object类的二进制形式的完全限定名,每个类都默认继承Object,不会忘了吧?

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

import java.util.Date;
public class ClassTest {
    private Date date; //只创建了引用,没有实列化的不会加载到常量池中
}

总结一波

  1. 对于某个类或接口而言,其自身、父类和继承或实现的接口的信息会被直接组装成CONSTANT_Class_info常量池项放置到常量池中

  2. 类中或接口中使用到了其他的类,只有在类中实际使用到了该类时,该类的信息才会在常量池中有对应的CONSTANT_Class_info常量池项

  3. 类中或接口中仅仅定义某种类型的变量,JDK只会将变量的类型描述信息以UTF-8字符串组成CONSTANT_Utf8_info常量池项放置到常量池中,上面在类中的private Date date;JDK编译器只会将表示date的数据类型的“Ljava/util/Date”字符串放置到常量池中,但不会有实列的二进制形式的完全限定名加载到常量池中

哪些字面量会进入常量池

  1. 【final修饰】的8种基本类型的值会进入常量池

  2. 【非final类型】(包括static的)的8种基本类型的值,只有【double、float、long】的值会进入常量池

  3. 常量池中包含的字符串类型字面量(【双引号引起来的字符串值】)

Class中的符号引用和直接引用

符号引用

  • 符号引用以一组符号来定位所引用的目标,符号可以是任何形式的字面量

  • 比如上面我们说到的String,Class类型等

  • 符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中

    • 在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替

    • 比如我们的Test.class,引用了Data类,但是在编译的时候并不知道Data的实际内存地址

    • 因此只能使用符号引用的方式定位到Data类的地址所在

直接引用

  • 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)

  • 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

  • 一个能间接定位到目标的句柄

  • 直接引用是和虚拟机的布局相关的 ,如果有了直接引用,那引用的目标必定已经被加载入内存中了,直接定位内存地址

引用替换的时机

符号引用替换为直接引用的操作发生在类加载过程(加载 -> 连接(验证、准备、解析) -> 初始化)中的解析阶段

将符号引用转换(替换)为对应的直接引用,放入运行时常量池中

Class中的特殊字符串

特殊字符串包括三种:

  • 类的全限定名

  • 字段和方法的描述符

  • 特殊方法的方法名

类的全限定名

  • 源文件中一个类的名字, 在class文件中是用全限定名表述的

  • Object类,在源文件中的全限定名是 java.lang.Object 。而class文件中的全限定名是将点号替换成“/” , 也就是 java/lang/Object

字段和方法的描述符

对于字段的数据类型,其描述符主要有以下几种:

  • 基本数据类型:(byte、char、double、float、int、long、short、boolean):

    • 除 long 和boolean,其他基本数据类型的描述符用对应单词的大写首字母表示。

    • long 用 J 表示,boolean 用 Z 表示

  • void:描述符是 V

  • 对象类型: 描述符用字符 L 加上对象的全限定名表示,如 String 类型的描述符为Ljava/lang/String

  • 数组类型:每增加一个维度则在对应的字段描述符前增加一个 [ ,如一维数组 int[] 的描述符为 [I ,二维数组 String 的描述符为 [[Ljava/lang/String

对于字段的描述符:

  • int i 中, 字段i的描述符就是 I
    Object o中, 字段o的描述符就是 Ljava/lang/Object;
    double[][] d中, 字段d的描述符就是 [[D

对于方法的描述符:

  • 方法的描述符比较复杂, 包括所有参数的类型列表和方法返回值。 它的格式是这样的

    • (参数1类型 参数2类型 参数3类型 ...)返回值类型

  • 不管是参数的类型还是返回值类型, 都是使用对应字符和对应字符串来表示的, 并且参数列表使用小括号括起来, 并且各个参数类型之间没有空格, 参数列表和返回值类型之间也没有空格

  • 为了方便理解,下面我们来看看

方法描述符 方法声明
()I int getSize()
()Ljava/lang/String; String toString()
([Ljava/lang/String;)V void main(String[] args)
()V void wait()
(JI)V void wait(long timeout, int nanos)
(ZILjava/lang/String;II)Z boolean regionMatches(boolean ignoreCase, int toOffset, String other, int ooffset, int len)
([BII)I int read(byte[] b, int off, int len )
()[[Ljava/lang/Object; Object getObjectArray()

特殊方法的方法名

  • 首先要明确一下, 这里的特殊方法是指的类的构造方法和类型初始化方法

  • 构造方法就不用多说了, 至于类型的初始化方法, 对应到源码中就是静态初始化块

  • 静态初始化块, 在class文件中是以一个方法表述的, 这个方法同样有方法描述符和方法名,具体如下

    • 类的构造方法的方法名使用字符串 表示

    • 静态初始化方法的方法名使用字符串 表示

    • 除了这两种特殊的方法外, 其他普通方法的方法名, 和源文件中的方法名相同

Javap命令

在上面的学习中,我们频繁的使用到了javap命令来查看class文件的常量池

现在我们来对他进行一个比较全面的掌握

  • javap是jdk自带的反解析工具

  • 它的作用就是根据class字节码文件,

    • 反解析出当前类对应的code区(汇编指令)、

    • 本地变量表、

    • 异常表、

    • 代码行偏移量映射表、

    • 常量池等等信息

语法格式:javap <options> <classes>

帮助命令查看所有options:javap -help

-version 版本信息,其实是当前javap所在jdk的版本信息,不是class在哪个jdk下生成的。
-v -verbose 输出附加信息(包括行号、本地变量表,反汇编等详细信息)
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类 和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反编译生成汇编代码
-s 输出内部类型签名
-sysinfo 显示正在处理的类的系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示静态最终常量
-classpath <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置

我们一般常用的就是:-v -l -c

 

posted @ 2019-01-09 14:32  鞋破露脚尖儿  阅读(434)  评论(0编辑  收藏  举报