建设银行缴费网站登录,餐饮网站建设规划书,网站建设包六个,网站上的中英文切换是怎么做的文章目录 1、概述1.1、class文件的跨平台性1.2、编译器分类1.3、透过字节码指令看代码细节 2、虚拟机的基石#xff1a;class文件2.1、字节码指令2.2、解读字节码方式 3、class文件结构3.1、魔数#xff1a;class文件的标识3.2、class文件版本号3.3、常量池#xff1a;存放所… 文章目录 1、概述1.1、class文件的跨平台性1.2、编译器分类1.3、透过字节码指令看代码细节 2、虚拟机的基石class文件2.1、字节码指令2.2、解读字节码方式 3、class文件结构3.1、魔数class文件的标识3.2、class文件版本号3.3、常量池存放所有常量3.4、访问标识 一段Java程序编写完成后会被存储到以.java为后缀的源文件中源文件会被编译器编译为以.class为后缀的二进制文件之后以.class为后缀的二进制文件会经由类加载器加载至内存中。本贴要讲的重点就是以.class为后缀的二进制文件也简称为class文件或者字节码文件。接下来将会介绍class文件的详细结构以及如何解析class文件。 1、概述
1.1、class文件的跨平台性
Java是一门跨平台的语言也就是我们常说的“Write once,run anywhere”意思是当Java代码被编译成字节码后就可以在不同的平台上运行而无须再次编译。但是现在这个优势不再那么吸引人了Python、PHP、Perl、Ruby、Lisp等语言同样有强大的解释器。跨平台几乎成为一门开发语言必备的特性。
虽然很多语言都有跨平台性但是JVM却是一个跨语言的平台。JVM不和包括Java在内的任何语言绑定它只与class文件这种特定的二进制文件格式关联。无论使用何种语言开发软件只要能将源文件编译为正确的class文件那么这种语言就可以在JVM上执行如下图所示 比如Groovy语言、Scala语言等。可以说规范的class文件结构就是JVM的基石、桥梁。
JVM有很多不同的实现但是所有的JVM全部遵守Java虚拟机规范也就是说所有的JVM环境都是一样的只有这样class文件才可以在各种JVM上运行。在Java发展之初设计者就曾经考虑并实现了让其他语言运行在Java虚拟机之上的可能性他们在发布规范文档的时候也刻意把Java的规范拆分成了Java语言规范及Java虚拟机规范。官方虚拟机规范如下图所示 想要让一个Java程序正确地运行在JVM中Java源文件就必须要被编译为符合JVM规范的字节码。前端编译器就是负责将符合Java语法规范的Java代码转换为符合JVM规范的class文件。常用的javac就是一种能够将Java源文件编译为字节码的前端编译器。javac编译器在将Java源文件编译为一个有效的class文件过程中经历了4个步骤分别是词法解析、语法解析、语义解析以及生成字节码。
Oracle的JDK软件中除了包含将Java源文件编译成class文件外还包含JVM的运行时环境。如下图所示 Java源文件(Java Source)经过编译器编译为class文件之后class文件经过ClassLoader加载到虚拟机的运行时环境。需要注意的是ClassLoader只负责class文件的加载至于class文件是否可以运行则由执行引擎决定。
1.2、编译器分类
Java源文件的编译结果是字节码那么肯定需要有一种编译器将Java源文件编译为class文件承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源文件编译为字节码的前端编译器。
HotSpot VM并没有强制要求前端编译器只能使用javac来编译字节码其实只要编译结果符合JVM规范都可以被JVM所识别。
在Java的前端编译器领域除了javac还有一种经常用到的前端编译器那就是内置在Eclipse中的ECJ(Eclipse Compiler for Java)编译器。和javac的全量式编译不同ECJ是一种增量式编译器。
在Eclipse中当开发人员编写完代码使用CtrlS快捷键保存代码时ECJ编译器会把未编译部分的源码逐行进行编译而不是每次都全量编译。因此ECJ的编译效率更高。
ECJ不仅是Eclipse的默认内置前端编译器在Tomcat中同样也是使用ECJ编译器来编译jsp文件。由于ECJ编译器是采用GPLv2的开源协议进行开源的所以大家可以在Eclipse官网下载ECJ编译器的源码进行二次开发。另外IntelliJ IDEA默认使用javac编译器。
我们把不同的编程语言类比为不同国家的语言它们经过前端编译器处理之后都变成同一种class文件。如下图所示 前端编译器把各个国家的“你好”编译为一样的“乌拉库哈吗哟”这个“乌拉库哈吗哟”就好比class文件中的内容。class文件对于执行引擎是可以识别的所以JVM是跨语言的平台其中起关键作用的就是前端编译器。JIT编译器可以对程序做栈上分配、同步省略等优化。为了区别前面讲的javac把JIT称为后端编译器。
除了上面提到的前端编译器和后端编译器还有AOT编译器和Graal编译器。
1.3、透过字节码指令看代码细节
通过学习class文件可以查看代码运行的详细信息。代码清单如下所示测试不同Integer变量是否相等。 运行结果如下 truefalse显而易见两次运行结果并不相同。定义的变量是Integer类型采用的是直接赋值的形式并没有通过某一个方法进行赋值所以无法看到代码底层的执行逻辑是怎样的那么只能通过查看class文件来分析问题原因。通过IDEA中的插件jclasslib查看class文件如下图所示 class文件中包含很多字节码指令分别表示程序代码执行期间用到了哪些指令。这里仅说一下Integer i1 10语句执行的是java/lang/Integer.valueOf方法也就是Integer类中的valueOf方法我们查看源代码如下图所示 可以发现对Integer赋值的时候通过i和IntegerCache类高位值和低位值的比较判断i是否直接从IntegerCache内cache数组获取数据。IntegerCache类的低位值为-128高位值为127。如果赋值在低位值和高位值范围内则返回IntegerCache内cache数组中的同一个值否则重新创建Integer对象。这也是为什么当Integer变量赋值为10的时候输出为true,Integer变量赋值为128的时候输出为false。
2、虚拟机的基石class文件
源代码经过编译器编译之后生成class文件字节码是一种二进制的文件它的内容是JVM的指令其不像C、C经由编译器直接生成机器码。
2.1、字节码指令
JVM的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数只有一个操作码。如下图所示 其中aload_0是操作码没有操作数。bipush 30中的bipush是操作码30是操作数。
2.2、解读字节码方式
由于class文件是二进制形式的所以没办法直接打开查看需要使用一些工具将class文件解析成我们可以直接阅读的形式。解析方式主要有以下三种。
1、使用第三方文本编辑工具我们常用的第三方文本编辑工具有Notepad和Binary Viewer。以NotePad为例需要在插件中安装“HEX-Editor”插件。安装完插件之后打开一个class文件如下图所示 展示结果为乱码。如果想要以十六进制视图展示单击“插件”→“HEX-Editor”→“View in HEX”即可如下图所示 2、使用javap指令JDK自带的解析工具。
3、jclasslib工具jclasslib工具在解析class文件时已经进行了二进制数据的“翻译”工作可以更直观地反映class文件中的数据。各位读者可以下载安装jclasslib Bytecode viewer客户端工具或者在IDEA的插件市场安装jclasslib插件如下图所示
3、class文件结构
任何一个class文件都对应着唯一一个类或接口的定义信息但是并不是所有的类或接口都必须定义在文件中它们也可以通过类加载器直接生成。也就是说class文件实际上并不一定以磁盘文件的形式存在。class文件是一组以8位字节为基础单位的二进制流它的结构不像XML等描述语言由于它没有任何分隔符号所以在其中的数据项无论是字节顺序还是数量都是被严格限定的哪个字节代表什么含义长度是多少先后顺序如何都不允许改变就好像一篇没有标点符号的文章。这使得整个class文件中存储的内容几乎全部是程序运行的必要数据没有空隙存在。class文件格式采用一种类似于C语言结构体的伪结构来存储数据这种伪结构只有无符号数和表两种数据类型。
无符号数属于基本的数据类型以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。对于字符串则使用u1数组进行表示。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据整个class文件本质上就是一张表。由于表没有固定长度所以通常会在其前面加上长度说明。在学习过程中只要充分理解了每一个class文件的细节甚至可以自己反编译出Java源文件。
class文件的结构并不是一成不变的随着JVM的不断发展总是不可避免地会对class文件结构做出一些调整但是其基本结构和框架是非常稳定的。class文件的整体结构如下表所示 官方对class文件结构的详细描述如下图所示 上面class文件的结构解读如下表所示 下面我们按照上面的顺序逐一解读class文件结构。首先编写一段简单的代码对照上面的结构表来分析class文件代码清单如下所示
这段代码很简单只有一个成员变量num和一个方法fun()。将源文件编译为class文件我们使用命令javac编译如下所示 javac ClassFileDemo.java上面命令的执行结果是生成一个ClassFileDemo.class文件。使用安装好HEX-Editor插件的Notepad打开ClassFileDemo.class文件结果如下图所示 篇幅原因展示部分截图可以看到每个字节都是十六进制数字通过分析每个字节来解析class文件。
3.1、魔数class文件的标识
每个class文件开头的4个字节的无符号整数称为魔数(Magic Number)。魔数的唯一作用是确定class文件是否有效合法也就是说魔数是class文件的标识符。魔数值固定为0xCAFEBABE如下图框中所示 之所以使用CAFEBABE可以从Java的图标一杯咖啡窥得一二。
如果一个class文件不以0xCAFEBABE开头JVM在文件校验的时候就会直接抛出以下错误的错误。 比如将ClassFileDemo.java文件后缀改成ClassFileDemo.class然后使用命令行解释运行就报出上面的魔数不对的错误。
使用魔数而不是扩展名识别class文件主要是基于安全方面的考虑因为文件扩展名可以随意改动。除了Java的class文件以外其他常见的文件格式内部也会有类似的设计手法比如图片格式gif或者jpeg等在头文件中都有魔数。
3.2、class文件版本号
紧接着魔数存储的是class文件的版本号同样也是4个字节。第5个和第6个字节所代表的含义是class文件的副版本号minor_version第7个和第8个字节是class文件的主版本号major_version。它们共同构成了class文件的版本号例如某个class文件的主版本号为M副版本号为m那么这个class文件的版本号就确定为M.m。版本号和Java编译器版本的对应关系如下表所示 Java的版本号是从45开始的JDK 1.1之后每发布一个JDK大版本主版本号向上加1。当虚拟机JDK版本为1.k(k≥2)时对应的class文件版本号的范围为45.0到44k.0之间含两端。字节码指令集多年不变但是版本号每次发布都会变化。
不同版本的Java编译器编译的class文件对应的版本是不一样的。目前高版本的JVM可以执行由低版本编译器生成的class文件可以理解为向下兼容。但是低版本的JVM不能执行由高版本编译器生成的class文件。一旦执行JVM会抛出java.lang.UnsupportedClass VersionError异常。在实际应用中由于开发环境和生产环境的不同可能会导致该问题的发生。因此需要我们在开发时特别注意开发环境的JDK版本和生产环境中的JDK版本是否一致。
上面的ClassFileDemo.class文件使用JDK8版本编译而成第5个字节到第8个字节如下图所示
3.3、常量池存放所有常量
紧跟在版本号之后的是常量池中常量的数量(constant_pool_count)以及若干个常量池表项(constant_pool[])。常量池是class文件中内容最为丰富的区域之一。常量池表项用于存放编译时期生成的各种字面量(Literal)和符号引用(Symbolic References)这部分内容在经过类加载器加载后存放在方法区的运行时常量池中存放。常量池对于class文件中的字段和方法解析起着至关重要的作用。随着JVM的不断发展常量池的内容也日渐丰富。可以说常量池是整个class文件的基石。
1、constant_pool_count常量池计数器 由于常量池的数量不固定时长时短所以需要放置两个字节u2类型来表示常量池容量计数值。常量池容量计数器从1开始计数constant_pool_count1表示常量池中有0个常量项。通常我们写代码时都是从0开始的但是这里的常量池计数器却是从1开始因为它把第0项常量空出来了这是为了满足某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义这种情况可用索引值0来表示。如下图所示
第9个字节和第10个字节表示常量池计数器其值为0x001f换算为十进制为31需要注意的是实际上只有30项常量索引范围是130。
我们也可以通过jclasslib插件来查看常量池数量如下图所示可以看到一共有30个常量。 2、constant_pool[]常量池 常量池是一种表结构从1到constant_pool_count–1为索引。常量池主要存放字面量和符号引用两大类常量。常量池包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项常量的结构都具备相同的特征那就是每一项常量入口都是一个u1类型的标识该标识用于确定该项的类型这个字节称为tag byte标识字节如下图所示 一旦JVM获取并解析这个标识JVM就会知道在标识后的常量类型是什么。常量池中的每一项都是一个表其项目类型共有14种下表列出了所有常量项的类型和对应标识的值比如当标识值为1时表示该常量的类型为CONSTANT_utf8_info。 这14种类型的结构各不相同各个类型的结构如下表所示 根据上表中对每个类型的描述我们可以知道每个类型是用来描述常量池中的字面量、符号引用比如CONSTANT_Integer_info是用来描述常量池中字面量信息而且只是整型字面量信息。标识值为15、16、18的常量项类型是用来支持动态语言调用的它们在JDK7时加入。下面按照标识的大小顺序分别进行介绍。
(1)CONSTANT_Utf8_info用于表示字符常量的值。(2)CONSTANT_Integer_info和CONSTANT_Float_info表示4字节int和float的数值常量。(3)CONSTANT_Long_info和CONSTANT_Double_info表示8字节long和double的数值常量在class文件的常量池表中所有的8字节常量均占两个表项的空间。如果一个CONSTANT_Long_info或CONSTANT_Double_info的项在常量池表中的索引位n则常量池表中下一个可用项的索引为n2此时常量池表中索引为n1的项仍然有效但必须视为不可用的。(4)CONSTANT_Class_info用于表示类或接口。(5)CONSTANT_String_info用于表示String类型的常量对象。(6)CONSTANT_Fieldref_info、CONSTANT_Methodref_info表示字段、方法。(7)CONSTANT_InterfaceMethodref_info表示接口方法。(8)CONSTANT_NameAndType_info用于表示字段或方法但是和之前的3个结构不同CONSTANT_NameAndType_info没有指明该字段或方法所属的类或接口。(9)CONSTANT_MethodHandle_info用于表示方法句柄。(10)CONSTANT_MethodType_info表示方法类型。(11)CONSTANT_InvokeDynamic_info用于表示invokedynamic指令所用到的引导方法(Bootstrap Method)、引导方法所用到的动态调用名称(Dynamic Invocation name)、参数和返回类型并可以给引导方法传入一系列称为静态参数(Static Argument)的常量。
这14种表或者常量项结构的共同点是表开始的第一位是一个u1类型的标识位(tag)代表当前这个常量项使用的是哪种表结构即哪种常量类型。在常量池列表中CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。这14种常量项结构还有一个特点是其中13个常量项占用的字节固定只有CONSTANT_Utf8_info占用字节不固定其大小由length决定。因为从常量池存放的内容可知其存放的是字面量和符号引用最终这些内容都会是一个字符串这些字符串的大小是在编写程序时才确定比如定义一个类类名可以取长取短所以在代码源文件没编译前大小不固定代码源文件编译后可以通过utf-8编码知道其长度。
常量池可以理解为class文件之中的资源仓库它是class文件结构中与其他项目关联最多的数据类型后面讲解的很多数据结构都会指向此处也是占用class文件空间最大的数据项目之一。
Java代码在进行javac编译的时候并不像C和C那样有“连接”这一步骤而是在虚拟机加载class文件的时候进行动态链接。也就是说在class文件中不会保存各个方法、字段的最终内存布局信息因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址也就无法直接被虚拟机使用。当虚拟机运行时需要从常量池获得对应的符号引用再在类创建时或运行时解析、翻译到具体的内存地址之中。本章先弄清楚class文件中常量池中的字面量符号引用。
(1)字面量和符号引用。常量池主要存放两大类常量字面量和符号引用。字面量和符号引用的具体定义如下表所示 字面量很容易理解例如定义String str “xiaoyang”和final int NUM 10其中atguigu和10都是字面量它们都放在常量池中注意没有存放在内存中。符号引用包含类和接口的全限定名、简单名称、描述符三种常量类型。
①类和接口的全限定名:com/yung/ClassFileDemo就是类的全限定名仅仅是把包名的“.”替换成“/”为了使连续的多个全限定名之间不产生混淆在使用时最后一般会加入一个“;”表示全限定名结束。②简单名称:简单名称是指没有类型和参数修饰的方法或者字段名称。③描述符描述符的作用是用来描述字段的数据类型、方法的参数列表包括数量、类型以及顺序和返回值。
(2)常量解读针对ClassFileDemo.class文件我们解读其中的常量池中存储的信息。首先是第一个常量其标识位如下图所示
其值为0x0a即10查找表可知其对应的项目类型为CONSTANT_Methodref_info即类中方法的符号引用其结构如下图所示 可以看到标识后面还有4个字节的内容分别为两个索引项如下图所示 其中前两位的值为0x0006即6指向常量池第6项的索引后两位的值为0x0013即19指向常量池第19项的索引。至此常量池中第一个常量项解析完毕。再来看下第二个常量其标识位如下图所示 标识值为0x09即9查找表可知其对应的项目类型为CONSTANT_Fieldref_info即字段的符号引用其结构如下图所示 同样后面也有4字节的内容分别为两个索引项如下图所示 同样也是4字节前后都是两个索引。分别指向第5项的索引和第20项的索引。后面常量项就不一一去解读了这样的class文件解读起来既费力又费神还很有可能解析错误。我们可以使用“javap -verbose ClassFileDemo.class”命令去查看class文件如下图所示 可以看到常量池中总共有30个常量项第一个常量项指向常量池第6项的索引以及指向常量池第19项的索引第二个常量项指向常量池第5项的索引和指向常量池第20项的索引。和我们上面按照字节码原文件解析结果一样。虽然使用javap命令很方便但是通过手动分析才知道这个结果是怎么出来的做到知其然也知其所以然。
3.4、访问标识
常量池后紧跟着访问标识。访问标识(access_flag)描述的是当前类或者接口的访问修饰符如public、private等标识使用两个字节表示用于识别一些类或者接口层次的访问信息识别当前Java源文件属性是类还是接口是否定义为public类型是否定义为abstract类型如果是类的话是否被声明为final等。访问标识的类型如下表所示
比如当标识值为0x0001的时候访问标识的类型是public。