文本已收录至个人GitHub仓库,欢迎Star:github.com/bin39232820…
种一棵树最好的时间是十年前,其次是如今
我知道不少人不玩qq了,可是怀旧一下,欢迎加入六脉神剑Java菜鸟学习群,群聊号码:549684836 鼓励你们在技术的路上写博客java
昨天讲了类加载机制,其实那个应该算是第二步,第一步仍是咱们的.Class文件的结构,可是直接讲这个未免太枯燥,因此我就写讲了类加载机制,再讲文件结构git
咱们知道咱们写完的Java程序通过javac xxx.java编译后生成了xxx.class文件,但是你是否想过xxx.class文件究竟是什么?这个文件中到底包含了什么内容?那么如今咱们就一块儿经过解析一个.class文件来深刻的学习一下类文件结构,经过此次的学习,我想你会对class文件了如指掌。github
在解析一个class文件以前,咱们须要先学习一下Class类文件的结构,这个类文件结构至关于一个总纲,咱们立刻就会对照着这个类文件结构解析真正的class文件。数组
类文件结构图:bash
package temp; public class HelloWorld { public static void main(String[] args) { System.out.println("Hello,World"); } } 复制代码
咱们经过16进制编辑器打开编译后的HelloWorld.class文件,其十六进制的文件内容以下:markdown
对应到class文件中就是:数据结构
紧接着主版本号的就是常量池,常量池能够理解为class文件的资源仓库,它是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,也是class文件中第一个出现的表类型数据项目。并发
因为常量池中常量的数量不是固定的,因此常量池入口须要放置一项u2类型的数据,表明常量池中的容量计数。不过,这里须要注意的是,这个容器计数是从1开始的而不是从0开始,也就是说,常量池中常量的个数是这个容器计数-1。将0空出来的目的是知足后面某些指向常量池的索引值的数据在特定状况下须要表达“不引用任何一个常量池项目”的含义。class文件中只有常量池的容量计数是从1开始的,对于其它集合类型,好比接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。编辑器
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近Java语言的常量概念,如文本字符串、声明为final的常量等。而符号引用则属于编译原理方面的概念,它包括三方面的内容:工具
Java代码在进行javac编译的时候并不像C和C++那样有链接这一步,而是在虚拟机加载class文件的时候进行动态链接。也就是说,在class文件中不会保存各个方法、字段的最终内存布局信息,所以这些字段、方法的符号引用不通过运行期转换的话没法获得真正的内存入口地址,虚拟机也就没法使用。当虚拟机运行时,须要从常量池得到对应的符号引用,再在类建立时或运行时解析、翻译到具体的内存地址中。
常量池中的每一项都是一个表,在JDK1.7以前有11中结构不一样的表结构,在JDK1.7中为了更好的支持动态语言调用,又增长了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。不过这里不会介绍这三种表数据结构。
这14个表的开始第一个字节是一个u1类型的tag,用来标识是哪种常量类型。这14种常量类型所表明的含义以下:
由class文件结构图可知:
常量池的开头两个字节0x0022是常量池的容量计数,这里是34,也就是说,这个常量池中有33个常量项。 咱们能够看一下这33个常量:
蓝色部分的内容就是33个常量,咱们能够发现图片右边用UTF-8编码后已经把常量翻译成了英文字母。能够看到这部分的内容很是多。由于常量池中的常量比较多,每一中常量还有本身的结构,致使常量池的结构很是复杂,这里只解析第一个常量做为示例:
看看这个例子的第一项,容量计数后面的第一个字节标识这个常量的类型,是0x0A,即10,查表可知是类方法的符号引用,这个常量表的结构以下:
按照这个结构,能够知道name_index是6(0x0006),descriptor_index是20(0x0014)。这都是一个索引,指向常量池中的其余常量,其中name描述了这个方法的名称,descriptor描述了这个方法的访问标志(好比public、private等)、参数类型和返回类型。(这里由于手工解析常量池确实是一件很坑爹的工做,并且后面会介绍自动解析的工具,因此这里就不去管name和descriptor的内容了)
咱们能够看到手工解析常量池是一件很是痛苦的事情,这里还只是一个特别简单的例子生成的class文件,咱们能够本身想一想若是是本身写的一个程序编译为class文件后,它的常量池会很是大,因此Java已经为咱们提供了一个解析常量池的工具javap,咱们能够经过javap -verbose class文件名,就能够自动帮咱们解析了,下面是这个程序的解析结果:
警告: 二进制文件HelloWorld包含temp.HelloWorld Classfile /I:/work/out/production/work/temp/HelloWorld.class Last modified 2018-8-3; size 543 bytes MD5 checksum 5eeb0ca06c253d3206781e81895bd4a4 Compiled from "HelloWorld.java" public class temp.HelloWorld minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // Hello,World #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #26 // temp/HelloWorld #6 = Class #27 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Ltemp/HelloWorld; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Utf8 args #17 = Utf8 [Ljava/lang/String; #18 = Utf8 SourceFile #19 = Utf8 HelloWorld.java #20 = NameAndType #7:#8 // "<init>":()V #21 = Class #28 // java/lang/System #22 = NameAndType #29:#30 // out:Ljava/io/PrintStream; #23 = Utf8 Hello,World #24 = Class #31 // java/io/PrintStream #25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V #26 = Utf8 temp/HelloWorld #27 = Utf8 java/lang/Object #28 = Utf8 java/lang/System #29 = Utf8 out #30 = Utf8 Ljava/io/PrintStream; #31 = Utf8 java/io/PrintStream #32 = Utf8 println #33 = Utf8 (Ljava/lang/String;)V { public temp.HelloWorld(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 2: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ltemp/HelloWorld; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String Hello,World 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 4: 0 line 5: 8 LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String; } SourceFile: "HelloWorld.java" 复制代码
常量池结束后紧接着的两个字节表明访问标志,用来标识一些类或接口的访问信息,包括:这个Class是类仍是接口;是否认义为public;是否认义为abstract;若是是类的话,是否被声明为final等。具体的标志位以及含义以下表:
因为access_flags是两个字节大小,一共有十六个标志位可使用,当前仅仅定义了8个,没有用到的标志位都是0。对于一个类来讲,可能会有多个访问标志,这时就能够对照上表中的标志值取或运算的值。拿上面那个例子来讲,它的访问标志值是0x0021,查表可知,这是ACC_PUBLIC和ACC_SUPER值取或运算的结果。因此HelloWorld这个类的访问标志就是ACC_PUBLIC和ACC_SUPER,这一点咱们能够在javap获得的结果中验证:
在访问标志access_flags后接下来就是类索引(this_class)和父类索引(super_class),这两个数据都是u2类型的,而接下来的接口索引集合是一个u2类型的集合,class文件由这三个数据项来肯定类的继承关系。因为Java中是单继承,因此父类索引只有一个;但Java类能够实现多个接口,因此接口索引是一个集合。
类索引用来肯定这个类的全限定名,这个全限定名就是说一个类的类名包含全部的包名,而后使用”/”代替”.”。好比Object的全限定名是java.lang.Object。父类索引肯定这个类的父类的全限定名,除了Object以外,全部的类都有父类,因此除了Object以外全部类的父类索引都不为0.接口索引集合存储了implements语句后面按照从左到右的顺序的接口。
类索引和父类索引都是一个索引,这个索引指向常量池中的CONSTANT_Class_info类型的常量。而后再CONSTANT_Class_info常量中的索引就能够找到常量池中类型为CONSTANT_Utf8_info的常量,而这个常量保存着类的全限定名。
字段表集合,顾名思义就是Java类中的字段,字段又分为类字段(静态属性)和实例字段(对象属性),那么,在Class文件中是如何保存这些字段的呢?咱们能够想想保存一个字段须要保存它的哪些信息呢?
答案是:字段的做用域(public、private和protected修饰符)、是实例变量仍是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段的数据类型(基本类型、对象、数组)以及字段名称。
在字段表集合中介绍了字段的描述符和方法的描述符,对于理解方法表有很大帮助。class文件存储格式中对方法的描述和对字段的描述几乎相同,方法表的结构也和字段表相同,这里就再也不列出。不过,方法表的访问标志和字段的不一样,列出以下:
属性表在前面出现了屡次,在Class文件、字段表和方法表均可以携带本身的属性表集合,来描述某些场景专有的信息。 与Class文件中其余的数据项目要求严格的顺序、长度和内容不一样,属性表集合的限制比较少,不要求严格的顺序,只要不与已有的属性名重复,任何人实现的编译器均可以向属性表中写入自定义的属性信息,Java虚拟机会在运行时忽略掉那些不认识的信息。为了能正确解析class文件,《Java虚拟机规范(第二版)》中预约义了9项虚拟机应当识别的属性。如今,属性已经达到了21项。具体信息以下表,这里仅对常见的属性作介绍:
其实真心不想写这篇的,由于本身也没有静下心来,认真的一个个本身去实际,只是说把书上的东西搬过来,这个坑之后补吧,可能对字节码的东西仍是刚接触,等有了最基本的几率再去啃它,太难了
好了各位,以上就是这篇文章的所有内容了,能看到这里的人呀,都是真粉。
创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见
六脉神剑 | 文 【原创】若是本篇博客有任何错误,请批评指教,不胜感激 !