JVM Class 文件结构

1、基本概念

1、JIT

一般情况下,jvm 以解释的方式来执行 class 文件中的字节码,但当一段代码被频繁调用时,更好的做法是将其对应的字节码再次编译生成平台对应的机器码,这样就不需要每次都解释了,此之谓 JIT。

虚拟机执行代码的两种方式:

  • 加载并执行 java 程序对应的 jvm 指令字节码;
  • 加载 jvm 指令字节码,并将其翻译成宿主机 CPU 的本地指令集,然后执行,即 JIT;

2、字节码是平台无关的,虚拟机是平台相关的

虚拟机有不同的版本,也就是在安装 jdk 或 jre 时,linux 平台要安装 linux 版本,Windows 要安装 windows 版本,不能乱装。
在这里插入图片描述
Java 代码会被编译成 .class 文件,这种字节码文件是平台无关的,也就是说,可以在任意平台的 JVM 上执行,这就是所谓的 一次编译,到处运行

3、语言与虚拟机

Java 虚拟机不与任何语言绑定,它只关注于 Class 文件,Class 文件中包含了虚拟机的指令集,符号表,以及若干其他辅助信息。

Java 语言中的各种变量、关键字和运算符号的语义最终都可以表示成多条字节码命令的组合,因此字节码命令所能提供的语义描述能力会比 Java 语言本身更加强大。

语言的能力与表现力是不同的概念,汇编语言尽管不够灵活,不适于快速开发,但它无所不能。

4、ELF

ELF 是 Linux 平台中目标文件可执行文件的格式,目标文件中的内容包括编译后的机器指令代码、数据,还包括链接时所需要的一些信息,如符号表、调试信息、字符串等。

一般目标文件将信息按类型,以 Section 的形式存储,有时候也叫 Segment。编译后的机器指令放在代码段,全局变量和局部静态变量数据放在数据段

class 文件与 elf 文件有相似之处,也有很多不同,可以对比理解。

5、JVM 指令操作码的长度只有一个字节:所以最大指令数目为 256!!!

2、Class 文件结构

Class 文件是一组以 8 位字节为基础单位的二进制流,也就是字节码流,各个数据项目严格按照顺序紧凑地排列在 class 文件中。

Class 文件的结构本质上是一种数据模型,存哪些信息,怎么存,存在哪里。是典型的系统数据或者底层数据建模的模式,跟网络协议中的数据建模方式很像,这类模型文件的特点是紧凑。

平时做业务层数据建模的时候,可以通过类,比如 UML,或者通过 XML Schema,JSON schema,Yaml schema,或 SQL Schema 的形式来建模,模型文件要简洁、易读,易修改。

在这里插入图片描述
class 文件结构的定义为给谁用的呢?编译器和加载器。

编译器会根据 class 文件的结构定义将 java 文件转换为 class 文件,而类加载器会根据 class 文件的结构定义读取的 class 文件字节流来创建 Class 实例。

所以,只要编译器与加载器它们俩对 class 文件结构有一致统一的认知,就可以了。而不像在业务建模的时候,你设计出来的业务模型,客户、产品经理、研发经理,甚至程序员,都要看懂,所以对易读性的要求很高,常常需要配合使用图表、图形,比如类图,流程图,交互图等等。

class 文件结构没这么多讲究。

下面是某个 class 文件的字节码信息,开头四个字节 CA FE BA BE 是魔数 ( 上图中的 u4 magic ),所有的 class 文件都以此开头,后面的四个字节 00 00 00 33 是版本( 上图中的 u2 minor_version 和 u2 major_version ), 后面的两个字节 00 17 是常量池的大小( 上图中的 u2 constant_pool_count ),对应十进制数为 23。后面的字节,可以按照上图中的 Class 结构文件,依次类推。
在这里插入图片描述
也可以使用 jclasslib.exe 来查看 class 文件内容,或者使用 javap 命令。
在这里插入图片描述
access flags 值得详细了解一下:
在这里插入图片描述

  • final: 不可被继承
  • super:使用 invokespecial 调用父类方法时,特殊对待
  • interface:接口类
  • abstract:不可实例化
  • synthetic:该类由编译器生成,没有对应的 java 文件
  • annotation:注解类
  • enum:枚举类

常量池

Class 文件中的常量池可以理解为资源仓库,主要存放两大类信息:字面量和符号表。

Java 代码在编译的时候,不像 c 和 c++ 那样有链接的过程,Java 实在虚拟机加载 Class 文件的时候进行动态链接。因此,Class 文件中不会保存方法、字段的内存布局信息。在类加载时,虚拟机从 class 文件的常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

