c语言会生成class文件,一文带你刨析class文件
Write Once , Run AnyWhere
java如何实现跨平台?
为啥C程序不能跨平台?
hello.cpp#include
int main()
{
printf("Hello Worldn");
return 0;
}
下面是这个c程序的执行过程
预处理会将hello.cpp文件中通过include引入的头文件插入到程序文本中,得到hello.i文件
编译器将hello.i中的C代码翻译成汇编指令,得到文件hello.s
汇编器将hello.s文件中的汇编指令翻译成机器指令,并且将其成一种可重定位目标程序的格式,然后将结果保存在目标文件中hello.o
在程序中我们使用printf函数,而这个函数存在于名字为printf.o的单独的预编译好的目标文件中,链接器负责将这个目标文件中机器指令与hello.o文件中的机器指令合并。结果得到一个可执行的目标文件,它被加载入内存后,就可以被执行了!
看到这里有没有发现,我们编写的C程序最终是直接被转换成机器码保存在文件中,然后被加载到内存中CPU取指执行。这就意味如果我将在intel架构下编译的C执行文件拿到AMD架构的计算机上面执行,就会出错;因为机器指令是CPU直接识别的指令,而按照intel指令集生成的机器指令你让AMD处理器去取指执行它,肯定会出错呀!
跨平台的java
从上面C程序的执行过程我们也看出,C程序之所以不跨平台是它可执行文件中的机器指令按照当前系统上CPU的指令集生成的。那么要实现跨平台,我们是不是可以设立一个规范,程序不直接编译为机器指令,而是编译成这个规范规定的形式保存在本地,然后在程序执行时由一个媒介根据不同的CPU指令集将这个规范转换为机器指令运行。
这里我所说的规范就是class文件格式,而媒介就是JVM或者也可以说是字节码指令引擎。下图是java源程序编译执行的大概过程,hello.java最终编译成hello.class文件保存在本地,然后被JVM加载入内存翻译或者是编译执行。
可以看出classs文件就是java实现平台无关性的关键,下面让我们一起揭开class文件的神秘面纱吧!!!
class文件
一图胜过千言
class文件的数据类型
根据《java虚拟机规范》的规定,Class文件格式采用一种类似于C语言的伪结构来存储数据,这种伪结构中只有俩种数据类型 : 无符号数和表无符号数属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节,8个字节的无符号数,无符号数可以用来表示数字,UTF8编码的字符串,引用等。
表是由多个无符号数或者是其他表作为数据项组成的符合数据类型,比如常量池表中的表项就是各种表,所有表的命名以_info结尾
class文件结构
在cmd中输入javap -verbose class文件路径查看class文件内容(非二进制)。
使用sublime打开class文件可以查看二进制内容。
魔数(magic)
class文件中的头四个字节被称为魔数,它的作用就是标识这个文件是一个可以被JVM接受的文件。Class文件的魔数是 “OXCAFEBABE”,一个具有浪漫气息的值。
用sublime打开一个class文件看一下cafe babe 0000 0034 0118 0a00 3c00 890a
看一下反编译结果minor version: 0
major version: 52
class文件的版本号
紧接着魔数的4个字节存储的就是class文件的版本号:其中第5,6个字节标识次版本号,第7,8个字节标识主版本号。java实现良好的向后兼容性也依赖于class中的版本号,《java虚拟机规范》规定即使class文件的格式没有发生变化,JVM也必须拒绝执行超过其版本号的Class文件。
Java的主版本号从45开始,从上面我贴出的部分class文件内容可以看出我本机是JDK8即主版本号是52
Java次版本号在Java2前被使用过,在java2到java12中均未被使用,全部固定是零。直到JDK12中重新使用来公测一些新的特性。不知道大家在初学java的时候,有没有经历辛辛苦苦在网上下载了jar包导入后console却疯狂输出啥 unsupport version 52.0之类的堆栈异常,还记得我当时疯狂问度娘,下载了一大堆jar包一个一个的试,解决后也是迷迷糊糊的。后来学习JVM相关内容的时候才恍然大悟(笨的离谱)
常量池
紧接着主次版本号后面就是常量池入口,常量池可以认为是class文件中的资源仓库,因为class文件中的其他项目几乎都和它有关联。在常量池前面有一个计数器,(constant_pool_count)来表示常量池中有多少个数据项。
常量池中主要存放俩大常量:字面量和符号引用
字面量:字面量就相当于是常量,例如String s="sheledon"中的"sheledon"就是一个字符串常量,还有被关键字final修饰的常量值等
符号引用:符号引用是编译原理方面的概念,它主要的意义就是通过符号无歧义的定位一个目标。在常量池表中主要包括以下内容类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
被模块导出或者开放的包
方法句柄和方法类型
下面看一下常量池的内容public class Test extends Random implements Serializable {
private int a;
private static int b;
public static void main(String[] args) {
show();
}
public static void show(){
System.out.println("欢迎来到小白的知识空间");
}
}tip:源文件中的类继承关系纯粹就是为了演示常量池中的内容
常量池Constant pool:
#1 = Methodref #7.#26 // java/util/Random."":()V
#2 = Methodref #6.#27 // cn/sheledon/test/Test.show:()V
#3 = Fieldref #28.#29 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #30 // 欢迎来到小白的知识空间
#5 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #33 // cn/sheledon/test/Test
#7 = Class #34 // java/util/Random
#8 = Class #35 // java/io/Serializable
#9 = Utf8 a
#10 = Utf8 I
#11 = Utf8 b
#12 = Utf8
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcn/sheledon/test/Test;
#19 = Utf8 main
#20 = Utf8 ([Ljava/lang/String;)V
#21 = Utf8 args
#22 = Utf8 [Ljava/lang/String;
#23 = Utf8 show
#24 = Utf8 SourceFile
#25 = Utf8 Test.java
#26 = NameAndType #12:#13 // "":()V
#27 = NameAndType #23:#13 // show:()V
#28 = Class #36 // java/lang/System
#29 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#30 = Utf8 欢迎来到小白的知识空间
#31 = Class #39 // java/io/PrintStream
#32 = NameAndType #40:#41 // println:(Ljava/lang/String;)V
#33 = Utf8 cn/sheledon/test/Test
#34 = Utf8 java/util/Random
#35 = Utf8 java/io/Serializable
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Utf8 java/io/PrintStream
#40 = Utf8 println
#41 = Utf8 (Ljava/lang/String;)V
下面带大家分析一下上面的常量池表,我们不用二进制来分析,用反编译更加的直观。#6 = Class #33 // cn/sheledon/test/Test #7 = Class #34 // java/util/Random #8 = Class #35 // java/io/Serializable
Class是CONSTANT_Class_info型常量,它表示的是类或者是接口的符号引用,它的结构如下类型 名称数量描述u1 tag1标志位区分常量类型值为7
u2 name_index1常量池索引值
tag是一个标志位,用于区分常量类型;name_index则是一个常量池的索引值,指向常量池中的一个CONSTANT_Utf8_info类型的常量。上面索引为6的class中的name_index是33。#33 = Utf8 cn/sheledon/test/Test
Utf8表示这是一个CONSTANT_Utf8_info型的常量,结构如下类型名称数量描述u1tag1标志位区分常量类型值为8
u2length1UTF8编码字符串占用字节数
u1byteslength字符串的字节
由于java源码中方法和字段的名称在编译后都是由CONSTANT_Utf8_info来表示的,所以utf8_info常量的最大长度也就是方法和字段名称的最大长度。#1 = Methodref #7.#26 // java/util/Random."":()V
Methodref表示这是一个CONSTANT_Methodref_info型的常量,结构如下类型名称描述u1tag标志位区分常量类型值为10
u2index指向声明方法的类描述符CONSTANT_Class_info的索引
u2index指向简单名称以及类型描述符CONSTANT_NameAndType_info的索引#3 = Fieldref #28.#29 //java/lang/System.out:Ljava/io/PrintStream;
Fieldref表示这是一个CONSTANT_Fieldref_info型的常量,结构如下类型名称描述u1tag标志位区分常量类型值为9
u2index指向声明字段的类或者是接口的描述符CONSTANT_Class_info的索引
u2index指向字段描述符CONSTANT_NameAndType_info的索引#26 = NameAndType #12:#13 // "":()V #27 = NameAndType #23:#13 // show:()V #32 = NameAndType #40:#41 // println:(Ljava/lang/String;)V
NameAndType表示这是一个CONSTANT_NameAndType_info型的常量,结构如下类型名称描述tagtag标志位区分常量类型值为12
indexindex指向一个字符串常量的索引,表示字段或方法简单名称
indexindex指向一个字符串常量的索引,表示字段或方法描述符
介绍一下方法和字段的简单名称和描述符的概念
简单名称:方法和字段的简单名称就是名称(呃呃呃),例如这个类中的show()方法和静态类变量b的简单名称就是"show"和"b"
描述符;方法和字段的描述符是用来描述字段的数据类型,方法的参数列表和放回值的,根据描述符规则,基本数据类型以及void用一个大写字符代表,例如B代表byte,C代表char,V代表void;而引用类型则用大写字母L+类的全限定名; 对于数组来,每一个维度前面要加'['描述
字段举例:int age; //简单名称:age , 描述符 :I
String gender; //简单名称:gengder, 描述符: Ljava/lang/String
int [] times; //简单名称:times , 描述符: [I
String[][] colors; //描述符 : [[Ljava/lang/String
方法举例
描述符先参数列表后返回值void show() {...} //简单名称: show , 描述符 : V
public static String valueOf(char data[], int offset, int count){...} //描述符: ([CII)Ljava/lang/String
访问标志
在常量池结束后,紧接着就是2个字节表示访问标志(access_flags),这个标志用于识别一些类或者是接口的信息,例如这个Class是接口还是类,是否是public,是否定义为abstract等.
class文件内容:flags: ACC_PUBLIC, ACC_SUPER
标志名称标志值含义ACC_PUBLIC0x0001是否为public类型ACC_FINAL0x0010是否被声明finalACC_INTERFACE0x0200标识接口ACC_ABSTRACT0x0400标识为abstract
类索引,父类索引,接口索引集合
class文件根据这三项数据来确定该类的继承关系. 类索引用于确定这个类的全限定名;父类索引用于确定这个类的父类的全限定名,除了java.lang.Object外所有的类都有父类;接口索引集合用于确定这个class实现的接口,前面会有一个计数器来指明集合的大小.
字段表集合
字段表集合用户描述接口或者是类中声明的变量,这里的变量指的是类变量和实例变量.集合中的每一个表描述了一个变量所包含的信息,例如它的作用域,是否可序列化,可变性,常量值等
结构如下类型名称描述u2access_flags访问标志
u2name_index指向常量池的索引,字段的简单名称
u2descriptor_index指向常量池的索引,字段的描述符
u2attribute_count属性表集合计数器
attribute_infoattributes属性表集合
access_flags取值如下标志名称含义ACC_PUBLICpublic字段
ACC_PRIVATEprivate字段
ACC_PROTECTEDprotected字段
ACC_STATICstatic字段
ACC_FINALfinal常量
ACC_VOLATILEvolatile修饰保障可见性和有序性
ACC_TRANSIENTtransient修饰,不被序列化
ACC_SYNTHETIC标识该字段是否由编译器产生
ACC_ENUM是否是枚举类型
属性表相关内容见下文
需要注意的是字段表集合中并不包含从父类或者是父接口中继承的字段,这些字段在创建对象的时候会被分配空间存放在对象的实例数据中
方法表集合
方法表和字段表结构相同,只不过是在表中的一些表项的取值有所差异,比如访问标志中增加了 ACC_SYNCHRONIZED,ACC_NATIVE等.方法的定义通过access_flags,name_index, descriptor_index描述.而方法的方法体在由javac编译成字节码指令后添加到了方法属性表中的Code属性中.
如果子类没有重写父类的方法,那么父类中的方法就不会出现在子类的字段表集合中,但是可能出现由编译器自动添加的方法,例如类构造方法和实例构造方法.在初学java的时候,老师都会和我们说,如果不显示的在类中编写类的构造方法,那么在创建对象的时候就会调用默认构造方法,这里的默认构造方法就是javac编译器在编译的时候自动为我们添加的.
属性表相关内容见下文
属性表集合
属性表相当于是class文件中的附加信息,方法表和字段表都可以通过携带自己的属性表来描述某些场景的专有信息
下面对属性表中的部分属性做一下简单的介绍属性名称使用位置含义Code方法表方法体中代码编译后的字节码指令
ConstantValue字段表final常量的值
Deprecated类,方法表,字段表被废弃的类,方法,字段
Exceptions方法表方法抛出的异常表
InnerClasss类文件内部类列表
LineNumberTableCode属性java源码行号和字节码指令的对应关系
LocalVariabeTbaleCode属性方法的局部变量描述
SourceFile类文件class源文件的记录
属性表的结构类型名称数量描述u2attribute_name_index1指向常量池索字符串索引,标识属性名称
u4attribute_length1标识属性值所占的长度
infoattribute_length属性值,不同属性自定义Code属性
java方法的方法体中的代码在经javac编译为字节码后保存在code属性中,而抽象方法,接口中的非default和非static方法没有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
看一下下面这个main方法的class文件内容,源码中添加行号来说明LineNumberTable属性7:public static void main(String[] args) {
8: try {
9: Thread.sleep(1000);
10: } catch (InterruptedException e) {
11: e.printStackTrace();
12: }
13:}public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V //方法描述符
flags: ACC_PUBLIC, ACC_STATIC //方法访问标志
Code: //字节码
stack=2, locals=2, args_size=1 //操作数栈最大深度 为2,局部变量大小为2
0: ldc2_w #2 // long 1000l
3: invokestatic #4 // Method java/lang/Thread.sleep:(J)V
6: goto 14
9: astore_1
10: aload_1
11: invokevirtual #6 // Method java/lang/InterruptedException.printStackTrace:()V
14: return
Exception table: //异常表
from to target type
0 6 9 Class java/lang/InterruptedException
LineNumberTable: //指示源码与字节码行号
line 9: 0 //源码第9行对应字节码0
line 12: 6 //源码第12行对应字节码6 return
line 10: 9
line 11: 10
line 13: 14
LocalVariableTable: //局部变量属性
Start Length Slot Name Signature
10 4 1 e Ljava/lang/InterruptedException;
0 15 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 73 /* same_locals_1_stack_item */
stack = [ class java/lang/InterruptedException ]
frame_type = 4 /* same */
下面我着重解释一下异常表
这个异常表是指方法内部try catch会出现的异常,而属性Excetions指明这个方法可能抛出的异常Exception table: //异常表
from to target type
0 6 9 Class java/lang/InterruptedException
上面内容就是在执行索引从0到6的字节码过程中出现了java/lang/InterruptedException异常,那么就跳转到索引为9的字节码处执行.Exceptions属性
与Code属性平级,列举方法中可能爬出的异常
结构如下类型名称数量u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exception_index_tablenumber_of_exceptions
实例Exceptions:
throws java.lang.InterruptedException, java.lang.ClassNotFoundExceptionLineNumberTable 属性
LineNumberTable用来描述java源码中行号与字节码行号(偏移量)之间的关系.它的作用主要就是在堆栈异常中显示出错的源码行号还有在调试程序的时候按照行号设置断点
实例看上面main方法中的注释LocalVariableTable属性
该属性描述栈帧中局部变量表与源码定义变量的关系
结构如下类型名称数量u2attribute_name_index1
u4attribute_length1
u2local_variable_table_length1
local_variable_infolocal_variable_tablelocal_variable_table_length
local_variable_info代表源码中的局部变量,结构如下类型名称数量u2start_pc1
u2length1
u2name_index1
u2descriptor_index1
u2index1
前面讲java虚拟机栈栈帧中的局部变量表说过,为了节省内存空间,局部变量表中的变量槽是根据局部变量的作用域而复用的,而这里的start_pc代表局部变量生命周期开始的字节码偏移量,length代表其作用覆盖的长度.俩者结合起来就是这个局部变量在字节码之中的作用域范围.
最后
以上内容参考 《深入理解java虚拟机》,《深入理解计算机系统》,站在巨人的肩膀上面我们才可以看的更高更远.
c语言会生成class文件,一文带你刨析class文件相关推荐
- linux缓存写入文件,实验5Linux文件操作之带缓存和非缓冲文件的读写
<实验5Linux文件操作之带缓存和非缓冲文件的读写>由会员分享,可在线阅读,更多相关<实验5Linux文件操作之带缓存和非缓冲文件的读写(15页珍藏版)>请在人人文库网上搜索 ...
- linux缓存文件和非缓存文件格式,实验5 Linux文件操作之带缓存和非缓冲文件的读写...
1 实验5Linux 文件操作之带缓存和非缓冲文件的读写 学生姓名:王祥真学号:6103114095专业班级:计科143 实验类型:□ 验证▥综合 □ 设计 □ 创新实验日期:2017.4.20实验成 ...
- linux 页缓存 读写,实验5Linux文件操作之带缓存和非缓冲文件的读写(10页)-原创力文档...
实 验 5 Linux 文 件 操 作 之 带 缓 存 和 非 缓 冲 文 件 的 读 写 学生姓名:王祥真 学号: 6103114095 专业班级: 计科 143 实验类型: □ 验证 ? 综合 □ ...
- R语言从入门到入土--一文带你解锁ggplot2绘图大法
一.准备工作 (1)下载包:install.packages(tidyverse)--注意选择国内源 (2)载入包:library(tidyverse) (3)了解一下本次实验所使用的几个数据包 可以 ...
- 一文带你透析zookeeper原理
文章目录 一:Zookeeper集群组成 1.1 Leader领导者 1.2 Follower跟随者 1.3 Observer观察者 二:zookeeper的数据结构--目录树 2.1 Node节点的 ...
- python-docx中文开发文档_使用Python语言-docx生成Word文档
本文主要向大家介绍了使用Python语言-docx生成Word文档,通过具体的内容向大家展示,希望对大家学习Python语言有所帮助. < 学会来使用python操作数据表和PDF,今天我们尝试 ...
- R语言使用fs包的path_wd函数基于自定义文件路径规则,批量生成多个文件或者文件夹对应的绝对(absolute)文件路径(constructs absolute path)
R语言使用fs包的path_wd函数基于自定义文件路径规则,批量生成多个文件或者文件夹对应的绝对(absolute)文件路径(constructs an absolute path from the ...
- flowable 中文文档_取出word文档文字内容生成加了目录、标号和页码的PDF文件
word文档内的一页: 将文本取出来,生成自定义格式的PDF文件: 从word取出文本时标题的标号和页码是取不出来的,要自己加.另外就是目录也要自己生成和添加: 代码和解释如下: from repor ...
- c语言 自动生成word文件,C#根据Word模版生成Word文件
本文实例为大家分享了C#根据Word模版生成Word文的具体代码,供大家参考,具体内容如下 1.指定的word模版 2.生成word类 添加com Microsoft word 11.0 Object ...
最新文章
- 我的WCF之旅(12):使用MSMQ进行Reliable Messaging(转载)
- 深入ASP.NET数据绑定(中)——数据双向绑定机理
- ALV列(Column)换到行(Row) 之 列上限不固定篇
- C语言 · 比较字符串
- java开发环境及数据类型实验_实验项目1 Java开发环境与语言基础
- [BUGKU][CTF][Reverse][2020] Reverse writeup 1-7 暂时肝不动了
- SpringBoot 使用 log4j2
- 最完整的MySQL规范
- R语言题目及参考答案(3)
- Spring Cloud与微服务学习总结(13)——云原生趋势下,微服务的拆分粒度如何把握?
- 现代通信原理3.3:两个重要的信号处理模块-乘法器与滤波器
- 手把手教你搭建微信小程序服务器(HTTPS)
- PROFINET通信技术总结
- sonar打包出现的问题The forked VM terminated without saying properly goodbye. VM crash or System.exit called
- springboot集成阿里云直播,低延时直播
- 冬季好去处七彩蝴蝶园,温暖如春彩蝶纷飞
- 关于深度学习云服务器推荐
- 林轩田机器学习基石笔记5 - Training versus Testing
- 室内定位系统算法--无线时钟同步的比较
- PWNFEST黑客大会:苹果Safari与微软Edge浏览器均被攻破
热门文章
- yii 执行指定迁移文件_laravel的迁移文件
- 仿腾讯图文轮播.html
- Java中static的作用详解_详解java中static关键词的作用
- php网页留言本过程,PHP实现简单留言本功能代码示例
- 动态分区分配算法代码_【代码】巩敦卫等TEVC论文:基于区间相似度分析的协同动态区间多目标进化优化算法...
- 如何启动一个新的cmd窗口并在其内执行命令
- 韩国各大银行纷纷开始引进区块链技术
- 全息营销话题提纲(2-1)--王甲佳全息营销系列15
- 纳德拉:微软正计划“终极移动设备”
- Gradle-jar-aar