java class文件的结构

1. JVM的 平台无关性

转载文章,纯粹是觉得文章写的不错才转载的。

Java具有平台无关性,即任何操作系统都能运行Java代码。 之所以能实现这一点,是因为Java运行在虚拟机之上,不同的操作系统都拥有各自的Java虚拟机,因此Java能实现"一次编写,处处运行"。

而JVM不仅具有平台无关性,还具有语言无关性:

  • 平台无关性是指不同操作系统都有各自的JVM
  • 语言无关性是指Java虚拟机能运行除Java以外的代码!

但JVM对能运行的语言是有严格要求的。首先来了解下Java代码的运行过程: Java源代码首先需要使用Javac编译器编译成class文件,然后启动JVM执行class文件,从而程序开始运行。 即JVM只认识class文件,它并不管何种语言生成了class文件,只要class文件符合JVM的规范就能运行。

因此目前已经有Scala、JRuby、Jython等语言能够在JVM上运行。它们有各自的语法规则,不过它们的编译器都能将各自的源码编译成符合JVM规范的class文件,从而能够借助JVM运行它们。
Class文件是JVM的输入, Java虚拟机规范中定义了Class文件的结构。Class文件是JVM实现平台无关、技术无关的基础。
在这里插入图片描述

2. class文件结构

class文件包含Java程序执行的字节码,数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符。 文件开头有一个0xcafebabe(16进制)特殊的一个标志。

下图展示为16进制
在这里插入图片描述
class文件内容:
在这里插入图片描述
8字节为单位的二进制字节流,对于占用空间大于8字节的数据项,按照高位在前的方式分割成多个8字节进行存储。 它的内容具有严格的规范,文件中没有任何分隔符,全是连续的0/1。
class文件中的所有内容被分为两种类型:

  • 无符号数 基本的数据类型,以u1、u2、u4、u8,分别代表1字节、2字节、4字节、8字节的无符号数
  • 表 class文件中所有数据(即无符号数)要么单独存在,要么由多个无符号数组成二维表.即class文件中的数据要么是单个值,要么是二维表.通常以_info 结尾

class 文件内容:

类型名称数量说明
u4magic1魔数:确定一个文件是否是Class文件
u2minor_version1Class文件的次版本号
u2major_version1Class文件的主版本号:一个JVM实例只能支持特定范围内版本号的Class文件(可以向下兼容)JDK5,6,7,8 分别对应49,50,51,52
u2constant_pool_count1常量表数量
cp_infoconstant_poolconstant_pool_count -1常量池:以理解为Class文件的资源仓库,后面的其他数据项可以引用常量池内容。
u2access_flags1类的访问标志信息:用于表示这个类或者接口的访问权限及基础属性。
u2this_class1指向当前类的常量索引:用来确定这个类的的全限定名。
u2super_class1指向父类的常量的索引:用来确定这个类的父类的全限定名。
u2interfaces_count1接口的数量
u2interfacesinterfaces_count-1指向接口的常量索引:用来描述这个类实现了哪些接口。
u2fields_count1字段表数量
field_infofieldsfields_count-1字段表集合:描述当前类或接口声明的所有字段。
u2methods_count1方法表数量
method_infomethodsmethods_count-1方法表集合:只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。
u2attributes_count1属性表数量
attributes_infoattributesattributes_count-1属性表集合:用于描述某些场景专有的信息,如字节码的指令信息等等。

2.1 文件格式

javap工具生成非正式的"虚拟机汇编语言” ,格式如下:

<index> <opcode>[<operand1> [<operand2> ...]][<comment>]

<index>是指令操作码在数组中的下标,该数组以字节形式来存储当前方法的Java虚拟机代码;也可以是相对于方法起始处的字节偏移量
<opcode>是指令的助记码
< operand>是操作数
<comment>是行尾的注释

实践: 将Demo1.java 通过javac 编译成class文件,通过javap生成非正式虚拟汇编语言。

  1. Demo.java文件内容:
public class Demo1 {
    public static void main(String[] args) {
        int x= 500;
        int y= 100;
        int a= x/y;
        int b=50;
        System.out.println(a + b);
    }
}
  1. Demo1.txt 文件内容:
Classfile /D:/temp/java_class/Demo1.class
  Last modified 2021-10-9; size 423 bytes
  MD5 checksum 0e82815ca86548ef87cca2c8411e5e67
  Compiled from "Demo1.java"
