class文件结构详解

简介: 写在最前:学习class文件结构不像学习JVM内存结构、垃圾收集器那样,可以对我们写代码时有很多帮助,学习了JVM内存结构,我们在配置虚拟机参数时就会有更全面的考虑,写代码时就可以注意到代码的的优化空间,学习了垃圾收集器,让我们可以根据服务器的配置,更好的选择出适合程序最大吞吐量的收集器,更好的根据服务器硬件配置出合适的参数,学习class呢,则更多的是为了让我们知其然,也知其所以然,让我们知道我们写出的代码在JVM里面到底是怎么运行的,这部分内容会相对枯燥,白话多一些,这里主要分两个部分来详细讲述class文件的机构[class文件结构、字节码指令]。

写在最前:学习class文件结构不像学习JVM内存结构、垃圾收集器那样,可以对我们写代码时有很多帮助,学习了JVM内存结构,我们在配置虚拟机参数时就会有更全面的考虑,写代码时就可以注意到代码的的优化空间,学习了垃圾收集器,让我们可以根据服务器的配置,更好的选择出适合程序最大吞吐量的收集器,更好的根据服务器硬件配置出合适的参数,学习class呢,则更多的是为了让我们知其然,也知其所以然,让我们知道我们写出的代码在JVM里面到底是怎么运行的,这部分内容会相对枯燥,白话多一些,这里主要分两个部分来详细讲述class文件的机构[class文件结构、字节码指令]。


一.class文件结构



这部分内容虽然会枯燥些,但是比较友好的是这部分内容从JDK1.0开始就没有太大的变动,所以说这部分只要掌握了基本可以说就是一劳永逸的了,不会像JVM内存分布会随着收集器改变而改变,也不会像收集器那样,不断的更新,因为每一代JDK版本的发布都会做到兼容以前的版本,必须保证以前的程序在新版的JDK上运行是可行的,这也保证了class文件结构的相对稳定性,下面看下class文件的各个结构以及作用。


1.什么是class文件?


JVM并不认识java格式文件,它所能执行的都是class文件,java程序通过javac编译器将java文件转化为class文件,然后就可以被虚拟机执行了,所有的虚拟机都是执行class文件,这也使得java文件呢一次编译后在其他机子上也是可以执行的,也就是java语言所说的一次编译处处运行了。class文件是一组以8字节为基础单位的二进制流,各个数据项目(魔数、版本。。。)严格按照顺序排列在一起,中间没有间隔符,遇到8个字节以上的空间存储时,则会按照高位在前的方式分割成若干个8个字节进行存储,这就是class文件,总结一句话class文件就是存储java编译后可被JVM识别信息的文件,里面数据以8字节为单位,各数据项有序排列,无间隔。


2.什么是无符号数?表?集合?


在说class文件的具体结构之前,必须要先介绍下这三种结构,因为class中的各项信息均是存储在这三种结构之中的。


无符号数:顾名思义无符号数就是一组没有符号的数据,是一种基本的数据类型,无符号数有1个字节(u1),2个字节(u2),4个字节(u4),8个字节(u8)这些结构,主要用以存储数字、索引引用、数量值或者字符串值(UTF-8编码后),它是class文件里面存储数据的最小单元。


表:无符号数是class文件存储数据的最小单元,表则是由多个无符号数组成的数据类型(多个表构成的数据项也叫表),表的命名习惯以“_info”结尾。主要用以描述拥有层次关系的复合结构数据。整个class文件其实也可以视为一张表(各种信息分层次存储)。


集合:集合其实就是无符号数或者表,之所以叫集合是因为它存储的是多个无符号数、或者表。既然存储的是多个那虚拟机是需要知道具体存储的无符号数或者表的个数的,因此在集合的开头会有一个u2类型的数据用以存储集合中数据项的个数叫容量计数器,这样就构成了一个集合。集合顾名思义用以存储相同类型的多个数据。


总结这三个结构我们可以看出,其实存储的最小结构都是无符号数,多个无符号数构成表,多个无符号数加前置的容量计数器构成集合。他们的本质都是无符号数变化而来。


3.魔数与Class文件的版本号


