前言
这篇内容是上一篇[动态代理三部曲:上] - 动态代理是如何"坑掉了"我4500块钱的补充,进一步分析篇。
建议二者结合食用,醇香绵软,入口即化。
好了,不扯淡了,开始...
正文
2、Class 文件的格式
这里为啥是2开头呢?因为上篇文章是1
这部分内容不知道各位小伙伴是怎么感觉的。最开始学习的时候,我是一头雾水,不知道如何下手。当一步步结合反射、JVM内存模型,类加载机制后。再回过头来就会发现一起豁然开朗。
此篇内容的开始,让我们根据我们demo中所用的类:RentHouseProcessorHandler来分析这个问题。
如果我们用十六进制编辑器(比如:Sublime)打开这个RentHouseProcessorHandler.class文件:
说实话这一行行的文字,最开始我是拒绝的。哦,上帝,为什么要让我看这些鬼东西...其实如果我们静下心来,想当年高中时代学习数学,物理公式那样去认真的对待它。就会发现它不过就是:一堆人为的定义上了含义的符号而已。
在我们准备读懂这些十六进制文字时,先让我们看一幅《Java虚拟机规范(Java SE 7)》对class文件的定义:
2.1、Class文件的规范结构
2.1.1、标准结构
上图的内容,其实非常的通俗易懂,不要因为是不常见的英文就抵触它们。让我们尝试着去翻译它们:
1、魔数;2、次版本号;3、主版本号;4、常量池数量;5、常量池;6、权限标识;7、此类;8、父类;9、接口数目;10、接口;11、变量数目;12、变量;13、方法数目;14、方法....
其实是不是发现了什么,这不是就一个类应该存在的东西么?没错啊,Class文件的结构就是固定了我们编写的Class类所存放的规则而已。最开始的我,以为是深奥,没敢去了解他们。当我踌躇满志,鼓足勇气去准备好好大干一场的时候,才发现它太简单了...就是一些规则,仅此而已。
2.1.2、特别注意的结构:表
虽然只是一些规则,但规则之中,总会有一些特别需要我们去注意的地方:比如cp_info这个类型。在《深入理解Java虚拟机》中,作者把以_info结尾的类型称之为“表”。这里让我们也沿用这种表达方式。说白了,它就是拥有多级关系的类型。
cp_info 表示常量池(常量池:首先它和方法区中运行时常量池不是同一个内容。这里的常量池存放了字面量和符号引用)。
符号引用:
符号引用:
- 类和接口的全局限定名
- 字段的名称和描述符
- 方法的名称和描述符
这里符号引用的作用,我们想先一个问题。CPU执行程序的时候,实际上是去寻找对应指令的内存地址。但是我们的Class文件是先被编译出来的,但是此时还没有被JVM加载到内存,所以肯定是不可能存在内存地址这一说的。因此我们的Class文件需要一些标识,让JVM加载内容的时候从常量池中获取到对应的符号引用,然后在映射到具体的内存地址上。
放到常量池的中数据项在《Java虚拟机规范(Java SE 7)》中一共有14个常量,每一种常量都是一个“表”,并且每种常量都用一个公共的tag来表示是哪种类型的常量。具体内容如下图:
这里让我们先解读一下这个常量池:让我们跳过u4的魔数、u2的此版本、u2的主版本。直接来看constant_pool_count。跳过对应的内容,那么我们的constant_pool_count就对应十六进制的20,对应十进制的32,也就是说常量池中有32个内容?实际不是的,因为设计者将第0个位置空出来另做打算。所以我们的常量池只有31个内容。
我们可以通过javap命令证实这个问题。
接下来的一个字节:0a,翻译成十进制就是10,对应我们表中的CONSTANT_Methodref_info,而接下来的四个字节。分别代表索引3,20。这里的索引代表什么意思呢?注意理解下图中标红的地方:
接下来就不逐个解读这些内容了,因为它就是一个对应的过程。如果小伙伴们有兴趣可以自行去尝试解读一番哦。推荐一个工具JavaClassViewer,可以比较方便的查看这些内容:
常量池结束之后,便是我们正常的变量,方法的信息。而这里我们需要了解一个全新的概念:描述符。
对于我们来说一个变量、方法在java源码里是什么样子我们很清楚。但是它们在class文件里是什么样子的呢?这个样子其实就被称之为:描述符。
上文谈动态代理的时候,我们了解到了ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);
方法中,通过:
dout.writeInt(0xCAFEBABE);
MethodInfo minfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V",ACC_PUBLIC);
等方法,构建了我们的$Proxy0所需要的class结构,是不是和我们javap出来的内容很类似?接下来让我们走进描述符。
2.2、变量、方法的描述符
经过上述内容的铺垫,0xCAFEBABE是什么意思,应该无需多言了。而<init>
是构造方法的意思。再加上(Ljava/lang/reflect/InvocationHandler;)V
以及ACC_PUBLIC就可以表示为InvocationHandler的public的构造方法,其中V表示无返回值。
-
<init>
:对象构造器方法。 -
<clinit>
:类构造器方法。
这里的内容就被称之为方法的描述符,让我们简单的看一些图,加深这方面内容。
基本类型和void在描述符中都有一个大写字符和他们对应; 那么引用类型的描述符,又是什么样子的呢?
“L” + 类型的全限定名 + “;”
例如下图中的:Ljava/lang/Object;就是表示这是一个Object类型。
而方法描述符的规则也很简单,上图中,总结出来就是一句话:
(参数类型1参数类型2参数类型3 ...)返回值类型
例如上图中的:int[] m(int i,String s)转换为描述符:(ILjava/lang/String;)[I
不知道截了这么多图,大家对描述符有没有比较明确的认识。说白了我们我们ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);
中所write的内容就是具体方法的描述符。
然后通过DataOutputStream转成byte数组,那么就是我们Class文件所固定的内容了。因此,此时我们的Class文件就已经构建完毕,接下来所需要的就是将其加载到内存中,供我们使用。
2.3、收尾
到这准备结束class文件结构的内容。不知道小伙伴们是否有收获。因为篇幅是在有限,有些内容又不是一句话俩句话可以描述清楚的。所以有些内容一带而过,实在抱歉。具体细节内容,大家可以参考《深入理解Java虚拟机》。
希望大家可以谅解。