public class com.hope.Demo1
  minor version: 0   // 次版本号
  major version: 52  // 主版本号  JDK5,6,7,8 分别对应49,50,51,52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V
   #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #17.#18        // java/io/PrintStream.println:(I)V
   #4 = Class              #19            // com/hope/Demo1
   #5 = Class              #20            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               main
  #11 = Utf8               ([Ljava/lang/String;)V
  #12 = Utf8               SourceFile
  #13 = Utf8               Demo1.java
  #14 = NameAndType        #6:#7          // "<init>":()V
  #15 = Class              #21            // java/lang/System
  #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;
  #17 = Class              #24            // java/io/PrintStream
  #18 = NameAndType        #25:#26        // println:(I)V
  #19 = Utf8               com/hope/Demo1
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/System
  #22 = Utf8               out
  #23 = Utf8               Ljava/io/PrintStream;
  #24 = Utf8               java/io/PrintStream
  #25 = Utf8               println
  #26 = Utf8               (I)V
{
  public com.hope.Demo1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: sipush        500
         3: istore_1
         4: bipush        100
         6: istore_2
         7: iload_1
         8: iload_2
         9: idiv
        10: istore_3
        11: bipush        50
        13: istore        4
        15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_3
        19: iload         4
        21: iadd
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: return
      LineNumberTable:
        line 5: 0
        line 6: 4
        line 7: 7
        line 8: 11
        line 9: 15
        line 10: 25
}
SourceFile: "Demo1.java"

  1. 文件编译命令:
D:/environment/java_environment/JDK8_64/bin/javac  D:/temp/java_class/Demo1.java

D:/environment/java_environment/JDK8_64/bin/javap  -v  D:/temp/java_class/Demo1.class > Demo1.txt

2.2 魔数(Magic Number)

class文件的头4个字节称为魔数,唯一作用是确定这个文件是否为一个能被JVM接受的Class文件. 作用就相当于文件后缀名,只不过后缀名容易被修改,不安全. 是用16进制表示的"CAFEBABE".

2.3 版本信息

紧接着魔数的4个字节是版本号.它表示本class中使用的是哪个版本的JDK. 在高版本的JVM上能够运行低版本的class文件,但在低版本的JVM上无法运行高版本的class文件.

2.4 常量池

2.4.1 什么是常量池

紧接着版本号之后的就是常量池. 常量池中存放两种类型的常量:

  • 字面量 (Literal) 接近Java语言的常量概念,如:字符串文本、final常量值等.
  • 符号引用 (Symbolic Reference) 属于编译原理方面,包括下面三类常量:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

2.4.2 常量池的特点

  • 长度不固定 常量池的大小不固定,因此常量池开头放置一个u2类型的无符号数,代表当前常量池的容量. 该值从1开始,若为5表示池中有4项常量,索引值1~5
  • 常量由二维表表示 开头有个常量池容量计数值,接下来就全是一个个常量了,只不过常量都是由一张张二维表构成,除了记录常量的值以外,还记录当前常量的相关信息
  • class文件的资源仓库
  • 与本class中其它部分关联最多的数据类型
  • 占用Class文件空间最大的部分之一 ,也是第一个出现的表类型项目

2.4.3 常量池中常量类型

根据常量的数据类型不同,被细分为14种常量类型,都有各自的二维表示结构 每种常量类型的头1个字节都是tag,表示当前常量属于14种类型中的哪一个.
在这里插入图片描述

以CONSTANT_Class_info常量为例,它的二维表示结构如下: CONSTANT_Class_info表

类型名称数量
u1tag1
u2name_index1
  • tag 表示当前常量的类型(当前常量为CONSTANT_Class_info,因此tag的值应为7,表一个类或接口的全限定名);
  • name_index 表示这个类或接口全限定名的位置.它的值表示指向常量池的第几个常量.它会指向一个CONSTANT_Utf8_info类型的常量

CONSTANT_Utf8_info表:

类型名称数量
u1tag1
u2length1
u1byteslength
  • tag 表当前常量的类型,这里是1
  • length 表该字符串的长度
  • bytes为这个字符串的内容(采用缩略的UTF8编码)

Java中定义的类、变量名字必须小于64K 类、接口、变量等名字都属于符号引用,它们都存储在常量池中 而不管哪种符号引用,它们的名字都由CONSTANT_Utf8_info类型的常量表示,这种类型的常量使用u2存储字符串的长度 由于2字节最多能表示65535个数,因此这些名字的最大长度最多只能是64K

UTF-8编码 VS 缩略UTF-8编码 前者每个字符使用3个字节表示,而后者把128个ASCII码用1字节表示,某些字符用2字节表示,某些字符用3字节表示。

类信息包含的静态常量,编译之后就能确认:
在这里插入图片描述

2.5 JVM 指令

指令说明
invokeinterface用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。(Invoke interface method)
invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(Invoke instance method; dispatch based on class)
invokestatic用以调用类方法(Invoke a class (static) method )
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。(Invoke instance method; special handling for superclass, private, and instance initialization method invocations )

invokedynamic JDK1.7新加入的一个虚拟机指令,相比于之前的四条指令,他们的分派逻辑都是固化在JVM内部,而invokedynamic则用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,从而达到动态语言的支持。(Invoke dynamic method)

2.6 访问控制

在常量池结束之后是2字节的访问控制 表示这个class文件是类/接口、是否被public/abstract/final修饰等.