魔数(Magic Number):class文件的第一个u4结构存储的就是魔数,魔数的唯一作用就是供虚拟机辨别是否是可执行的class文件,有人会说不是有后缀名辨别文件类型了吗,用魔数岂不是多余,其实也不是多余,使用魔数作为文件的辨别可以增加安全性,因为后缀名是可以随便更改的,class文件的魔数是0xCAFEBABE,而且并不是只有class文件才有魔数,比如常见的后缀为jpg、jpeg、png、gif、zip、jar等等这些文件都是有魔数的,如下图,我们使用WinHex这款十六进制编辑器打开一个class文件看下是否是0xCAFEBABE,很明显就是了。


20210302112428292.png


版本号:紧接着魔数存储的就是版本号了,版本号占用一个u4,前两个u2存储次版本,后一个u2存储主版本,次版本其实在JDK1.2到JDK12之间是没有用处的。只有在JDK12之后,java文件中如果使用了预览功能,则会在生成的class文件中次版本号存储为65535。这也是次版本目前的唯一用处了。主版本则是比较主要的一项,它存储的是JDK的版本号,这个值在JDK1.0和JDK1.1使用的是45,往后都是JDK发布一个大版本这个值就相应的增加1,到了我们常用的JDK8时,该值就是52了,为什么要表示JDK的版本号呢,因为虚拟机必须要保证向下兼容,以前虚拟机编译出来的文件必须在当前虚拟机是可以执行的,此外虚拟机是拒绝执行高于当前虚拟机版本的class文件的。也就是说JDK8的虚拟机是执行不了JDK9编译的class文件的但是JDK7,JDK6等之前的虚拟机编译的class文件都可以执行。


下面展示下次版本、主版本,这是16进制打开的文件,34转换成10进制也就是52了。


20210302113626393.png


总结魔数与版本号可以发现,这一块内容会随着虚拟机的固定而固定,同一个虚拟机下不会因为文件的不同而不同(JDK12之前),此外前面也说过class文件是一组以8个字节为基础单位的二进制文件,魔数与版本号则是占用了第一个八字节的数据项。


4.常量池


什么是常量池?


这里说的常量池是class常量池,我们常见的常量池有class常量池、运行时常量池、字符串常量池。这三种常量池是三种东西,这里简单说下,class常量池是存储在class文件里面的静态数据,运行时常量池在方法区里存储的是被加载后该class文件的字面量与符号引用,字符串常量池在堆中,专门用于存储字符串常量。言归正传那什么是常量池呢(class常量池)?常量池顾名思义用以存储常量的池子,class文件里面所有的常量都会存储在这个池子里面,他是class文件的资源仓库,也是与其他项交集最多的一项结构,主要存储的常量是字面量、符号引用。字面量就是我们在类中定义的常量、字符串等等(常量比如局部变量int,class被加载后存储在了局部变量表,字符串则会进入字符串常量池),符号引用则是类或接口的全限定名,方法和字段的名称和描述符,方法的句柄类型等信息,这就是常量池。


常量池的特点:


常量池是class文件里的第一个表结构型数据,前面已经说过表是由多个无符号数,或者多个表机构构成的。常量池就是由很多个表构成,因为是多个表构成所以他的开头是一个u2类型的容量计数器,存储的是该常量池中表的个数,常量池的容量计数器与其他不同,该计数器是从1开始真正计数的代表的是各个常量的索引,0不指向任何表,而是代表“不引用任何一个常量池中项目”的含义,下图看下常量池的数量是0x2B,代表十进制的43,则表示常量池中有42个表结构数据。

20210302150830997.png


我们使用javap -verbose 后面跟上class文件名可以查看,该class文件的字节码内容。我们看下该文件的字节码信息常量池是不是42个常量,信息如下,可以清晰看出总共是有42个常量(表)存储在常量池中。

20210302151136832.png


先解释下上面常量池表的结构,第一列#1、#2等是索引号,也是存储的序号,第三列Methodref、String、Fieldref存储的则是表的类型,第四列存储的则是当前表存储的信息,第五列双斜杠后表示当前结构存储的具体的值起到说明的作用相当于注释,从上面的图片我们不仅可以看到常量池中有42项常量表,图中出现的有Utf8、String、Fieldref、Methodref等等这些表,那常量池中都有哪些表结构呢?


常量池中有哪些表结构?


常量池中总共有17种表结构(截止JDK13),用以存储类中的字面量与符号引用,这些信息在虚拟机解析时会根据索引号找到具体值被加载进虚拟机中,这17种表结构涵盖了所有的java信息,所有的表如下所示:


2021030216142346.png

