类文件结构(java虚拟机系列:一文明解 .class 文件)

一次编写,到处运行,.class文件功不可没

​ java在刚刚诞生的时候有一个很著名的口号,叫做"write once,run anywhere"(一次编写,到处运行),这句话体现了java语言跨平台的特性,但在先前就没有跨平台的语言了吗?答案是否定的。在c语言里也有一句话叫做"一次编写,到处编译",也就是说编写了c语言的源文件,然后拿到不同操作系统分别进行编译,再运行,看起来也是跨平台的,但这必须有一个前提,就是c语言源文件中使用的都必须是标准的类库,而因为c语言标准类库中实现的接口很少 ,所以各个系统厂家为自己的系统设计了不同的类库,导致了在windows平台下调用windows类库接口没问题,但换到linux上就不行了,就无法做到跨平台性。

没有加一层解决不了的事,如果有就加两层。以c语言为代表的语言,它们会直接把我们编写的程序直接编译为二进制的本地机器码,而又因为像上文所说的,不同系统厂家又有自己的类库,所以导致了编写的程序与操作系统耦合在了一起,就无法跨平台。但是java不一样,它会先把源代码——.java文件,然后把 .java 文件编译为 .class 文件,再把 .class 文件交给java虚拟机处理,相比c语言,它中间多了一层编译为 .class 文件的过程,这个 .class 文件是统一的,它可以运行在不同的java虚拟机上(每个不同系统都设计了对应的虚拟机),所以说java虚拟机不是跨平台的,但 .class 文件时跨平台的,而因为 .class 文件时经过源文件编译出来的,所以导致了java这门语言时跨平台的。更宏观的来说,只要一门语言能被编译为标准的 .class 文件并运行在java虚拟机上,那么这门语言就具备了跨平台的特性,像Kotlin、Clojure等。

类文件结构的总览

下面放一幅图,来看看.class文件里到底有什么
在这里插入图片描述

魔数与版本号

在.class文件开头的四个字节,用16进制表示,分别是"CA FE BA BE",它被用于标识这是一个.class文件。

不同版本的jdk支持着不同的.class文件版本号,高版本的jdk可以兼容低版本的版本号,但低版本的jdk不能往上兼容运行高版本的Class文件

JDK版本号Class文件版本号
JDK751.0
JDK852.0
JDK953.0
JDK1054.0
JDK1155.0

无符号数与表

无符号数和表都是类文件中的数据结构并且只有这两种。其中无符号数是类文件的基本数据结构,表是由多个无符号数或者表构成的更大的数据结构,其实有点类似于c语言的结构体。

  • 无符号数:以u1(一个字节)、u2(两个字节)、u4、u8表示,它可以用来描述数字、索引引用、数量值或者由utf-8编码成的字符串
  • :由多个无符号数或者其它表组成的更大的数据结构,通常以_info结尾。

常量池

​ 它是Class文件第一个表结构的项目,需要提醒一下,类文件中的每个大的结构(常量池、访问标志…)都会有一个容器计数(是一个u2类型),一般是从0开始的,但对于常量池很特殊,它是从1开始的。

​ 常量池主要存放:字面量和符合引用。

  • 字面量:像文本字符串、被声明为final的常量值等。
  • 符号引用:包括类和接口的全限定名称、字段和方法的描述符、方法句柄和方法类型等。

截至JDK13,常量池一共包括了13中类型的常量,其中每一个类型的常量都是一个表。下面列举常量池的项目类型

类型标志描述
CONSTANT_Utf8_info1UTF-8编码的字符串
CONSTANT_Interger_info3整形字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口方法的符号引用
CONSTANT_NameAndType_info12字段或方法的部分符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MethodType_info16表示方法类型
CONSTANT_Dynamic_info17表示一个动态计算常量

下面我们来看看两个简单的常量类型

CONSTANT_Class_info,它用来表示一个类或接口的描述符,下面是它的结构

类型名称数量
u1tag1
u2name_index1

其中name_index指向了一个CONSTANT_Utf8_info,代表了一个类或接口的全限定名称

