全网最详细完备的class类文件结构解析

全网最详细完备的class类文件结构解析写在前面本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!本专栏目录结构和文献引用请见100个问题搞定Java虚拟机解答Class文件是一组以8位字节为基础单位的二进制流,不同的数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有任何空隙存在。这些数据项目由无符号数和表来存储数据,按照顺序依次是:1.魔数和Class文件的版本2.常量池3.访问标志4.类索引、父类索引与接口索引集合5.字段表

大家好,又见面了,我是你们的朋友全栈君。

写在前面

本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和文献引用请见100个问题搞定Java虚拟机

解答

在这里插入图片描述

Class文件是一组以8位字节为基础单位的二进制流,不同的数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有任何空隙存在。

这些数据项目由无符号数和表来存储数据,按照顺序依次是:
1. 魔数和Class文件的版本
2. 常量池
3. 访问标志
4. 类索引、父类索引与接口索引集合
5. 字段表集合
6. 方法表集合
7. 属性表集合

Java代码之所以能够一直保持良好的向后兼容性,就是因为class类文件结构一直比较稳定。

补充

可能很多人觉得了解学习 class 类文件结构对于开发 Java 代码没有什么用处,深度学习了本文,下面的问题你就能自己回答了。

  1. Java为什么能一直保持良好的向后兼容性?
  2. Java虚拟机是怎样处理异常的?
  3. Java代码抛出异常堆栈的时候是如何打印出代码行号的?
  4. Java代码抛出异常堆栈的时候是怎样识别文件名称的?
  5. IDE调试的时候Java程序是如何识别断点的?
  6. IDE调试的时候是如何识别方法参数名称的?
  7. 类变量和实例变量是什么时候赋值的?
  8. Java中泛型是如何实现的?泛型信息是如何保存的?
  9. Java模块化功能是如何实现的?

无符号数

无符号数是基本的数据类型,u1、u2、u4、u8分别表示1个字节、2个字节、4 个字节和8个字节的无符号数。

无符号数可以用来表示数字、索引引用、数量值或者按照UTF-8编码构成字符串。

表用于描述有层次关系的复合结构的数据,由多个无符号数或者其他表构成。

所有表都习惯性地以”_info”结尾。

容器计数器

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用个前置的容量计数器加若干个连续的数据项的形式, 这时称这一系列连续的某一类型的数据为某一类型的集合。

魔数(Magic Number)

每个Class文件的头4个字节称为魔数,即:OXCAFEBABE(咖啡宝贝)

作用

确定这个文件是否是一个合法的Class文件。

因为文件扩展名可以随意地改动,所以使用魔数而不是扩展名来进行识别class文件更加的可靠。

Class文件的版本

紧接着魔数的4个字节存储的是Class文件的版本号:

5、6个字节是次版本号

7、8个字节是主版本号

Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1。

可以推导出 JDK8 的版本号是 52.0

JDK1.2~JDK12之间次版本号都为0。

JDK12以后考虑到一些复杂的特性需要公测,重新启用了次版本号。

高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的 Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

常量池(Constant Pool)

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件的资源仓库

常量池是Class文件结构中与其他数据项目关联最多的数据类型,通常也是占用 Class文件空间最大的数据项目之一,同时它还是在 Class文件中第一个出现的表类型数据项目。

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。

与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

常量池中主要存放两大类常量:字面量和符号引用

字面量(Literal)

字面量类似于Java语言里面的常量,包括文本字符串、声明为final的常量值等。

符号引用(Symbolic References)

符号引用包括了下面6类常量:

  1. 被模块导出或者开放的包(JDK9+)
  2. 类和接口的全限定名
  3. 字段的名称和描述符
  4. 方法的名称和描述符
  5. 方法句柄和方法类型
  6. 动态调用点和动态常量

关于 5、6 请见我的另一篇博客——invokedynamic是如何实现的?

Java代码需要在虚拟机加载Class文件的时候进行动态连接。

当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

访问标志(Access Flags)

在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:

  1. 这个Class是类还是接口;
  2. 是否定义为public类型;
  3. 是否定义为abstract类型;
  4. 如果是类的话,是否被声明为final等

类索引、父类索引与接口索引集合(this class,super class and interfaces)

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合。

Class文件中由这三项数据来确定这个类的继承关系

类索引

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。

父类索引与继承

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

接口索引集合

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

字段表集合(Field Info)