因为每一种表结构的存储结构都不相同,去介绍每一种表结构不太现实,在我看来也没有必要去对每一种表结构进行熟练掌握,只需要知道这些表结构,知道用来存储什么即可,下面附上这些表结构的详细解释,以供需要时进行参考:


20210302162636655.png20210302162655159.png20210302162713694.png


5.标志位


紧挨着常量池的一个u2类型数据就是标志位了,标志位用以存储当前类或者接口的访问标志,有:是否是public、是否是abstract、是否是final等等,总共有9种类的修饰信息如下图


20210302164126412.png


其中该标志ACC_SUPER比较特殊,在JDK1.0.2之后就必须是真了,所以标志位的最小值就是0x0020了,那如果多个标志都是true,标志位是如何表示的呢,在多个标志位都是true时,会对其对应的标志值进行相加,得到的值就是标志位的展示值了,我们通过标志位展示的值很容易就可以推断出哪些标志是true的。


6.类索引、父类索引、接口索引集合

这一项存储的信息主要就是确定当前类的类全限定名、父类全限定名、实现的接口的全限定名,看到这里肯定有人会有疑问,这部分信息不是在常量池中声明过了吗,这些都是属于符号引用,前面说过常量池相当于一个资源仓库,这里的类全限定名、父类全限定名、接口全限定名引用的都是常量池中的信息。类索引、父类索引都是各使用一个u2类型的数据存储,java支持多实现所以接口索引集合使用多个u2类型的数据进行存储。


7.字段表集合


这是一种集合结构的数据,前面我们已经介绍过集合的定义,集合是相同数据结构的无符号数或者表多个汇集在一起,加上前置的容量计数器来构成的。字段表集合结构自然也是这样的是由一个容量计数器加上多个字段表构成的。


什么是字段表?


字段表用以描述类或接口中声明的变量,java语言中说变量默认是指类变量与实例变量,是不包含局部变量的,因此字段表肯定是不存储局部变量的。一个字段表分为三个部分,每个部分个占用一个u2结构:访问标志、字段简单名称(字段名)、字段描述符(描述字段类型)。如下图所示:


20210303090142718.png


上图所示就是一个字段表的结构图,但是无论是字段表还是方法表有时后面都会跟有对应的属性表集合,该集合下面会具体介绍,现在只需要知道,属性表集合是对字段表或者方法表的一个补充说明,并不会单独存在。此外针对字段表的三个部分,都是需要单独说一下的。


访问标志:这个访问标志与class文件中的访问标志是十分类似的,都是用以表示访问修饰符所用。字段表的访问修饰符有如下几种,我们可以通过其对应的u2数据轻松推断出该字段表描述的字段的访问修饰符。


20210303091217547.png

字段简单名称(name_index):name_index存储的是常量池中的索引号,这个索引号对应的常量就是该字段的简单名称。

字段描述符(descriptor_index):descriptor_index存储的也是常量池中的索引号,这个索引号对应的常量存储的是描述符的标识。下面展示下描述符与标识之间的关系:

20210303110920601.png


如上所示,如果定义的类型是byte类型,那么字段描述符在常量池中真正存储的值就是B。


字段表集合总结,从上面可以看出字段表的结构相对还是复杂一些的,所以这里举个例子更形象的去说明下字段表的结构。如下图:


20210303111437791.png

20210303111231721.png


上图是一个字段表的信息,fields_count是1表示容量计数器是1,说明只有一个字段表,access_flags是0x0002,对照字段表访问修饰我们可以看出该对象是private的,name_index存储的是常量池中的索引,如上方第二张图可以看出字段的简单名称是m,descriptor_index存储的也是常量池中的索引号,通过上方第二张图片可以看出该常量是I,对照字段描述符的标识表格我们可以看到I代表的是int,所以这个字段表存储的是一个实例变量private int m。


8.方发表集合


如果字段表集合掌握了,其实方法表集合也就掌握了,因为字段表中存储的信息比较绕,还是需要花费一点时间掌握的,掌握了字段表集合,方法表也就ok了,这两项基本没有区别。方法表集由一个容量计数器和多个方法表以及属性表集合构成。一个方法表也是有三个部分(与字段表相同)访问标志、方法名称索引、方法描述符索引三项。与字段表基本一致,不一致的地方是每个方发表都会有自己的属性表集合,因为方法的代码会存储的code这个属性表中(很少有没有代码的方法)。


下面是方法表中所有的访问标志:


20210303180443870.png


这里就不详细的去重复介绍方法表这三个部分了,与字段表是没有区别的。


