JVM中篇( 一、Class文件结构)

JVM_(中篇)Class文件结构

提示: 本材料只做个人学习参考,不作为系统的学习流程,请注意识别!!!



一. Class文件结构

1. 概述

1.1 字节码文件的跨平台性

  1. java语言:跨平台的语言,一次编译,到处运行
  2. java虚拟机
    ①跨语言的平台,java虚拟机不和包括java在内的任何语言绑定,只与特定的二进制文件格式所关联
    ②所有的jvm全部遵守Java虚拟机规范
  3. Java源码必须要被编译为符合JVM规范的字节码
    ①javac是一种能将java源码编译为字节码的前端编译器
    ②javac编译四个步骤:词法解析-语法解析-语义解析-生成字节码
  4. Oracle 的 JDK包含两部分
    ①将java源码编译为java虚拟机指令集的编译器
    ②实现java虚拟机的运行时环境

1.2 Java的前端编译器

  1. javac编译器(全量编译器),Idea默认编译器
  2. Eclipse 的ECJ编译器(增量式编译器)

:前端编译器不会直接涉及编译优化方面的技术,而是将这些具体的优化细节移交给了JVM的JIT编译器负责

1.3 透过字节码指令看代码细节

示例一

package com.yuan;

/**
 * @description: 字节码查看
 * @author: ybl
 * @create: 2021-02-08 18:16
 **/
public class IntegerTest {
    public static void main(String[] args) {
        Integer x = 5;
        int y = 5;
        System.out.println(x == y);//true

        Integer i1 = 10;
        Integer i2 = 10;
        System.out.println(i1 == i2);//true

        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4);//false
    }
}

在这里插入图片描述
示例二

package com.yuan;

/**
 * @description: 字节码查看继承示例
 * @author: ybl
 * @create: 2021-02-08 18:44
 * 成员变量(非静态)赋值过程:①默认初始化-②显示初始化/代码块中初始化-③构造器中初始化-④对象.属性或对象.方法赋值
 **/
class Father {
    int x = 10;
    public Father() {
        this.print();
        x = 20;
    }
    public void print() {
        System.out.println("Father,x = " + x);
    }
}

class Son extends Father {
    int x = 30;
    public Son() {
        this.print();
        x = 40;
    }
    public void print() {
        System.out.println("Son,x = " + x);
    }
}

public class SonTest {
    public static void main(String[] args) {
        Father f = new Son();
        System.out.println(f.x);
        //Son,x = 0
		//Son,x = 30
		//20
    }
}

在这里插入图片描述

2. 虚拟机的基石(Class文件)

字节码文件里面是什么?

  1. 字节码文件是一种二进制的类文件,内容是JVM指令,而不像C和C++直接编译成机器码

什么是字节码指令(byte code)?

  1. Java虚拟机指令是由一个字节长度的、代表某种特定操作含义的==操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)==构成,虚拟机中的指令可以不包含操作数

解读Class文件的 三种方式

  1. 查看二进制文件,使用Notepad++(需要安装插件),或者Binary Viewer
  2. 使用javap指令:jdk自带的反解析工具
  3. 使用idea插件:jclasslib或jclasslib bytecode viewer客户端工具(可视化效果好)

3. Class文件结构

Class文件的本质:一组以8位字节为基础单位的二进制流
Class文件格式:

  1. 由于class文件没有分隔符,因此字节顺序、数量、以及每个字节代表的含义、长度等都是不允许改变的
  2. class文件采用类似C语言的结构体进行数据存储:无符号数和表
    ①无符号数:属于基本数据类型,以u1、u2、u4、u8表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值
    ②表:由多个无符号数或者其他表作为数据项构成的有层次关系的复合数据类型,表习惯以"_info"结尾;整个Class文件本质上就是一张表,由于表没有固定长度,所以通常会在其前面加个数说明

Class文件结构概述
Class文件结构并不是一层不变的,但 基本结构如下:魔数、Class文件版本号、常量池(存放所有常量)、访问标识、类索引、父类索引、接口索引集合、字段表集合、方法表集合、属性表集合(细分如下图)
在这里插入图片描述

3.1 魔数(Magic Number):Class文件的标志

  1. 每个class文件开头的4个字节的无符号整数称为魔数,魔数是Class文件的标识符
  2. 固定以0xCAFEBABE开头
  3. 使用魔数而非扩展名来进行标识,主要数基于安全方面,因为扩展名可以随意改动

3.2 Class文件的版本号

  1. 魔数后面的4个字节存储的是Class文件的版本号,第5和第6个字节代表的是编译的副版本号minor_version,例如为m;第7和第8个字节是编译的主版本号major_version,例如为m。那么这个Class文件的格式版本号就是M.m
    在这里插入图片描述
  2. 不同版本的Java编译器编译的Class文件对应的版本不一样,目前高版本Java虚拟机可以执行由低版本编译生成的Class文件,反过来不可以,否则抛UnsupportedClassVersionError异常(向下兼容)

