上一篇看得懂的字节码讲了怎么看字节码,以及字节码中的魔数、版本号和常量池部分,这篇文章接着字节码顺序往下将。请必定要看了上一篇以后再看这一篇,由于有绝对的逻辑关系,不看上一篇这篇基本看不懂。java
若是这篇文章中有我没有讲到怎么出现的东西,(好比程序的代码,javap工具,查看字节码的工具等)就是我在上一篇文章中已经提到过的,能够查看个人上一篇文章来找到答案。git
上一篇地址 能看懂的字节码-上github
个人全部文章同步更新与Github--Java-Notes,想了解JVM,HashMap源码分析,spring相关能够点个star,剑指offer题解(Java版),设计模式。能够看个人github主页,天天都在更新哟。spring
邀请您跟我一同完成 repo设计模式
紧接着常量池后面的两个字节(u2类型)就表示访问的标志,在字节码中是这两个数数组
那么他表明什么呢?他表示这个类或者接口声明为 publicide
咱们看他的全部含义的表 函数
21就是 ACC_PUBLIC 和 ACC_SUPER 这两个标志值相或的结果。即 0x0001 | 0x0020 = 0x0021。工具
而咱们的代码中为源码分析
public class TestClass {
.......
}
复制代码
你能够试下将它声明为 private 或者其余状况是什么值
这三个很是相似,因此放在一块儿讲。类索引和父类索引都是u2的数据。接口索引是一组u2类型的数据。由于Java只能单继承,可是能够实现多个接口
咱们回到字节码文件,接着上次看到的 0x0021.
后面是 0x0003, 0x0004,0x0000。这些啥意思呢?
刚刚说了指向CONTANT_Class_info,而这个class_info 存了全限定名的信息,结构以下
在上一篇我也说了,由于他已经肯定了类型,因此是没有tag的位置的,就只有后面的index。因此0x0003表示它指向 第三个常量。第三个常量是什么呢?
咱们回到命令行的内容(这个在上篇介绍过怎么打开的),
第三个常量指向第17个常量,第17个常量是一个 uft8类型的。他的信息为·com/swu/leosanqing/TestClass
。这个不就是该类的名称吗?
同理,咱们找到 0x0004指向的含义,他是指向第18个常量,第18个常量也是一个utf8类型的数据,所存储的信息为 java/lang/Object
,这个类不就是咱们TestClass的父类吗/
0x0000 表示这个类没有实现接口(或者这个接口没有继承其余接口),因此这三个就已经解析完了。
字段表(field_info)用于描述接口或类中声明的变量,字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量
而这个field_info是一个这样的结构
他跟以前的类的访问标志很像,都是u2类型的数据,可是有一些差异
若是是相同的话,那么他们的标志值是如出一辙的
紧随access_flags后面的两个是 name_index和descriptor_index,这两个分别表示字段的简单名称以及字段和方法的描述符。
简单名称是指没有类型和参数修饰的方法或者字段的名字,好比咱们定义的方法
public class TestClass {
private int m;
public int inc(){
return m+1;
}
}
复制代码
那么简单名称就是 "inc"和"m".等会儿你跟着我翻译一下字节码就知道了
字段和方法的描述符稍微复杂一些,他的做用是用来描述字段的数据类型、方法的参数列表(包括数量、类型和顺序,看到这三个有没有想到啥?没错,重载!等下就知道重载为啥要三个不同)和返回值。
根据描述符规则,基本数据类型(byte,short,int…….)以及表明无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L+对象的全限定名来表示。
对于数组来讲,每个字符将使用一个前置的"["字符来描述,
下面我来分别举个例子
/* 若是我要表示一个整形数组 int[] 那么就应该表示为 [I 若是我要表示一个二维的数组char[][] 那么就应该表示为 [[C 若是我要表示一个二维的字符串数组String[][] 那么就应该表示为 [[Ljava/lang/String; 注意有分号 由于String不是基本类型也不是void类型,因此要用对象类型来描述 因此他就应该用字符L+String类的全限定名来表示 若是我要表示一个自定义的Person类,他在包 com.swu.leosanqing下 那么就应该表示为 Lcom/swu/leosanqing/Person */
复制代码
若是描述符(descriptor_index)要描述一个方法的话,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号"()"中。
下面我举几个例子
/* 先拿本程序举例,他有一个 方法 public int inc(){ return m+1; } 那么就应该表示为 "()I" 假设 Person类 在包 "com.swu.leosanqing"下 若是一个方法为 public Person (char[] source,int[] a,char[][] c,int b,int c,String d). 那么他就应该表示为 ([C[I[[CIILjava/lang/String;)Lcom/swu/leosanqing; */
复制代码
这个后面的方法表集合中会用到
咱们接着类索引、父类索引和接口索引往下看。
咱们对着下面的表进行一一对应,他前面还有一个 field_count
0x0001对应field_count
表示只有一个参数,由于代码中咱们就只定义了一个 private int m
0x0002对应access_flags
,而后咱们查上面的表,它的意思为 字段是private
而后咱们再看 0x0005,它对应的是u2类型的 name_index
,咱们应该找常量池中的第5个参数,咱们经过命令行中的找,他的名字是 "m"
而后咱们再往下走,0x0006 ,它对应descriptor_index
,而后咱们去常量池中寻找第6个常量,他是"I",而后后面两个字节是0x0000,表示没有属性值。若是个人程序是 private int m = 123;
那么后面的属性值就应该有表示的值
因此看下来,就是 private int m,跟咱们程序中定义的是如出一辙的,代表咱们的解读正确。
Java中没有用过字段的重载吧?也就是说咱们不能在程序中这样定义。
private int m;
public String m;
// 咱们都知道这样作ide会报错,两个字段的数据类型、修饰符不论是否相同
// 都必须使用不同的名字
//可是在字节码中就不同,若是两个字段的描述符不一致,那么他是能够重名的,且是合法的
复制代码
下一个小节将方法的重载,这个应该就稍微清晰一点
若是你理解了上一小节的内容,那么这一小节将会比较好懂,由于两个都差很少。
他就是用来表示字节码中的方法的各类数据项
你看这个表是否是很眼熟,和上一小节的字段表结构很是像?
访问标志也很像不过有些增减,好比加了方法的修饰符 native 的标志,删去了字段的 volatile 标志
有没有发现,方法表结构并无表示或者存放代码的内容,那么他去哪里了呢?他存放到方法表中的属性表里的 "Code"属性里。我会在下一节中讲到。
咱们先看下咱们的方法对应字节码中的是哪些东西。这个恰好是接着上一节的字段表的位置开始的
跟上一节的字段表同样,首先他有一个表示方法多少的计数器"methods_count",而后才是方法表(method_info)里面的数据,因此 0x0002表示里面有两个方法。
你可能疑惑了,我只定义了一个inc()方法啊,怎么会有两个呢?别忘了,咱们还有一个默认的构造函数。因此有两个
而后咱们再对应方法表来看
咱们再看下一个 0x0001,它对应access_flags
,咱们查方法访问标志表,发现他是一个public
而后咱们再看下一个0x0007,他对应name_index
,咱们去常量池找第七个常量,发现他是 <init>
,这个表示实例构造器.
而后看下一个 0x0008,它对应描述符的索引(descriptor_index),去常量池找,发现他是()V,、
再看下一个 0x0001,它对应属性值的的个数(attributes_count),说明有1个。
再看下一个 0x0009,他对应属性值(attributes),咱们查看常量池发现他是表示"Code",Code也是一个复合表,他的各项定义在下一节中会讲到。
因此综合起来这些字节码就表示 有两个方法,其中一个是构造函数public void
那咱们定义的public int inc()
方法哪去了?这两个离的比较远,由于里面还有其余的东西。这些东西是啥,下节讲属性表会介绍
根据刚刚分析第一个方法的方法,咱们能够得出,解析出来是public int inc()
,彻底是咱们定义的方法。
Java中,方法的重载只要知足是三个条件的其中一个就行:参数个数不一样,参数类型不一样,参数顺序不一样。
那是由于Java代码中要重载一个方法,除了要有相同的名字以外,还得有不一样的特征签名。这个特征签名只包括了方法名称、参数顺序、参数类型。
可是字节码中,特征签名不只包括上面的部分,还要包括返回值和受查异常表,若是仅仅只是Java代码部分的特征签名相同,可是返回值或者受查异常表不一样,那么字节码也是容许那样的同名方法。
属性表(attribute_info)在以前的小节中出现了屡次,它是用来描述某些场景的专有信息
好比咱们以前说的Code,还有在字段表小节说的假如 咱们有一个代码是private int m =123;
,属性表就存123
的值。
虚拟机规范预约义了很是很是多属性
对于每个属性,他的名称须要从常量池中引用一个CONSTANT_Utf8_info
类型的常量来表示,而这个属性的结构彻底是自定义的,只须要经过一个u4的长度属性去说明属性值所占用的位数便可。
咱们来看Code属性表结构(不是每个方法表都有Code属性,好比接口和抽象类中的方法就没有这个)
因为这是Code表结构,并且以前的方法表已经记录了Code前两个数据项的内容,因此属性值的长度固定为整个属性表长度减去6个字节,即减去 (u2+u4)
咱们先看他的范围
前面的4个字节(0x0000001D)对应attribute_length
,表示这个Code属性表一共有多长,总共占用多少个字节。咱们换算成十进制,就是29,咱们数一下他后面的,恰好是29个字节。从 03后面就是第二个方法的存储位置了,恰好对应上一节的第二个方法的起始位置,能够翻回去验证一下(不过以前的红线少画了两个字节,分析的时候加上了)
后面的 0x0001,0x0001,0x00000005分别对应 max_stack
,max_local
,code_length
.
后面的 2A B7 00 01 B1
分别就是字节码指令了。咱们来翻译一下
invokespecial
,这条指令的做用是以栈顶的reference 类型的数据所指向的对象做为方法接受者,调用此对象的实例构造器方法、private方法或者他的父类的方法。这个方法有一个u2类型的参数说明具体调用哪个方法,他指向常量池中的一个CONSTANT_Methodref_info 类型常量,即次方法的方法符号调用(就是下一个参数)invokespecial
的参数,查常量池得 0x0001对应的常量为 实例构造器"<init>"
方法的符号引用咱们经过上述的字节码指令能够看出,虚拟机的这个执行过程是基于栈的,因此咱们能够初步推测,Java虚拟机执行字节码是基于栈的体系结构
后面的那些字节码是什么呢?
00 00 00 01 00 0A 咱们接着看code表后面的内容
他的意思分别是 exception_table_length参数为0,attribute_count参数为1,0A表示指向常量池的第10个常量,也就是LineNumberTable。
咱们来看LineNumberTable的结构
他跟属性表结构CONTANT_Utf8_info差不太多,因此咱们能够省掉第一个,由于已经知道他是这个名字了,而后再接着上面的字节码,解析一下他们分别表示啥。
首先他有一个u4类型的数据表示长度,因此 0x00000006就表示后面的6个字节用来描述这个属性。后面恰好只有6个字节。
而后一个 u2类型的 line_number_table_length这个就表示有一个行号表,行号表只有两个u2类型的数据,start_pc和line_number,前者表示字节码行号,后者表示Java源码行号,因此分别是 0和3
经过命令行的结果来验证咱们是否解析的正确
咱们看到和咱们的解析结果一致。因此咱们的解析过程是正确的
后面的第二个方法你能够按照个人思路本身解析一下,应该没有太大的问题。
至此字节码基本已经解析完
经过两篇文章咱们知道了字节码文件中有哪些东西,又该怎么去看字节码文件。
Class文件中一共就只有两种数据类型
Class文件中一共有哪些东西呢?大的分类一共有下面几种
固然我这个程序只是很是简单的程序,还有不少内容用不到,好比异常、接口、同步锁等,可是已经能够将整个Class串起来了,若是你想验证,能够根据个人方法,自行验证