9.属性表集合


属性表集合是class文件结构里要介绍的最后一项了,属性表集合并不会单独存在,会和字段表、方发表配合使用,作为这些表的补充存在,这里存储的信息很多,比如常见的方法编译后的代码是存在code属性表中的,方法中定义的异常是存在Exceptions表中的,同样的这块内容也是很多,且自己实现的编译器是支持新增属性的。除了属性表集合,以上介绍的几种结构中没有存储的信息,基本都是在这个属性表里了,下面列下所有的属性表以供参考,在我看来并不需要每个属性表都知道干啥,我们只需要知道,哪些信息在这里存储就行。


20210303181439176.png20210303181528282.png


二.字节码指令介绍


如果有人十分认真看完上面介绍的这部分内容,那我还是十分佩服的,因为这部分实在是太枯燥了,如果看了上面部分,别急还有下面这部分呢,也是比较枯燥的。


10.什么是字节码指令?


字节码指令是操作系统中的概念,一般包含两部分①操作码②操作数。操作码是一个占用了一个字节代表了某种特殊含义的二进制数,该数值是被预先定义好了会执行某种操作的。操作数通常跟在操作码之后,可以是0至多个,就像方法里的入参一样。他们俩一起就构成了一个字节码指令。在JVM虚拟机中采用的是面向操作数栈的架构。所以只有操作码的存在,操作数一般存放在操作数栈中,配合操作码一起完成虚拟机的一次指令。


11.操作码的类型


我们写的所有程序都是需要依赖操作码来完成的,我们可以回忆下平时写的程序:比如对象的创建、类型的转化、数据的加减乘除处理、方法调用、异常处理、方法的同步、代码块的同步等等。这些能想到的操作都是操作码和操作数共同协作完成的。而且操作码的命名一般都会尽量保持与数据类型相关,尽量会做到见名知意。比如int类型对应的加减乘除的操作码就是:iadd、isub、imul、idiv。这样我们一看就知道这些操作码是操作int类型的。下面介绍下几种常用的操作码。


12.对象创建相关指令


普通对象:new

数组对象:newarray、anewarray、multianewarray

置变量值与访问变量值指令:getfield、putfield、getstatic(类变量)、putstatic(类变量)

将值(操作数栈中)存入数组指令:bastore、castore、sastore、iastore、fastore。。。

获取数组长度指令:arraylength

java中只要不是语法糖实现的功能其实都会对应有响应的指令,这些指令有很多,也不需要都完全张给我,在我看来我们只需要掌握一些常见的就可以。下面会继续列出几种常见的指令。


13.数据运算相关的指令


加:iadd、fadd、ladd、dadd

减:isub、fsub、lsub、dsub

乘:imul、fmul、lmul、dmul

除:idiv、fdiv、ldiv、ddiv

求余:irem、frem、lrem、drem

取反:ineg、fneg、lneg、dneg

等等其中byte、short、boolean、char都是使用int的操作指令,他们四个是没有单独的操作指令的,但是存储(局部变量表、操作数栈中)时依然存储的是他们本身的类型。


14.方法调用与方法返回指令


这部分指令还是比较重要的,java中常说的解析和分派就和方法调用指令息息相关,所以这块还是需要记住的,下面具体介绍下这些指令(解析和分派需要说到很多这里不去讲解了)。


①invokevirtual指令:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派)。


我们测试下这个指令,写了如下代码:


public class TestSuper {
  public TestSuper(){
  }
  public void test(){
  }
  public static void mian(String[] args){
    TestSuper testSuper = new TestSuper();
    testSuper.test();
  }
}


根据invokevirtual指令的描述,那么我们在执行testSuper.test();这段代码时应该是使用的该指令,然后我们使用javap 指令看下class的字节码信息,如下图:

20210303193036464.png


从上方图片中我们可以轻易的看到在调用test方法时使用的就是invokevirtual指令。


②invokeinterface指令:用于调用接口方法,它会在运行时搜索一个该接口的实现对象,寻找到合适的方法进行调用。可以采用上面的方法进行验证,这里不做重复工作了。