CONSTANT_String_info,用来表示utf-8的字符串

类型名称数量
u1tag1
u2length1
u1byteslength

length表示的是字符串有多少个字节,而bytes是使用缩略编码表示的utf-8的字符串。下面是17中类型的常量的总结
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

访问标记

access_flags(访问标记),它是紧随常量池的两个字节,用于描述这个Class是否被final修饰、是否是一个接口、是否是一个注解等。标识符如下

在这里插入图片描述

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

类索引和父类索引都是u2类型的数据,它们各指向一个CONSTANT_Class_info常量类型(除了Object外,所有类都有父类),在接口集合前会有一个u2类型的接口计数器,用来记录对应的Class实现了几个接口,如果没有则为0。

字段表集合

它表示了类或者接口中声明的变量,就是在java中的字段,不包括方法中的局部变量。字段表集合包含了字段的修饰符信息(public、private、static、volatile…)、字段类型和名称,当然在一开始也会有一个计数器来记录有多少个字段

下面是字段表的结构

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count
  • access_flags代表着访问修饰符,它有下图几种标识

在这里插入图片描述

  • name_index:对常量池的引用,标识该字段的简单名称

  • descriptor_index:指向常量池的描述符,代表字段的类型

  • attributes:引用属性表集合,储存额外的信息。(下面会讲到属性表集合)

方法表集合

它跟字段表集合的结构一致

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count
  • descriptor_index:这个与字段表集合的有点不同,它指向的常量池描述符描述了方法的参数列表和返回值。所以在这里总结一下:描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

属性表集合

属性表集合里有很多属性表,如下图

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

在这里挑相对重要的Code属性来讲

Code属性

下面是Code属性表的结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2max_stack1
u2max_locals1
u4code_length1
u1codecode_length
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attributes_count1
attribute_infoattributesattributes_count

Code里面主要保存着方法里代码编译后变成的字节码指令

  • max_stack:操作数栈,虚拟机会根据这个值来分配栈帧中的操作栈的深度

  • max_locals:代表着局部变量表的储存空间。储存空间的单位是槽位,对于不超32位的数据类型,占用一个槽,而超过了32位的,如long、double就占两个槽。当然,并不是方法里面声明了几个局部变量,为了节省空间,javac在编译时会重用槽位,主要时根据变量的作用域范围决定的,根据同时生存的最大局部变量数量和类型计算出max_locals的大小。

  • code:这是重头戏,用于储存方法编译后生成的字节码指令,基本的字节码指令会在稍后提到。

下面为了学习字节码指令,我们写一个简单的类,如下

public class JVMTest {
    public void testMethod(){
        int a;
        try {
            a = 10/3;
        }catch (Exception e){
            a = 10;
        }finally {
            a = 100;
        }
    }
}

我们先用javac命令编译,然后再用 javap -verbose xxx 命令显示出字节码文件,如下图

Classfile /D:/IDEAProject/jdk11reading/src/com/test/JVM/JVMTest.class
  Last modified 2021-10-30; size 426 bytes
  MD5 checksum 6a2994261bb809af15cf00631ecb56ba
  Compiled from "JVMTest.java"