字段表用于描述接口或者类中声明的变量。

字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。(方法内部的局部变量存储在栈帧的局部变量表内部。)

在Java中描述一个字段可以包括的信息有:

  1. 字段的作用域(public、 private、 protected修饰符)
  2. 是实例变量还是类变量(static修饰符)
  3. 可变性(final)
  4. 并发可见性(volatile修饰符,是否强制从主内存读写)
  5. 可否被序列化(transient修饰符)
  6. 字段数据类型(基本类型、对象、数组)
  7. 字段名称

上述这些信息中,每个修饰符都是布尔值,要么有某个修饰符,要么没有,所以很适合使用标志位来表示。

而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

方法表集合

方法表的结构类似于字段表,依次包括了

  1. 访问标志
  2. 名称索引
  3. 描述符索引
  4. 属性表集合

方法表存储的是方法的元数据信息,真正的代码存储在方法属性表中的 Code 属性里面。

属性表集合

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

《Java虚拟机规范》不再要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,

Java虚拟机运行时会忽略掉它不认识的属性。

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
Exceptions 方法表 方法抛出的异常列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 记录源文件名称
ConstantValue 字段表 由 final 关键字定义的常量值
InnerClasses 类文件 内部类列表
Signature(JDK5+) 类、方法表、字段表 用于支持泛型情况下的方法签名
BootstrapMethods(JDK7+) 类文件 用于保存 invokedynamic 指令引用的引导方法限定符
MethodsParameters(JDK8+) 方法表 支持将方法名称编译进 Class 文件中,可以运行时获取
Module/ModulePackages/ModuleMainClass(JDK9+) 支持模块化相关功能

Code

Java程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。

Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性, 譬如接口或者抽象类中的方法就不存在Code属性,

Code属性是 Class 文件中最重要的一个属性,

如果把一个Java程序中的信息分为代码(code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,

那么在整个 Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

异常表(可选)

在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于Code属性来说并不是必须存在的

关于异常表的详细内容请参考我的另一篇博客——JVM是如何处理异常的?

Exceptions

这里的Exceptions属性是在方法表中与Code属性平级的一项属性,上面的异常表是 Code 属性里面的内容。

Exceptions属性的作用是列举出方法中可能抛出的受检异常(Checked Exceptions),也就是方法描述时在throws关键字后面列举的异常。

LineNumberTable(可选)

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。

默认会生成到 Class文件之中,可以在Javac中分别使用-g:none 或-g:lines选项来取消或要求生成这项信息。

如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,

并且在调试程序的时候,也无法按照源码行来设置断点。

LocalVariableTable属性(可选)

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,

默认会生成到 Class文件之中,可以在 Javac 中分别使用-g.none或-g:vars选项来取消或要求生成这项信息。

如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,

这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。

SourceFile属性(可选)

SourceFile属性用于记录生成这个Class文件的源码文件名称。

可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。

在Java中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。

如果不生成这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值

只有被static关键字修饰的变量(类变量)才可以使用这项属性。

实例变量什么时候进行初始化?

对于实例变量的赋值是在实例构造器方法中进行的;

类变量什么时候初始化?

对于类变量,则有两种方式可以选择

  1. 在类构造器方法中
  2. 使用ConstantValue属性

目前Oracle实现的Java编译器的选择是

  1. 如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成 ConstantValue 属性来进行初始化,
  2. 如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在方法中进行初始化。

InnerClasses属性

InnerClasses 属性用于记录内部类与宿主类之间的关联。

如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成 InnerClasses属性。

Signature属性

Signature属性在JDK1.5发布后增加到了 Class 文件规范之中,它是一个可选的定长属性,可以出现于类、属性表和方法表结构的属性表中。

在JDK1.5中大幅增强了Java语言的语法,在此之后,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types), 则Signature属性会为它记录泛型签名信息。

擦除法实现泛型

之所以要专门使用这样一个属性去记录泛型类型,是因为Java语言的泛型采用的是擦除法实现的伪泛型,

在字节码(Code属性)中,泛型信息编译(类型变量、参数化类型)之后都通通被擦除掉。

擦除法的好处与坏处

使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport(即将一个软件的补丁应用到比此补丁所对应的版本更老的版本的行为),运行期也能够节省一些类型所占的内存空间。

但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得到泛型信息。

Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。

BootstrapMethods 属性

