Java 入门指南:JVM(Java虚拟机)—— Java 类文件结构

文章目录

    • 字节码
    • [Class 文件结构](#Class 文件结构)
      • [魔数(Magic Number)](#魔数(Magic Number))
      • [Class 文件版本号(Minor&Major Version)](#Class 文件版本号(Minor&Major Version))
      • [常量池(Constant Pool)](#常量池(Constant Pool))
      • [访问标志(Access Flags)](#访问标志(Access Flags))
      • [前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合](#前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合)
      • 字段表集合(Fields)
      • 方法表集合(Methods)
      • 属性表集合(Attributes)
    • [索引值定位 class 文件中的位置](#索引值定位 class 文件中的位置)

字节码

字节码是一种中间表示形式,它是由Java编译器将Java源代码编译得到的。字节码文件通常以 .class 为扩展名,每个Java类都会生成一个对应的字节码文件。字节码是一种低级的指令集合,它由一系列的指令组成,这些指令是为 JVM 设计的,而不是为特定的硬件平台设计的。

JVM 与字节码

在 Java 中,JVM 可以理解的代码是字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

Groovy、Scala、JRuby、Kotlin 等语言都是运行在 Java 虚拟机之上。不同的语言被不同的编译器编译成 .class 文件最终运行在 Java 虚拟机之上。.class 文件的二进制格式可以使用 WinHexopen in new window 查看。

字节码的生成过程

当Java源代码被编译时,编译器会生成字节码文件。这个过程可以分为以下几个步骤:

  1. 词法分析:将源代码分解成一个个有意义的记号(Token)。
  2. 语法分析:将记号组合成符合 Java 语言语法的抽象语法树(AST)。
  3. 语义分析:检查语法树中的语义错误,如类型不匹配等。
  4. 代码生成:将语法树转换为字节码指令序列。

Class 文件结构

.class 文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。根据 Java 虚拟机规范,Class 文件通过 ClassFile 定义,有点类似 C 语言的结构体。

ClassFile 的结构如下:

java 复制代码
ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version; //Class 的小版本号
    u2             major_version; //Class 的大版本号
    u2             constant_pool_count; //常量池的数量
    cp_info        constant_pool[constant_pool_count-1]; //常量池
    u2             access_flags; //Class 的访问标记
    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]; //属性表集合
}

类文件的内容通常可以分为以下的部分:

通过 IDEA 插件 jclasslib 不光可以直观地查看某个类对应的字节码文件,还可以查看类的基本信息、常量池、接口、属性、函数等信息。

魔数(Magic Number)

Java 复制代码
u4             magic; // Class 文件的标志

每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件 。Java 规范规定魔数为固定值:0xCAFEBABE

JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果读取的文件不是以这个魔数开头,Java 虚拟机将拒绝加载它,并会抛出 ClassFormatError

Class 文件版本号(Minor&Major Version)

java 复制代码
u2             minor_version; // Class 的小版本号
u2             major_version; // Class 的大版本号

紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本号 ,第 7 和第 8 个字节是主版本号

每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 javap -v 命令来快速查看 Class 文件的版本号信息。

高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。

常量池(Constant Pool)

java 复制代码
u2             constant_pool_count; // 常量池的数量
cp_info        constant_pool[constant_pool_count-1]; // 常量池

紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表"不引用任何一个常量池项")。

常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池中每一项常量都是一个表,这 14 种表有一个共同的特点:

  • 开始的第一位是一个 u1 类型的标志位
  • -tag 来标识常量的类型,代表当前这个常量属于哪种常量类型。
类型 标志 (tag) 描述
CONSTANT_utf8_info 1 UTF-8 编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_FieldRef_info 9 字段的符号引用
CONSTANT_MethodRef_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodRef_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodType_info 16 标志方法类型
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点
.class 文件可以通过 javap -v class类名 指令来看一下其常量池中的信息(javap -v class类名-> temp.txt:将结果输出到 temp.txt 文件)。

访问标志(Access Flags)

java 复制代码
u2             access_flags; // Class 的访问标记

在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public 或者 abstract 类型,如果是类的话是否声明为 final 等等。

总共有 16 个标记位可供使用,但常用的只有其中 7 个:

标识符 标记位 描述
ACC_PUBLIC 0x0001 声明为公共的;可以从其包之外访问
ACC_FINAL 0x0010 声明为最终的;不允许有子类
ACC_SUPER 0x0020 在通过 invokespecial 指令调用时,特别处理超类方法
ACC_INTERFACE 0x0200 是一个接口,不是一个类
ACC_ABSTRACT 0x0400 声明为抽象的;不能实例化
ACC_SYNTHETIC 0x1000 声明为合成的;不在源代码中存在
ACC_ANNOTATION 0x2000 声明为注解类型
ACC_ENUM 0x4000 声明为枚举类型
ACC_MODULE 0x8000 声明为模块类型

通过 javap -v class类名 指令可以查看指定类的访问标志。

前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合

java 复制代码
u2             this_class; // 当前类
u2             super_class; // 父类
u2             interfaces_count; // 接口数量
u2             interfaces[interfaces_count]; // 一个类可以实现多个接口

Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引、父类索引和接口索引集合按照顺序排在访问标志之后,

类索引(this_class)用于确定这个类的全限定名(当前类的索引)。

父类索引(super_class)用于确定这个类的父类的全限定名(父类的索引)。

由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。

接口索引 集合用来描述这个类实现了哪些接口,这些被实现的接口将按 implements (如果这个类本身是接口的话则是 extends) 后的接口顺序从左到右排列在接口索引集合中。

字段表集合(Fields)

java 复制代码
u2             fields_count; // 字段数量
field_info     fields[fields_count]; // 一个类会可以有个字段

字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。

field info(字段表) 的结构:

java 复制代码
field_info {
  u2 access_flag;
  u2 name_index;
  u2 description_index;
  u2 attributes_count;
}
  • access_flag:字段的作用域(public ,private,protected 修饰符),是实例变量还是类变量(static 修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。

  • name_index:字段名的索引,指向常量池中的 CONSTANT_Utf8_info, 比如说下面示例中的值就为 name。

  • description_index:字段的描述类型索引,也指向常量池中的 CONSTANT_Utf8_info,针对不同的数据类型,会有不同规则的描述信息。

  • attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;

  • attributes [attributes_count]: 存放具体属性具体内容。

java 复制代码
public class FieldsExample {
    private String name;
}

这段代码的字段只有一个,修饰符为 private,类型为 String,字段名为 name

方法表集合(Methods)

java 复制代码
u2             methods_count; // 方法数量
method_info    methods[methods_count]; // 一个类可以有个多个方法

methods_count 表示方法的数量,而 method_info 表示方法表。

Class 文件存储格式中对方法的描述,与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志名称索引描述符索引属性表集合几项。二者的区别是用来存储方法的信息,包括方法名,方法的参数,方法的签名。

method_info(方法表的) 结构:

标识符 标记位 描述
ACC_PUBLIC 0x0001 声明为 public;可以从其包之外访问。
ACC_PRIVATE 0x0002 声明为 private;只能在其定义类中访问。
ACC_PROTECTED 0x0004 声明为 protected;可以从其包内或子类中访问。
ACC_STATIC 0x0008 声明为 static
ACC_FINAL 0x0010 声明为 final;不允许有子类。
ACC_SYNCHRONIZED 0x0020 声明为 synchronized;调用时被监视器包装。
ACC_BRIDGE 0x0040 一个桥接方法,由编译器生成。
ACC_VARARGS 0x0080 声明为可变参数。
ACC_NATIVE 0x0100 声明为 native;在 Java 之外的其他语言中实现。
ACC_ABSTRACT 0x0400 声明为 abstract;没有提供实现。
ACC_STRICT 0x0800 声明为 strictfp;浮点模式为 FP-strict。
ACC_SYNTHETIC 0x1000 声明为 synthetic;不在源代码中存在。

因为 volatile 修饰符和 transient 修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了 synchronizednativeabstract 等关键字修饰方法,所以也就多了这些关键字对应的标志

属性表集合(Attributes)

java 复制代码
u2             attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合

在 Class 文件中,字段表和方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。

以下是包含属性表在内的完整的表示字段的结构

索引值定位 class 文件中的位置

通过索引值,可以定位到在 class 文件中的位置。

在 Java 类文件中,常量池是一个索引表 ,它从索引值 1 开始计数,每个条目都有一个唯一的索引。

  • 常量池计数器:在常量池之前,类文件有一个 16位 的常量池计数器,表示常量池中有多少项。它的值比实际常量数大1(因为索引从1开始)。

  • 常量池条目:每个常量池条目的开始是一个标签(1个字节),表明了常量的类型(如Class、Fieldref、Methodref等)。根据这个类型,后面跟着的数据结构也不同。

定位过程大致如下:

  • 读取常量池计数器:首先,从类文件的开头读取常量池计数器的值,确定常量池中有多少条目。

  • 遍历常量池:从常量池的第一项开始遍历。由于不同类型的常量长度不同,需要根据每个常量的类型来确定它的长度。

  • 根据索引定位:继续遍历,直到到达所需的索引值。每次遍历时,根据条目类型读取相应长度的数据,直到达到目标索引。

相关推荐
2401_857439691 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna2 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
向前看-2 小时前
验证码机制
前端·后端
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
Dream_Snowar3 小时前
速通Python 第三节
开发语言·python
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫3 小时前
泛型(2)
java
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
南宫生3 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论