public class com.test.JVM.JVMTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
//下面代表的就是常量池
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Class              #16            // java/lang/Exception
   #3 = Class              #17            // com/test/JVM/JVMTest
   #4 = Class              #18            // java/lang/Object //它指向了CONSTANT_Utf8_info所代表的utf-8编码字符串
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               testMethod
  #10 = Utf8               StackMapTable
  #11 = Class              #16            // java/lang/Exception
  #12 = Class              #19            // java/lang/Throwable
  #13 = Utf8               SourceFile
  #14 = Utf8               JVMTest.java
  #15 = NameAndType        #5:#6          // "<init>":()V
  #16 = Utf8               java/lang/Exception
  #17 = Utf8               com/test/JVM/JVMTest
  #18 = Utf8               java/lang/Object
  #19 = Utf8               java/lang/Throwable
{
  public com.test.JVM.JVMTest();
    //这个是该方法的描述符
    //括号里面是方法参数,括号后面的V代表返回值是void
    descriptor: ()V
    //它的访问访问标志是 public
    flags: ACC_PUBLIC
    //下面就是该方法的属性Code,储存着字节码指令
    //可以先看下面对字节码的讲解,然后再回过头来看下面的字节码
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V //它指向了方法描述符“#1”,表示的是构造方法
         4: return
      LineNumberTable:
        line 3: 0

  public void testMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      //这里操作栈深最大为1、局部变量表大小为4、方法传入的参数为1
      //大家有没有疑问,明明该方法没有传入参数,为什么args_size=1 ?
      //是因为我们在构造方法或者普通的非静态方法中是可以用this关键字的
      //而这个关键字引用的是实例对象本身,所以这些方法都把实例对象当作参数传入
      //所以我们就可以在这些方法中调用this
      stack=1, locals=4, args_size=1
         0: iconst_3
         1: istore_1
         2: bipush        100
         4: istore_1
         5: goto          24
         8: astore_2
         9: bipush        10
        11: istore_1
        12: bipush        100
        14: istore_1
        15: goto          24
        18: astore_3
        19: bipush        100
        21: istore_1
        22: aload_3
        23: athrow
        24: return
      //下面这个是异常表
      //如果方法中用了try{}catch(){}语句,那么就会生成一个异常表(异常表不是必须的)
      //异常表总共有三行,它们对应的意思分别是
      //1.如果try语句块(对应字节码指令第0行到第2行)出现了Exception类或者其子类的异常,则跳到catch语句块执行(第8行)
      //2.如果try语句块内没有出现Exception及其子类的异常,那么跳到finnal语句块(第18行)
      //3.catch语句块无论任何情况下,最后会跳到finnal语句块执行
      Exception table:
         from    to  target type
             0     2     8   Class java/lang/Exception
             0     2    18   any
             8    12    18   any
      LineNumberTable:
        line 7: 0
        line 11: 2
        line 12: 5
        line 8: 8
        line 9: 9
        line 11: 12
        line 12: 15
        line 11: 18
        line 12: 22
        line 13: 24
      StackMapTable: number_of_entries = 3
        frame_type = 72 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ][]
        frame_type = 73 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 252 /* append */
          offset_delta = 5
          locals = [ int ]
}
SourceFile: "JVMTest.java"

字节码指令简单介绍

字节码指令由两个部分构成,前面是一个字节的、代表着特定操作含义的数字,而后面跟随零或多个参数,代表该操作所需要的参数。

字节码指令可以分为一下几类,因为篇幅有限,只能大概总结一下,详细的请看《深入理解Java虚拟机》或者《Java虚拟机规范》
在这里插入图片描述

加载和存储指令

加载和存储指令主要用于操作数栈和局部变量表之前数据的传输。

  • 比如将一个局部变量加载到操作数栈,用iload、iload、lload等(load前面的代表操作的数据类型)
  • 如果将操作栈中的数据存入局部变量表中用 istore、lstore
  • 将常量加载到操作数栈中:bipushiconst_m1、iconst_<i>等

方法调用和返回指令

字面意思,这类指令用来执行方法调用

  • invokespectial:用于调用一些特殊的实例方法、比如实例初始化方法、私有方法和父类方法
  • invokevirtual:用于调用实例方法。

ConstantValue属性

这个属性是用来通知虚拟机对静态属性进行赋值的。Oracle对javac编译的实现规定,对于一个由final或者static修饰的字段,会为其生成对应的ConstantValue来负责变量的初始化,否则只能通过类构造器 ()方法来初始化赋值,下面是它的结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2constantvalue_index1

constantvalue_index代表了对常量池的一个字面量引用,比如CONSTANT_Float_info、CONSTANT_String_info等

小结

通过阅读这篇文章可以快速了解Class文件内的构造,本文是基于笔者在阅读《深入理解Java虚拟机后》后对其的总结并结合了自己的感悟和想法。

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值