BootstrapMethods属性在JDK1.7发布后增加到了 Class文件规范之中,它是一个复杂的变长属性,位于类文件的属性表中。

这个属性用于保存 invokedynamic 指令引用的引导方法限定符。

MethodParameters属性

作用是记录方法的各个形参名称和信息。

编译器可以(编译时加上-parameters 参数)将方法名称写入 Class 文件。

MethodParameters VS LocalVariableTable

LocalVariableTable是Code属性的子属性(抽象方法和接口方法没有方法体就没有对应的 Code属性)。

JDK8 以前要获取方法名称(比如 IDE 的代码提示)只能通过 JavaDoc 得到。

MethodParameters是方法表的属性,和 Code 平级。

模块化相关属性

JDK9的最重要的功能是提供 Java 的模块化功能,因为模块描述文件(module-info.java)最终要编译成一个独立的 Class 文件来存储的。

所以 Class 文件格式也扩展了 Module/ModulePackages/ModuleMainClass 三个属性用于支持 Java 模块化的相关功能。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/138764.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • DialogResult的使用方法

    DialogResult的使用方法usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Data;usingSystem.Drawing;usingSystem.Linq;usingSystem.Text;usingSystem.Windows.Forms;namespace_2014

    2022年6月22日
    43
  • 数据库自动化运维平台–自助DML

    数据库自动化运维平台–自助DML今天介绍下最近开发的一个平台,自助DML。什么是DML,就是平常执行的增删改查数据库操作。有人有疑问这不是程序访问的操作,为什么还要做一个平台操作这些呢,其实这种操作主要是开发需要线下修复数据的一种操作,不只是增删改,还有建表,建索引,添加字段等,这些操作开发一般会提给DBA协助操作数据库。可能你会觉得这些活能有多少,其实这种活真不少,我上家公司是电商互联网公司,大概有七八百个实例,每天的这种操作有近百个。处理近百个这种需求,基本上一个人一天就不用干别的了。虽说现在的公司实例少点,但每天的工作量还是很大,关

    2022年5月17日
    45
  • 自动化测试平台(四):前端环境搭建

    自动化测试平台(四):前端环境搭建上一章节我们实现了用户模块的增删改查接口,现在有了接口了就需要开始开发前端页面对其进行展示交互了。现在越来越多的前端开发框架和UI组件让我们能够更容易迅速的去开发前端页面,这一章节将通过react(Web开发框架)+antd(UI组件库)+ts(Javascript的超集)的技术栈来搭建我们的前端项目。

    2022年6月16日
    42
  • Arduino使用HC05蓝牙模块与手机连接[通俗易懂]

    Arduino使用HC05蓝牙模块与手机连接[通俗易懂]通过本文,可以了解到以下内容:进入AT模式进行蓝牙基本参数设置Arduino蓝牙控制LED电路设计以及代码编写利用Andorid蓝牙串口调试软件测试功能进入At模式进行蓝牙基本参数设置想要使用Arduino的蓝牙模块,首先要对蓝牙模块进行基本参数设置。基本参数设置主要包含:蓝牙名称、模式以及匹配密码等。设置蓝牙模块可以使用USB-TTL连接电脑使用串口调试软

    2022年5月9日
    67
  • 第926期机器学习日报(2017-04-01)

    第926期机器学习日报(2017-04-01)机器学习日报2017-04-01自然语言生成任务全面综述@阿儁是个nerd深度学习进行目标识别的资源列表@爱可可-爱生活物体检测算法全概述:从传统检测方法到深度神经网络框架@爱可可-爱生活PyTorch资源大列表@爱可可-爱生活解密滴滴大数据和人工智能@新智元@好东西传送门出品,由@AI100运营,过往目录见http://ml.memect.com订阅:

    2022年5月9日
    38
  • 国内最好用的dns地址_dns的服务器地址设置

    国内最好用的dns地址_dns的服务器地址设置对于DNS我想我们大部分人都会使用运营商自动推荐使用他们自己DNS服务器,使用它们的DNS服务器容易出现被劫持。所以今天我在这里推荐几个国内安全稳定的DNS服务器供大家使用……1、中国互联网络信息中心公共DNS服务器(CNNIC’sDNS)首选地址:1.2.4.8备用地址:210.2.4.82、百度公共DNS(BAIDU’SDNS)地址:180.76.76.763、阿里公共DNS服务器(Ali…

    2025年9月27日
    3

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号