public class Person implements Action {
    public String name;
    public int age;
    public String gender;

    {
        name = "tom";
        age = 18;
    }

    public Person() {
    }

    public Person(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public String say() {
        return String.format("name = %s, age = %s", name, age);
    }

    public void walk() {
        System.out.print("walk");
    }
}

该 java 文件对应的 class 文件的常量池大小为 62,CONSTANT_Class_info 的数据为 6, CONSTANT_Fieldref_info 的数目为 4。

CONSTANT_Class_info 对应的具体类信息:

  • java/lang/Object
  • chorus/Person
  • chorus/Action
  • java/lang/Integer
  • java/lang/String
  • java/lang/System
  • java/io/PrintStream

CONSTANT_Fieldref_info 对应的具体字段信息:

字段表

在这里插入图片描述
1、字段包括类变量及实例变量,不包括在方法内部声明的局部变量

2、字段表中不会列出从超类或者父接口中继承而来的字段

但是有可能列出原 Java 代码之中不存在的字段,也就是 SYNTHETIC 字段,比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

方法表

在这里插入图片描述

1、方法的代码存放在方法属性表中一个名为 Code 的属性里面

在这里插入图片描述
2、<clinit><init>

如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。

有时,会出现由编译器自动添加的方法,最典型的便是类构造器方法 <clinit> 和示例构造器方法 <init>

<clinit> 可以理解为类中的 static{} 块,但并不是所有的 static {} 块都会生成对应的 <clinit> 方法,编译器会做一定程度的优化。

属性表

Java SE 8 的预定义属性为 23 项:

在这里插入图片描述

1、Code 属性

Code 属性中存储 Java 方法编译生成的字节码指令信息;

接口或者抽象类中的方法不存在 Code 属性。

Code 属性是 Class 文件中最重要的一个属性,Java 程序中的信息可以分代码和元数据两部分:

  • 代码: Code 属性
  • 元数据: 类、字段、方法定义以及其他信息;

能够直接阅读字节码是工作中分析 Java 代码语义问题的基本技能。

2、InnerClass 属性

InnerClass 属性用于记录内部类和宿主类之间的关联,如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成 InnerClasses 属性。

3、EnclosingMethod 属性

当且仅当一个类为局部类或者匿名类时,该类才能拥有此属性,这个属性用于标识这个类所在的外围方法。

例如以下这段代码: org.postgresql.Driver在这里插入图片描述
编译器会生成一个匿名类: org.postgresql.Driver$1
在这里插入图片描述

3、字节码

1、操作数栈

Java 虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。汇编语言的指令集就是面向寄存器架构的,有很多寄存器相关的指令。

寄存器是 CPU 的重要组成部分,分为指令寄存器(IR),程序计数器(PC),其中指令寄存器又分为:数据寄存器,地址寄存器,段寄存器等等。

2、操作码数目

受限于 Java 虚拟机操作码的长度设定,Java 指令集的操作码总数不超过 256 个。

3、局部变量表和操作数栈

java 虚拟机中的局部变量表和操作数栈,可以理解为汇编语言中的函数栈与寄存器。

4、指令分类

  • 加载与存储指令:
  • 运算指令:
  • 类型转换指令:
  • 对象创建与访问指令:
  • 操作数栈管理指令:
  • 控制转移指令:
  • 方法调用与返回指令:
  • 异常处理指令
  • 同步指令

5、栈帧:Stack Frame

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。在这里插入图片描述
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表中的 Code 属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量:
在这里插入图片描述

6、字节码指令中有特定含义的字符:

  • i 代表 int:例如 iconst, iload
  • l 代表 long:
  • s 代表 short
  • b 代表 byte: 例如 bipush, sipush
  • c 代表 char
  • f 代表 float
  • d 代表 double
  • a 代表 reference
  • ia 代表 int 型数组
  • fa 代表 float 数组
  • da 代表 double 数组
  • aa 代表数组的数组

7、iload_<n>

这种形式的指令,比如 iload_0, iload_1, iload_2,代表某个带有一个操作数的通用指令的特殊形式,对于这若干特殊指令来说,它们省略掉了显示的操作数。

8、ifeq, ifne, iflt, ifge, ifgt, ifle

  • if : 表示该指令为条件判断并跳转的指令,
  • eq: equal 等于
  • lt: less than 小于
  • gt: great than 大于
  • ge: 大于等于
  • le:小于等于
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值