由于这些标志都由是/否表示,因此可以用0/1表示. 访问标志为2字节,可以表示16位标志,但JVM目前只定义了8种,未定义的直接写0.

标志名称标志值含义
ACC_PUBLIC0X0001是否为public 类型
ACC_MODULE声明的模块; 可能无法从其模块外部访问。 仅当ClassFile具有Module属性时才可以设置。
ACC_STATIC0X0008声明为静态
ACC_FINAL0X0010是否被声明成final, 只有类可设置
ACC_SUPER0X0020是否允许使用invokespecial 字节码指令的新语义,invokespeci 指令的语意在JDK 1.0.2 发生过改变,为了区别这条指令使用哪种语义,JDK 1.0.2 编译出来的类这个标识必须为真
ACC_INTERFACE0X0200标识这是一个接口
ACC_ABSTRACT0X0400是否为abstract类型,对于接口或者抽象类来说,此表示为真,其他类值为假
ACC_SYNTHETIC0X1000标识这个类并非由用户代码产生
ACC_ANNOTATION0X2000标识这是一个注解
ACC_ENUM0X4000标识这是一个枚举

Demo1.txt中的构造方法:

  public com.hope.Demo1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

Demo1这个示例中,我们并没有写构造函数。 由此可见,没有定义构造函数时,会有隐式的无参构造函数。

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

表示当前class文件所表示类的名字、父类名字、接口们的名字. 它们按照顺序依次排列,类索引和父类索引各自使用一个u2类型的无符号常量,这个常量指向CONSTANT_Class_info类型的常量,该常量的bytes字段记录了本类、父类的全限定名. 由于一个类的接口可能有好多个,因此需要用一个集合来表示接口索引,它在类索引和父类索引之后.这个集合头两个字节表示接口索引集合的长度,接下来就是接口的名字索引.

2.8 字段表的集合

2.8.1 什么是字段表集合

用于存储本类所涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量. 每一个字段表只表示一个成员变量,本类中所有的成员变量构成了字段表集合.

2.8.2 字段表结构定义

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attributeattributesattribute_count
  • access_flags 字段的访问标志。在Java中,每个成员变量都有一系列的修饰符,和上述class文件的访问标志的作用一样,只不过成员变量的访问标志与类的访问标志稍有区别。
  • name_index 本字段名字的索引。指向一个CONSTANT_Class_info类型的常量,这里面存储了本字段的名字等信息。
  • descriptor_index 描述符。用于描述本字段在Java中的数据类型等信息(下面详细介绍)
  • attributes_count 属性表集合的长度。
  • attributes 属性表集合。到descriptor_index为止是字段表的固定信息,光有上述信息可能无法完整地描述一个字段,因此用属性表集合来存放额外的信息,比如一个字段的值。(下面会详细介绍)

2.8.3 描述符(descriptor_index)

成员变量(包括静态成员变量和实例变量) 和 方法都有各自的描述符。 对于字段而言,描述符用于描述字段的数据类型; 对于方法而言,描述符用于描述方法的数据类型、参数列表、返回值。

在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。 描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且,参数之间无需任何符号。

2.8.4 字段表集合注意点

  • 一个class文件的字段表集合中不能出现从父类/接口继承而来字段;
  • 一个class文件的字段表集合中可能会出现程序猿没有定义的字段 如编译器会自动地在内部类的class文件的字段表集合中添加外部类对象的成员变量,供内部类访问外部类。
  • Java中只要两个字段名字相同就无法通过编译。但在JVM规范中,允许两个字段的名字相同但描述符不同的情况,并且认为它们是两个不同的字段。

Demo1.txt中的程序入口main方法:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: sipush        500
         3: istore_1
         4: bipush        100
         6: istore_2
         7: iload_1
         8: iload_2
         9: idiv
        10: istore_3
        11: bipush        50
        13: istore        4
        15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_3
        19: iload         4
        21: iadd
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: return

在这里插入图片描述

2.9 方法表集合

在class文件中,所有的方法以二维表的形式存储,每张表来表示一个函数,一个类中的所有方法构成方法表的集合。 方法表的结构和字段表的结构一致,只不过访问标志和属性表集合的可选项有所不同。方法表的属性表集合中有一张Code属性表,用于存储当前方法经编译器编译过后的字节码指令。

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attributeattributesattribute_count

2.9.1 方法表集合的注意点

  • 如果本class没有重写父类的方法,那么本class文件的方法表集合中是不会出现父类/父接口的方法表;
  • 本class的方法表集合可能出现程序猿没有定义的方法 编译器在编译时会在class文件的方法表集合中加入类构造器和实例构造器。
  • 重载一个方法需要有相同的简单名称和不同的特征签名。JVM的特征签名和Java的特征签名有所不同:
    • java特征签名:方法参数在常量池中的字段符号引用的集合
    • JVM特征签名:方法参数+返回值

3. 程序加载运行分析

加载信息到方法区:

在这里插入图片描述
JVM 创建线程来执行代码

在这里插入图片描述
程序入口main方法:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值