Java-第十四部分-JVM-Class文件结构和解析JV

JVM全文

class文件概述

image.png

  • java语言,跨平台的语言,write once, run anywhere
  • java虚拟机,跨语言的平台,只与class文件这种特点的二进制文件格式所关联,无论使用何种语言,只要能将源文件编译为正确的class文件,就可以在java虚拟机上执行

image.png

  • 前端编译器,负责将符合java语法规范java代码转换成符合jvm规范的字节码文件
  1. javac,默认提供的前端编译器
  2. 词法解析、语法解析、语义解析、生成字节码
  3. 全量编译,将所有内容,都进行重新编译
  • 任何一个class文件都对应着唯一的类或接口的信息,但是class文件,不一定以磁盘文件的形式存在,class文件是一组以字节(8位)为单位的二进制流

字节顺序和数量,都被严格限定,哪个字节代表什么含义,长度多少,先后顺序,都不能改变

  • clas文件采用类似C语言结构体方式进行数据存储,只有两种数据类型,无符号数和表
  1. 无符号数属于基本数据类型,描述数字、索引引用、数量值或者按照UTF-8编码构成字符串,以u1、u2、u4、u8代表1、2、4、8个字节的无符号数
  2. 表,由多个无符号数或者其他表作为数据构成的复合数据,以_info结尾,用于描述有层次的复合结构的数据,没有固定长度,会在其前面加上个数说明
  3. class文件本质就是一张表
  • 字节码
  1. 源代码经过编译生成一个或多个字节码文件,一个类生成一个
  2. 字节码指令,由一个字节长度的、代表某种特定操作含义的操作码以及跟随其后的零至多个代表此操作所需参数的操作数构成

结构

  • 官网结构
ClassFile {
    u4             magic; //魔数,文件识别
    u2             minor_version; //小版本
    u2             major_version; //主版本
    u2             constant_pool_count; //常量池长度 10实际上是9 首索引没有分配
    cp_info        constant_pool[constant_pool_count-1]; //常量池
    u2             access_flags; //访问标识,类/接口+修饰
    u2             this_class; //类索引名字
    u2             super_class; //父类索引名字
    u2             interfaces_count; //接口索引长度,一个类可以实现多个接口
    u2             interfaces[interfaces_count]; //索引接口数组
    u2             fields_count; //字段数
    field_info     fields[fields_count]; //字段表
    u2             methods_count; //方法数
    method_info    methods[methods_count]; //方法表
    u2             attributes_count; //属性数,类似变量表/异常表里的信息
    attribute_info attributes[attributes_count]; //属性表
}
复制代码
  • 魔数,前四位,每个class文件开头的四个字节都是CA FE BA BE,确定这个文件是否为一个能被虚拟机接受的有效合法的class文件
  • 版本号,56位小版本,七八位大版本,00 00 00 34->52.0 JDK8,向下兼容
  • 访问标识,两个字节,用于识别一些类或者接口层次的访问信息,最终结果是下面的几项的或结果(并集)
  1. ACC_SUPER类默认都有
  2. 没有ACC_INTERFACE 则为类;如果有,同时也要ACC_ABSTRACT
  3. 如果有ACC_ANNOTATION,也要有ACC_INTERFACE
  4. 注解,flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
  5. public class TestDemo4对应字节码中00 21

image.png

  • 索引,包含类索引,父类索引,接口索引

指向常量池的符号引用

image.png

常量池

  • 常量池计数器+常量池表,内容丰富,class文件的资源仓库,保存字段和方法相关信息
  1. 常量池计数器,计数从1开始,实际长度少1,第0项空出来了,满足后面某些指向常量池的索引值的数据在特定情况下,不引用任何一个常量池项目
  2. 常量池表,存放编译期间生成的字面量和符号引用,经过类加载后,进入方法区的运行时常量池;红框jkd7引入,体现对动态语言的支持;存放的是标识tag,表示存放哪种类型
  3. 3456 用final修饰后的变量;byte/short/char/boolean,都以int型来保存,4个字节,32位
  4. 具体格式看官网

image.png

相关概念

  • 虚拟机在class文件时才会进行动态链接,class文件中不会保存各个方法和字段的最终内存布局信息,这些字段和方法的符号引用不经过转换无法直接被虚拟机使用;当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段,将其替换成直接引用,翻译到具体的内存地址中
  1. 符号引用,用符号来描述所引用的目标,可以是任何形式的字面量,无歧义地定位到目标
  2. 直接引用,直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄,与虚拟机实现的内存布局下你管
  • 字面量
  1. 文本字符串
  2. 声明为final的常量值
  • 符号引用
  1. 类和接口的全限定名
  2. 字段的名称和描述符
  3. 方法的名称和描述符
  • 全限定名

全类名com.java.Demo-> 全限定名com/java/Demo;分号结束

  • 简单名称,方法或者字段名称
  • 描述符,描述字段的数据类型,方法的参数列表(数量、类型及顺序)和返回值

image.png

//[Ljava.lang.Object;@12a3a380
Object[] objects = new Object[10];
//[[Ljava.lang.String;@29453f44
String[][] strings = new String[10][10];
//[[D@5cad8086
double[][] doubles = new double[10][10];
复制代码

字段表集合

  • 描述接口或类中声明的变量,包括类级变量和实例变量
  • 字段的名字和数据类型,都引用常量池中的符号引用,不能确定;
  • 指向常量池索引集合,描述每个字段的完整信息,包括字段修饰符、访问修饰符、类变量static/实例变量、常量final
  • 不会包含父类或者实现接口中继承而来的字段,但是会出现原本类中不存在的字段,如内部类保证对外部的访问,会保存外部类指针