③invokespecial指令:用于调用一些需要特殊处理的实例方法,比如构造器、私有方法、父类的构造器、父类的方法等都需要依赖这个命令来实现,这个也是很好验证的,但是需要说下另一个点,做一个小小的延伸,我们都知道this、super都是java中的关键字。this关键字的实现方式是隐式传参,那么super呢,这里不去解析this关键字是隐式传参的验证了,也不去验证super不是隐式传参的验证了。想要弄清楚这块的话可以去查下相关资料或者私聊我一起探讨。我们直说结论super的实现机制并不是隐式传参。我们看下invokevirtual这个指令的那段代码,其中有一个构造器。我们一样看下这个构造器对应的字节码信息:

20210303195017805.png


图中标红的就是super关键字的底层实现了,从这行指令我们可以看出当前指令正在调用的是Object的无参构造器,super在编译器编译后会被解释成invokespecial指令并携带参数取调用父类构造器,这就是super的实现机制了,并不是像有些人说的this、super都是隐式传参。


④invokestatic指令:从名字上看应该大家都会明白,该指令就是专门用于调用类方法的。这里不做重复验证了。


⑤invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。我们用的JDK8中的lamdba就是依赖这个指令才实现的。

15.同步指令


我们在写代码时经常会用到同步操作,比如常用的synchronized关键字。虚拟机中也会提供关键字对应到同步指令,不过方法的同步并不是依赖指令完成的而是方法表中会有一项是修饰符ACC_SYNCRONIZED,用以来表示方法是否是同步的,若是同步方法执行该方法的线程就必须持有一个锁,在执行完之后才会放掉,其他线程才有机会拿到这个锁。但是代码块的同步实现还是需要字节码指令来完成的,虚拟机提供monitorenter、monitorexit这两个指令用以支持synchronized这个关键字。


三.全文总结



这篇文章里介绍了class文件的结构,通过这块我们可以知道我们写的代码被编译器编译完以后各个信息存储的方式以及位置,然后介绍了一部分常用的字节码指令,字节码指令更多的对应着方法体内的信息,因为方法才是我们真正实现操作的地方。所以大部分的字节码指令也都是体现在字节码文件中的方法表中的code属性表中。我们通过这块可以了解到方法的底层实现到底是个怎么样子,比如各种方法的调用是怎么完成的,加减乘除又是怎么实现的。下面举个简单的例子来补充下字节码指令时怎么完成方法内的操作的,有如下代码:


public class TestByteCode {
    public static void main(String[] args){
        int a = 1;
        int b = 2;
        int c = a + b;
        System.out.println(c);
    }
}


我们看下main方法的方发表信息,如下:


20210303202042196.png


iconst_1表示将数值1压入操作数栈,istore_1表示将1这个常量方法局部变量表赋给局部变量a,

iconst_2表示将数值2压入操作数栈,istore_2表示将1这个常量方法局部变量表赋给局部变量b,

iload_1、iload_2则是将a、b两个局部加载到操作数栈中准备计算,iadd就是计算这两个int类型的变量了,istore_3则是将3这个数值放入局部变量表中的c,然后就是操作完成返回了(LineNumberTable是属性信息,与此无关)。


相关文章
|
4月前
|
存储 Oracle Java
JVM中Class文件结构详解
JVM中Class文件结构详解
71 0
|
6月前
|
存储 Java 程序员
【 class文件结构】
【 class文件结构】
|
11月前
|
存储 Java 开发者
【Class文件结构】
【Class文件结构】
|
Java 索引
Class文件结构分析
Class文件结构分析 1. Class文件的结构概览图 2. 每一项数据说明 类型 名称 数量 说明 u4 magic 1 魔数:确定一个文件是否是Class文件 u2 minor_version
74 0
|
存储 算法 前端开发
JVM Class 文件结构
本文着重介绍 JVM 中 Class 文件相关的内容
|
存储 Java 编译器
JVM的class文件结构详解(三)
JVM的class文件结构详解(三)
90 0
JVM的class文件结构详解(三)
|
存储 Java 编译器
JVM的class文件结构详解(一)
JVM的class文件结构详解(一)
105 0
JVM的class文件结构详解(一)
|
存储 Java C++
JVM的class文件结构详解(二)
JVM的class文件结构详解(二)
87 0
JVM的class文件结构详解(二)
|
Java
JVM的class文件结构详解(四)
JVM的class文件结构详解(四)
84 0
JVM的class文件结构详解(四)
|
存储 Java 项目管理
Class文件结构介绍[常量池]
常量池是紧接着主次版本号之后出现的,常量池可以理解为class文件之中的资源仓库,它是Class文件结构中与其他项目管理最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。案例代码还是和前一篇的一样
Class文件结构介绍[常量池]