3.3 常量池(存放所有常量)

概述

  1. 常量池是Class文件内容最为丰富的区域,对Class文件中的字段和方法解析有着至关重要的作用
  2. 常量池表项中,存放的是编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入运行时常量池中存放
  3. 版本号后面紧跟着常量池的容量计数器(constant_pool_count),以及若干个常量池表项(constant_pool),这一系列连续常量池数据称为常量池集合

常量池计数器(constant_pool_count)

  1. 由于常量池的数量是不固定的,所以在常量池的路口需要放置一项u2类型的无符号数,代表常量池容量计数值
  2. 注意:这个容量计数器是从1开始的(constant_pool_count=1表示0个常量项)
  3. 常量池计数器把第0项常量空出来,是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的含义,这种情况可以用索引值0表示

常量池表(constant_pool)

  1. 常量池中包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。其中第一个字节作为类型标记,用来确定该项的格式,这个字节称为tag byte(标记字节)
    在这里插入图片描述
  2. 常量池主要存放的常量:字面量(Literal)和符号引用(Symbolic Reference)
    字面量:文本字符串、声明为final的常量值
    符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符

    全限定名:将包名的’’.’‘换成’’/’’,使用时最后一般加上’’;’’,例如:com/jd/test/Demo;
    简单名称:指没有类型和参数修饰的方法或字段名称,例如add()方法中的add;
    描述符:描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值,详见如下:
    在这里插入图片描述
    用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号之内。如方法java.lang.String toString()的描述为:() Ljava/lang/String;,方法int abc(int[] x,int y)的描述为:([II) I

☆☆☆☆☆补充说明

  1. 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到了内存中
  2. 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,说明引用的目标已经存在于内存之中了
  3. 虚拟机在加载Class文件时才会进行动态链接,Class文件中不会保存各个方法和字符的最终内存布局信息,因此这些方法和字段的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池获取对应的符号引用,再在类加载过程的解析阶段(链接阶段)将其替换为直接引用,并翻译到具体的内存地址中

3.4 访问标识(access_flag)

  1. 常量池后面紧跟着就是访问标识,两个字节表示,用于识别一些类或接口层次的访问信息,详细如下:
    在这里插入图片描述
  2. 类的访问权限都是以ACC_开头得常量
  3. 补充说明:
    ①带有ACC_INTERFACE标志的Class文件表示的是接口而不是类,反之则表示的类而不是接口,如果一个Class文件设置了ACC_INTERFACE,那么同时也设置了ACC_ABSTRACT标志,并且不能设置为ACC_FINAL、ACC_SUPER或ACC_ENUM标志
    ②JDK8以后,java虚拟机都认为每个Class文件均设置了ACC_SUPER标志
    ③ACC_SYNTHETIC标志意味着该类或接口由编译器生成,而不是源代码生成
    ④注解类必须设置ACC_ANNOTATION标志,同时也必须设置ACC_INTERFACE标志
    ⑤ACC_ENUM标志表示该类或其父类为枚举类

3.5 类索引、父类索引、接口索引集合

  1. 访问标记后,紧接着指定类的类别、父类类别以及实现的接口,格式如下:
    在这里插入图片描述
  2. this_class(类索引)
    2字节无符号整数,指向常量池的索引,提供类的全限定名,如:com/jd/test/Demo。this_class的值必须是对常量池中某项的一个有效索引值。常量池在这个索引处的成员必须是CONSTANT_Class_info类型结构体,该结构体表示这个Class文件所定义的类或接口
  3. super_class(父类索引)
    2字节无符号整数,指向常量池的索引,提供父类的全限定名,如果没有继承关系,默认是java/lang/Object类,因为java是单继承,所以父类只有一个
  4. interface
    ①指向常量池的索引集合,它提供了一个符号引用到所有已实现的接口,因为java是多实现,所以用数组形式保存多个接口的索引,每个接口对应的索引是一个指向常量池的CONSTANT_Class_info(这里必须是接口)
    ②interface_count(接口计数器):表示当前类或接口的直接超接口数量
    ③interface [] (接口索引集合):数组接口索引的顺序应该与源代码中实现的接口顺序(从左到右)一致

3.6 字段表集合

field

  1. 用于描述接口或类中声明的变量,包含类级变量以及实例级变量,但是不包括局部变量
  2. 字段叫什么名字、被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
  3. 指向常量池索引集合,描述了每个字段的完整信息,比如字段的标识符、访问修饰符、是类变量还是实例变量、是否是常量等

注意

  1. 字段表集合中不会列出从父类或者实现的接口继承而来的字段,但是可能列出原本java代码中不存在的字段,如:在内部类中为了保持对外部类的访问,会自动添加指向外部类实例的字段
  2. Java语言中,字段是无法重载的,都必须使用不一样的名称;但是对于字节码文件,如果两个字段的描述符不一致,那字段重名就是合法的

fields_count(字段计数器):

  1. fields_count的值表示当前Class文件fields表的成员个数,使用两个字节表示

fields [] (字段表):

  1. fields表中每个成员都是一个fields_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或者父接口继承的那些字段
  2. 一个字段的信息包括如下:这些信息中,各个修饰符都是布尔类型,要么有,要么没有
    作用域(各类修饰符)、是实例变量还是类变量(static修饰)、可变性(final修饰)、并发可见性(volatile修饰)、可否序列化(transient修饰)、字段数据类型(基本数据类型,对象,数组)、字段名称
  3. 字段表结构如下:
    在这里插入图片描述

3.7 方法表集合

methods: 指向常量池索引集合,描述了每个方法的签名

  1. 字节码文件中,每个method_info项都对应着一个类或接口中的方法信息,比如方法的修饰符,方法的返回值类型以及方法的参数信息等
  2. 如果这个方法不是抽象的或者不是native的,那么字节码中会体现出来
  3. methods表中不包括从父类或者父接口继承的方法,但是可能会出现由编译器自动添加的方法,典型的是编译器产生的类(接口)初始化方法< clinit >()和实例初始化方法< init >()

注意:

  1. java语言中,无法仅仅依靠返回值进行方法的重载,但是Class文件格式中,特征签名的范围更大,也就是说,如果两个方法有相同的名称和特征签名,但是返回值不同,那么也是可以合法共存在同一个class文件中

methods_count (方法计数器):

  1. methods_count表示当前Class文件中methods表的成员个数,两个字节表示

methods[] (方法表):

  1. methods表中,每个成员都是一个methods_info结构,用于描述类或接口中方法的完整信息,如果某个method_info结构的access_flags既没有设置ACC_NATIVE标志,也没有设置ACC_ABSTRACT
    标志,那么该结构中也应该包含实现这个方法所用的Java虚拟机指令
  2. method_info结构可以表示类或接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口的初始化方法
  3. 方法表的结构如下:
    在这里插入图片描述

3.8 属性表集合

attributes(属性表集合):

  1. 方法表集合之后就是属性表集合,指的是Class文件所携带的辅助信息
  2. 此外,字段表和方法表都有自己的属性表,用于描述某些场景特有的信息
  3. 属性表集合不要求各个属性表有严格的顺序,只要不与已有的属性名重复即可

attributes_count(属性计数器):

  1. attributes_count的值表示当前Class文件属性表成员的个数

attributes [] (属性表):

  1. 属性表每一项都是attributes_info结构
  2. 属性的通用格式如下:在这里插入图片描述
  3. 属性的类型:
    Java8还定义了23中属性,下面介绍部分常见的属性:
    ①ConstantValue:表示一个常量字段的值,位于field_info结构的属性表中
    ②Deprecated:jdk1.1为了支持注解中的@deprecated而引入
    ③Code:存放方法体中的代码,接口或者抽象方法,没有具体的方法体,也没有Code属性,Code结构如下:
    在这里插入图片描述
    ④InnerClass:jdk1.1为了支持内部类和内部接口引入,位于ClassFile结构的属性表
    ⑤LineNumberTable:可选变长属性,位于Code结构的属性表,描述Java源码行号和字节码行号之间的对应关系。start_pc:字节码行号,line_number:java源代码行号
    ⑥LocalVariableTable:可选变长属性,位于Code结构的属性表,它被调试器用于确定方法在执行过程中局部变量的信息,Code属性中每个局部变量最多只能有一个LocalVariableTable属性
    start_pc+length:表示这个变量在字节码中的生命周期起始和结束的偏移位置
    index:这个变量在局部变量表中的槽位(槽位可复用)
    name:变量名称
    Descriptor:局部变量类型描述
    ⑦Signature:记录泛型签名信息
    ⑧SourceFile:固定长度为8字节,如下:在这里插入图片描述

4. 使用javap指令解析Class文件

  1. 前端编译:javac xx.java,不会生成对应的局部变量表信息,javac -g xx.java就可以生成所有相关的信息(idea默认采用)

javap语法格式: javap < options > < classes >,其中classes 就是需要反编译的class文件,option选项如下:

  1. -help 输入javap用法信息
  2. -version 版本信息,指的是当前javap所在jdk的版本信息,不是编译的jdk版本
  3. -public 仅显示公共类和成员
  4. -protected 显示受保护的/公共类和成员
  5. -p (-private) 显示所有类和成员
  6. -package 显示程序包/受保护的/公共类和成员
  7. -sysinfo 显示正在处理的类的系统信息
  8. -constants 显示静态最终变量(赋值之后的)
  9. -s 输出内部类型签名
  10. -l 输出行号和本地变量表
  11. -c 对代码进行反汇编
  12. -v (-verbose) 输出附加信息(包括行号、本地变量表、反汇编等详细信息),可与-p配合使用,展示所有信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值