image.png

  • java中字段不能重载,不能重名;但是对于字节码来说,描述符不一样,就是合法的
  • 字段计数器,表示当前表中,字段个数
  • 字段表,字段表中每一个都是表结构,保存字段的完整信息

每一个字段的结构

image.png

访问标识

image.png

  1. 字段名索引,访问常量池,找到字段名
  2. 字段描述符索引,字段数据类型
  3. 属性表集合,属性计数器+表集合,初始值、注释信息等,如用final修饰的变量,存成为常量

image.png

  • 属性表结构,属性名/属性长度(常量属性恒为2)/常量值的索引(常量池中)

image.png

方法表集合

  • 指向常量池索引集合,描述每个方法的签名,包括方法修饰符、返回值类型及参数信息
  • 会体现是否是抽象或者native
  • 不包括父类或接口中继承的方法,会自动产生方法,如类初始化<clinit>和实例初始化<init>
  • 方法重载,简单名称一样,但是返回值不包括在特征签名之中,不能靠修改返回值,进行重载

特征签名,一个方法中各个参数在常量池的字段符号引用的集合
但是,在class字节码中,只要描述符不一样,就是合法的,可以共存

  • 方法表计数器,两字节
  • 方法表结构,方法的完整描述

image.png

访问标识,修饰

image.png

  1. 方法名索引,指向常量池
  2. 方法描述符索引
  3. 属性表集合,具体看下面,当前<init>方法包含一个属性code,但是这个code属性下又包含两个属性,行号表和局部变量表

image.png

属性表集合

  • 方法表集合之后的属性集合,指class文件所属的信息,该class文件的源文件名称,注解,用于Java虚拟机的验证和运行,以及调试
  • 字段表、方法表中的属性表,用于描述某些专有信息
  • 属性表通用格式

image.png

  • sourcefile属性表结构
SourceFile_attribute {
    u2 attribute_name_index;
    u4 attribute_length; //始终是2
    u2 sourcefile_index; //原码文件名称索引
}
复制代码

方法中属性表

  • code

image.png

  • 字节码对应关系,每一条字节码指令都对应一个字节

image.png

  • LineNumberTable 行号对应表
LineNumberTable_attribute {
    u2 attribute_name_index; //属性名
    u4 attribute_length; //长度
    u2 line_number_table_length; //行号表长度
    {   u2 start_pc;
        u2 line_number;	
    } line_number_table[line_number_table_length];
}
复制代码
  • local_variableTable 局部变量表
LocalVariableTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_table_length;
    {   u2 start_pc;
        u2 length; //作用域长度
        u2 name_index; //变量名索引
        u2 descriptor_index; //描述符索引
        u2 index; //槽位起始序号
    } local_variable_table[local_variable_table_length];
}
复制代码

image.png

终极解析

  • 原码
package com.java;
public class TestDemo4 {
    public int num = 1;
    public int add() {
        num = num + 2;
        return num;
    }
}
复制代码

image.png

案例

基础案例

  • 源码
public class TestDemo4 {
    public int num = 1;
    public int add() {
        num = num + 2;
        return num;
    }
}
复制代码
  • 字节码反编译后用idea打开,多出了默认构造方法、调用用变量归属和全类名
public class TestDemo4 {
    public int num = 1;
    public TestDemo4() {
    }
    public int add() {
        this.num += 2;
        return this.num;
    }
}
复制代码

Integer案例

  • 源码
Integer x = 5;
int y = 5;
System.out.println(x == y); //true

//是在`IntegerCache`数组现取的
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2); //true

//是新new的对象
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); //false
复制代码
  • Integer底层,如果是在low(-128)high(127)之间,直接返回IntegerCache数组中的值,否则创建新的对象
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
复制代码
  • 字节码
//Integer x = 5;
0 iconst_5 //将5加载进操作数栈
//调用Integer对象的valueOf方法
1 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
4 astore_1 //存储引用类型
//int y = 5;
5 iconst_5 
6 istore_2 //存储int类型
7 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
//x == y
10 aload_1 //取出引用类型
//调用Integer对象的intValue方法,进行拆箱
11 invokevirtual #4 <java/lang/Integer.intValue : ()I>
14 iload_2
15 if_icmpne 22 (+7) //如果相等跳到22行
复制代码

多态案例

public class TestDemo3 {
    public static void main(String[] args) {
        Father father = new Son();
        System.out.println(father.x);
    }
}
class Father {
    int x = 10;
    public Father() {
        this.print();
        this.x = 20;
    }
    public void print() {
        System.out.println("father" + x);
    }
}

class Son extends Father {
    int x = 30;
    public Son() {
        this.print();
        this.x = 40;
    }
    @Override
    public void print() {
        System.out.println("Son" + x);
    }
}
复制代码
  • 输出
  1. 第一个为什么为0,在对son进行初始化的时候,先初始化父类,并调用父类中的print()方法,但是son重写了print(),就会调用son的print方法,但是由于此时son类中的x初始化为零值,那么就输出为0
  2. 父类初始化整体都在子类初始化前,而在调用print方法时,直接调用的是子类重写的方法,而此时子类的x仅仅是分配空间,有了零值
Son0 
Son30
20 //成员变量没有重写
复制代码