JVM中class文件探索与解析
一直想成为一名优秀的架构师的我,转眼已经工作快两年了,对于java内核了解甚少,闲来时间,看看JVM,吧自己的一些研究写下来供大家参考,有不对的地方请指正。
废话不多说,一起来看看JVM中类文件是如何加载和运行的。
(1)首先,编写简单代码,对其编译生成的class文件进行研究,其java代码如下:
public class test {
private static int count = 0;
public static void recursion(){
count++;
recursion();
} public static void main (String args[]){
try {
recursion();
}catch (Exception ex){
System.out.print("deep of callings:"+count+"\n");
ex.printStackTrace();
}
}
}
编译之后,用WinHex软件打开其class文件,可以看到其编译的十六进制文件如下:
按照上图分析,开头的前4个字节,是魔数(类似于拼音“咖啡宝贝”),它的用处是标识该文件是否能被java虚拟机识别;
紧接着魔数的4个字节,前两字节0x00代表次版本号(小数点之后的数字),后两字节0x0033代表是class文件的主版本号,换算成十进制是51,标识是JDK1.7可识别的版本(不同的版本可以查看class文件版本号表如下:)
版本号 | 对应十进制 | jdk版本号 |
---|---|---|
2E | 46 | jdk1.2 |
2F | 47 | jdk1.3 |
30 | 48 | jdk1.4 |
31 | 49 | jdk1.5 |
32 | 50 | jdk1.6 |
33 | 51 | jdk1.7 |
34 | 52 | jdk1.8 |
在主版本号字节之后的是常量池,可以理解为Class文件的资源仓库,存储着与class文件相关的数据项。由于不同class文件,常量池数量不同,常量池入口放置两个字节的数据(0x0028)为常量池计数器。十六进制的0x0028为十进制的40(地址偏移量),代表常量池中有39个常量,索引范围为1-40(注:java仅限于class文件结构中容量计数器是从1开始的,java的设计者将索引0拿出来是有特殊考虑的,用来表示不引用任何一个常量池中的项)。
常量池中,存放两类数据:(1)字面量:可以理解为java中的常量,例如:字符串、final修饰常量等。
(2)符号引用:主要包括①类、接口的全限定名②字段的名称和描述符③方法的名称和描述符
在常量池里,存储常量结构如下:u1(常量标志位,用于指明常量的类型,可以查看如下常量池项目类型对应表)+常量信息
让我们以上述class文件为例,索引为1的常量标志位是0x0A(十进制为10),对应上表中的CONSTANT_Methoddef_info类型的常量,参考常量结构表如下图(在jdk1.7中新增了tag=15/16/18的常量类型,更好的支持动态语言的调用,此处就不列举了),
该class文件中,常量池里索引为1的常量(const#1),项目类型标识符为0x0A,二进制为10,查询上表,代表着类方法的符号引用。紧接着两个u2字符代表该常量的信息内容,其中方法描述符0x0006为#6常量,名称及类型描述为0x001A指向#26常量。
紧跟其后的是索引为2的常量(const#2),其标志符为0x09(十进制为9),是字段的符号引用,紧接着的两个u2字符代表其引用索引ID,方法的类描述符指向#27常量,字段描述符指向#28常量;
分析了以上两个字节之后,这里就不一一分析后面的常量了,有兴趣的可以自己分析下。其他的常量池用jdk自带的javap进行生成,在windows中打开cmd(安装jdk并配置环境变量),输入:javap -verbose class文件路径,可以看到编译之后的常量池如下:
将上述class文件中常量池部分标记图如下,红色框代表一个常量池中的项,依次编号为1-39,
我们将上图和javap生成的常量内容对比一下,以const#9为例,#9项为:0x01 (utf8类型) 0x0006(占用字节) 0x3C 0x69 0x6E 0x69 0x74 0x3E(项内容),我们对项内容进行在线转换,将十六进制转换ASCII码值,得到该常量表示:<init>,如下图:
与javap生成的常量文件对比,发现两者完全一致。对字节码有兴趣的朋友可以逐个试一试。
在常量池区域结束之后,紧接着的一个u2(两个字节)类型的字符代表访问标志,它用于识别类或者接口的访问信息,例如:class是类还是接口,访问是private还是public等。访问标志表如下图:
在上述文件中,访问标志为:,即:0x0021,对照上表,只有ACC_PUBLIC和ACC_SUPER为真,其他几项为假。该类为public 能够使用invoke指令。
跟在访问标志之后的分别是类索引、父类索引。由于java不允许多继承,所以类索引和父类索引是一个u2类型的数据。在上述文件中,类索引为#5常量(TestClass),父类索引为#6常量(java/lang/Object);
紧接着类索引和父类索引的是接口索引信息。在java中一个类可以实现多个接口,所以用u2类型的数据集合来表示接口索引。在接口索引的入口,有一项u2类型的接口计数器,计数器为0表示接口的索引表不占用任何字节。
在接口相关描述信息之后的,是字段表集合,用于描述类或者接口中申明的变量(注:此处的变量是指类或者接口级变量,即类变量或者实例级变量,而不包括方法中的局部变量)。这些字段通常包含哪些信息呢?通常有:字段访问域(private、public、protected等)+是实例变量还是类变量(Static)+是否可修改(final)+并发可见性(volatitle,用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最终的值)+可否序列化(transient)+字段类型(基本类型、对象、数组)+字段名称。在JVM中,字段表结构如下:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
我们来看,字段表集合的入口u2类型数据项是字段表的容量计数器为(0x0001),表示只有一个字段项。紧接着字段表容量计数器的u2类型的数据为0x0002,参考下表,表示该字段为private。字段名称
为0x0007,其值为“m”,描述信息0x0008,其值为“I”,可以推断,原代码的定义字段为:“private int m”;
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x00 01 | 字段是否为public |
ACC_PRIVATE | 0x00 02 | 字段是否为private |
ACC_PROTECTED | 0x00 04 | 字段是否为protected |
ACC_STATIC | 0x00 08 | 字段是否为static |
ACC_FINAL | 0x00 10 | 字段是否为final |
ACC_VOLATILE | 0x00 40 | 字段是否为volatile |
ACC_TRANSTENT | 0x00 80 | 字段是否为transient |
ACC_SYNCHETIC | 0x10 00 | 字段是否为由编译器自动产生 |
ACC_ENUM | 0x40 00 | 字段是否为enum |
通常而言,在字段描述之后还有一些属性表信息存储额外的信息,在以上class文件中,属性计数器位0x0000,表示没有额外的属性信息。
在字段表之后的是方法表集合,其表示方法与字段信息表几乎一致。其结构如下表:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
但是,方法表的修饰属性比字段表要多,例如:方法有abstract、synchronize等。在方法表的入口,同样也有一个方法容量计数器,占用u2字节。在上述class中,方法的计数器为,即0x0003表示有三个方法。由上图可以看出,方法的前4个u2位分别为:访问标志、方法名索引、描述索引和属性数量。接在属性数量之后的,即为属性表集合。
我们来分析一下,第一个方法,function#1的第一、二、三、四u2数据项分别为:0x0001、0x0009、0x000A、0x0001,代表方法为public、方法名指向const#9("<init>")、方法描述为const#10(“()V”)、含有一个属性。然后我们看一下属性表,
属性表的头两位为属性名称索引,即0x000B(二进制11)该属性指向const#11(“Code”)属性,java呈现方法体重的代码经过javac编译之后,就存储在Code属性里。
(根据上表结构)接着的4个u2类型为属性长度,即,(换算成二进制47),表示属性长度为47。下图标红部分,即为该方法中属性的长度
。
表示属性即为之后的47个u1字节,
在属性长度之后的,两个u2类型的项分别是:操作数栈的最大深度和局部变量表存储的空间(最小单位为Slot)。对应class文件中的均为:0x0001,表示操作数栈的最大深度和局部变量表存储的空间均为1。之后,有一个u4类型的项0x00000005(二进制为5),表示字节码长度为5。在字节码长度之后紧跟的便是字节码,该项的长度与字节码的长度(code_length)完全一致,上述class中字节码长度为5个u2类型的项,对应0x2A 0xB7 0x00 0x01 0xB1,这里解释下以上字节码的含义。(关于字节码指令可以查看字节码指令表,由于项太多就不一一列举了)。
0x2A——对应指令aload_0,意思是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
0xB7——对应指令inbokespecial,意思是将以栈顶的reference类型的数据所指向的对象作为接收者,调用该类型的实例构造方法。
0x00 01——对应指令invokespecial的参数,差的常量池0x0001对应的const#1对应的<init>方法的符号引用。
0xB1——指令为return,意识是返回此方法,并且返回值为void;当次行命令执行之后,方法执行结束。
在方法字节码之后,便是异常表exceptions_table。在异常表的入口,有一项u2类型的数据,代表异常表的长度,对应上述class文件中的0x0000 ,表示该Code中没有异常表,
接在Code的异常表之后的u2项表示属性表,上述class中为0x0002(换算成十进制=2),表示该方法的Code属性中有两项Code属性,它是class文件中最重要的一个属性。如果把java代码分为方法体中的代码和元数据两部分,Code属性用于秒速代码,所有其他属性用于描述元数据。我们来看第一个Code属性,入口u2项为0x000C(换算成十进制的12),我们查看常量池中const#12代表LineNumberTable。这个属性是用于描述java源码和字节码行号之间的对应关系。若没有改项,程序编译不会出错,但是在抛出异常时,无法知道异常所在的行号。接着分析,我们来看一下LineNumberTable属性的结构:
根据上表,我们看一下接下来的u4项为:0x00000006,表示该属性的长度为6,即0x00 0x01 0x00 0x00 0x00 0x04,接下来的u2项0x0001表示有一个类型为line_number_info的集合。line_number_info为startpx和line_number两个u2类型的项,前者表示字节码起始行号,后者表示java行号,一一对应。分别对应class文件中的0x0000 和0x0004,表示字节码中0行和java代码中4行对应。(PS前三行为注释,字节码不翻译,所以对应关系是正确的),所里这里便是Code的第一个属性——LineNumberTable属性。
在之前的分析中,Code应该包含两个属性。我们接着来看看Code的第二个属性,我们看下第二个属性的入口u2项,0x000D(对应十进制的13),还是跟之前一样,查询常量池中const#13对应的项,我们可以看到便是LocalVariableTable属性。这个属性作用是什么呢?它是用于描述栈帧中局部变量表中的变量与java源码中定义变量之间的关系的。我们来看一下它的属性结构,如下图:
前面的u2和u4项分别代表其属性对应的索引和属性字节长度。索引为13,长度为0x0000000C,表示字节码有12个u1项。即为图中标出的12项,
在这12项中,头u2项为0x0001,表示local_variable_info_table的长度为1,我们看一下local_variable_info_table的结构,如下图
其中,start_pc和length分别代表了这个局部变量的生命周期字节码起始位置和其覆盖范围长度,对应的起始位置字节偏移量和覆盖长度分别为0x0000 和0x0005;name_index和descriptor_index都是指向常量池中的索引,代表了这个局部变量名称和其描述符,对应的是0x000E(14,对应常量池中的this)和0x000F(15,对应常量池中的LTestClass)。index表示这个局部变量在栈帧中的Slot位置,0x0000表示从占用index=0的Slot位置,这与之前this占用index=0的理论一致。
好了,到目前位置,我们已经分析过class文件中的一个方法表了,由于方法表结构复杂,剩下的两个就不一一分析了,下面我把另外两个方法所占用的字节给大家标注一下,大家有兴趣的可以自己研究、自行分析下。
上图中,以垂直分割线分开的即为一个方法描述,可以自行分析下。通过javap命令生成的以上方法截图如下:
接着方法表后面的,即为class文件SourceFile属性。其结构如下图:
入口0x0001,表示有一个SourceFile属性,接下来的u2、u4和u2项(0x0018、0x00000002、0x0019)转换成十进制为(24,2,25),对应着属性名索引、属性长度和sourcefile的索引,即为SourceFile TestClass.java,完全与java代码一致。
自此,关于class的文件解析工作就结束了,文中有不对的地方,欢迎各位大神批评指正。(文中结构表均为粘贴,如若转发,请标明出处!)
JVM中class文件探索与解析的更多相关文章
- JVM中class文件探索与解析(一)
一直想成为一名优秀的架构师的我,转眼已经工作快两年了,对于java内核了解甚少,闲来时间,看看JVM,吧自己的一些研究写下来供大家参考,有不对的地方请指正. 废话不多说,一起来看看JVM中类文件是如何 ...
- MFC Wizard创建的空应用程序中各个文件内容的解析
创建的MFC应用程序名为:wd,那么: 一.wd.h解析 // wd.h : main header file for the WD application // #if !defined(AFX_W ...
- JVM中 Class 文件分析
Java 虚拟机中定义的 Class 文件格式.每一个 Class 文件都对应着唯一一个类 或接口的定义信息,但是相对地,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过 类加载器直接生成). ...
- 深入解析多态和方法调用在JVM中的实现
深入解析多态和方法调用在JVM中的实现 1. 什么是多态 多态(polymorphism)是面向对象编程的三大特性之一,它建立在继承的基础之上.在<Java核心技术卷>中这样定义: 一个对 ...
- 在k8s中收集jvm异常dump文件到OSS
现状 加参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=logs/test.dump 可以实现在jvm发生内存错误后 会生成dump文件 方便开 ...
- FFmpeg中HLS文件解析源码
不少人都在找FFmpeg中是否有hls(m3u8)解析的源码,其实是有的.就是ffmpeg/libavformat/hlsproto.c,它依赖的文件也在那个目录中. 如果要是单纯想解析HLS的话,建 ...
- Json文件放入Assets文件,读取解析并且放入listview中显示。
package com.lixu.TestJson; import android.app.Activity; import android.content.Context; import andro ...
- js上传文件带参数,并且,返回给前台文件路径,解析上传的xml文件,存储到数据库中
ajaxfileupload.js jQuery.extend({ createUploadIframe: function(id, uri) { //create frame var frameId ...
- java代码中fastjson生成字符串和解析字符串的方法和javascript文件中字符串和json数组之间的转换方法
1.java代码中fastjson生成字符串和解析字符串的方法 List<TemplateFull> templateFulls = new ArrayList<TemplateFu ...
随机推荐
- spring与mybatis整合(扫描Mapper接口)
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" ...
- TestNG中DataProvider的用法一
目录 为什么要使用DataProvider DataProvider的常规用法 带Method参数的DataProvider 带ITestContext的DataProvider DataProvid ...
- PHP7安装mysql扩展
1.下载mysql扩展 http://git.php.net/?p=pecl/database/mysql.git;a=summary 2.解压后并使用phpize工具初始化(phpize一般在php ...
- +CIMG+彩色图片边缘提取实验记录_canny/hough transfrom
前言: 书到用时方恨少 正文: 边缘提取技术一直都有接触,最通用的莫过于拉普拉斯,sobel几个算子,两个算子都可通过简单的模板运算进行,而现在比较好的一个边缘提取技术是canny,文章中我是用的ca ...
- 【iOS】[[NSBundle mainBundle] loadNibNamed:nibTitle owner:self options:nil] 异常
这两天照书上的例子写代码时,出现了这个异常. 上网查了不少,有人说链接失效什么的……但发现都不是那些原因,问题出现在下面这句代码: [[NSBundle mainBundle] loadNibName ...
- 常用GDB命令行调试命令
po po是print-object的简写,可用来打印所有NSObject对象.使用举例如下: (gdb) po self <LauncherViewController: 0x552c570& ...
- 真千兆路由的极限之OPENWRT MAKE, 某品牌白菜价QCA9558/QCA9880/QCA8337N纯种组合OS搭建时记
自从上次仙人梦里放了一张无字天书,解惑了WPR003N的秘诀后,渐渐的,就忘了这件这事情,连想好的评测都拖延了好多月了,毕竟路由是拿来用的,不是用来写什么陈词滥调的评测的,无意间,热爱白菜的我发现了一 ...
- 多线程编程(Linux C)
多线程编程可以说每个程序员的基本功,同时也是开发中的难点之一,本文以Linux C为例,讲述了线程的创建及常用的几种线程同步的方式,最后对多线程编程进行了总结与思考并给出代码示例. 一.创建线程 多线 ...
- Java集合系列(一)List集合
List的几种实现的区别与联系 List主要有ArrayList.LinkedList与Vector几种实现. ArrayList底层数据结构是数组, 增删慢.查询快; 线程不安全, 效率高; 不可以 ...
- pytest
pytest可以生成多种样式的结果:1.生成JunitXML格式测试报告:命令: --junitxml=path(相对路径)2.生成result log 格式的测试报告: 命令:--resultlog ...