CLASS文件格式学习
来源:互联网 发布:形容男生的网络词语 编辑:程序博客网 时间:2024/05/01 03:40
写在前面
真知出于实践,最近在学习JAVA虚拟机的我看了半本《深入理解JAVA虚拟机》觉得也没有get到点。只是觉得第6章“类文件结构”还比较容易实践,那就实践下熟络熟络吧。
第一个例子
当然从helloworld开始了。不过既然是学习CLASS文件结构,那就换成hellojavaclass给自己来点互动的赶脚吧。
public class Main { public static void main(String[] args) { System.out.println("Hello JAVA Class"); }}
生成的class文件为
魔术&版本
头四字节标识了Class文件魔术,用以指导识别Class文件;次四字节标识了JAVA十进制版本号——52.0,也就是JDK 1.8.0
常量池
版本号后跟的是常量池。
首先是2字节的常量长度
标识了常量池个数为34个。注意常量池从1开始索引,索引0用以标识空引用。
紧接着的1字节07
标识了常量类型为CONSTANCT_Class_info
类型,而后2字节0002
标识了类名称CONSTANCT_Class_info.name_index
,指向了第二个常量。第二个常量的常量类型标识01
表示该常量为CONSTANT_Utf8_info
类型,其后0004
标识了该字符串的长度为4字节,而后四字节4D6169
标识了该字符串的二进制符,其utf8字符串为”Main”。至此,一个常量的解析完毕 。
借助javap -verbose
,整个文件的常量池概览如
Constant pool: #1 = Class #2 // Main #2 = Utf8 Main #3 = Class #4 // java/lang/Object #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Methodref #3.#9 // java/lang/Object."<init>":()V #9 = NameAndType #5:#6 // "<init>":()V #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 LMain; #14 = Utf8 main #15 = Utf8 ([Ljava/lang/String;)V #16 = Fieldref #17.#19 // java/lang/System.out:Ljava/io/PrintStream; #17 = Class #18 // java/lang/System #18 = Utf8 java/lang/System #19 = NameAndType #20:#21 // out:Ljava/io/PrintStream; #20 = Utf8 out #21 = Utf8 Ljava/io/PrintStream; #22 = String #23 // Hello JAVA Class #23 = Utf8 Hello JAVA Class #24 = Methodref #25.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V #25 = Class #26 // java/io/PrintStream #26 = Utf8 java/io/PrintStream #27 = NameAndType #28:#29 // println:(Ljava/lang/String;)V #28 = Utf8 println #29 = Utf8 (Ljava/lang/String;)V #30 = Utf8 args #31 = Utf8 [Ljava/lang/String; #32 = Utf8 SourceFile #33 = Utf8 Main.java
访问标志
常量池结束后,接2字节代表访问标志。
0021
标识ACC_PUBLIC|ACC_SUPER
,表示接下来描述的类定义为public类型,且允许invokespecial字节码指令的新语义。
类索引、父类索引与接口索引
访问标志后连着两个2字节数据0001
,0003
分别表示类索引,父类索引。紧接着的2字节数据为接口索引表容量,如果有索引表后面将继续接索引表。本例中没有索引表,所以接的是0000
。
字段表集合
类索引后跟的是字段表,首发的是4字节字段表长度0000
,本例中表示类Main
无字段。
方法表集合
同字段表集合,方法表集合首发的是4字节方法表长度0002
,表示方法表中有两个方法
两个红框分别代表了两个方法。
第一个方法的结构为
accesss_flag
: 0001 //表示public类型name_index
: 0005 //descripter_index
: 0006 //()V 表示void()类型方法attribute_count
: 0001 //表示属性表集合有一项属性atrribute_name_index
: 0007 //Code 说明此属性是方法的字节码描述atribute_length
: 0000002F //Code长度info
: …
第二个方法的结构为
accesss_flag
: 0009 //表示public static类型name_index
: 000E //maindescripter_index
: 000F //([Ljava/lang/String;)V 表示参量类型为String[],返回值为voidattribute_count
: 0001 //表示属性表集合有一项属性atrribute_name_index
: 0007 //Code 说明此属性是方法的字节码描述attribute_length
: 00000037 //Code长度info
: …
属性表集合
这个是重头戏了。上一节中提及的Code属性中,只给出了Code长度部分,而没有展开info
部分,本节将展开。
第一个方法<Init>
的属性表信息如
- attribute_name_index
: 0007 //属性类型:Code
- attribute_length
: 0000002F //属性表长度
- max_stack
: 0001 //操作数栈最大长度
- max_locals
: 0001 //局部变量表所需存储空间,单位Slot
- code_length
: 00000005 //字节码长度
- code
: 2AB70008B1 //字节码
- exception_table_length
: 0000 //异常属性为空
- attribute_count
: 0002 //两个属性
这段字节码代表的含义可用javap -verbose解读得
0: aload_0 1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return
这段代码将this
变量压倒操作数栈顶,并以之为参数调用java/lang/Object."<init>":()V
方法,最后返回。看样子java是默认给构造了一个构造函数,类似于
public Main() { super(); }
这段Code属性的两个额外属性分隔开,如
前者为LineNumberTable,后者为LocalVariableTable。
用javap -verbose可以解析到LineNumberTable信息为
LineNumberTable: line 1: 0
表示Java源码第1行对于字节码的第0行。
LocalVariableNumberTable信息为
LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LMain;
表示局部变量this
类型为Class Main
,作用域为从0开始的字节码,长度为5字节,它在栈中的Slot地址为0。
第二个方法main
的属性表信息如
- attribute_name_index
: 0007 //属性类型:Code
- attribute_length
: 00000037 //属性表长度
- max_stack
: 0002 //操作数栈最大长度
- max_locals
: 0001 //局部变量表所需存储空间,单位Slot
- code_length
: 00000009 //字节码长度
- code
: B200101216B60018B1 //字节码
- exception_table_length
: 0000 //异常属性为空
- attribute_count
: 0002 //两个属性
两个属性分别为
LineNumberTable: line 4: 0 line 5: 8
LocalVariableTable: Start Length Slot Name Signature 0 9 0 args [Ljava/lang/String;
表示代码段第0行对应字节码第8个字节;局部变量args
为[] java/lang/String
类型,并且其作用域为从字节码0开始的9个字节,最后,args局部变量的存储位置位于栈中的Slot0,也就是方法的第一个参数。
题外话
细心的朋友可能留意到,类的索引个数,字段个数,方法个数都是无符号2字节整型。同时,《深入理解JAVA虚拟机》还指出虚拟机对Code字节码长度的限制为65535字节。这样看来,索引个数,字段个数,方法个数的上限是65535,而方法内的代码长度也不能太长以至于超出虚拟机的限制。让我们来试试方法内代码超限的话会发生什么吧。
public class TestMethodCodeLen{ public void test() { int a = 1; int b = 2; a += b; b += a; ...//中间省略65536行a += b;b += a; System.out.println(a+b); }}
javac编译,结果显示
TestMethodCodeLen.java:3: 错误: 代码过长 public void test() ^1 个错误
还真是。
另一个例子
对于刚才的例子,没有继承,没有接口实现,没有类属性,没有方法局部变量——用例不具一般性。
这回来个像模像样的例子
public class Person { protected int gender; public String who() { return new String("Person"); }}
import java.util.Queue;public class Employee extends Person implements Comparable<Object>{ public int age = 20; static public String country = "China"; protected Queue<Integer> qi; @Override public int compareTo(Object arg0) { // TODO Auto-generated method stub return this.age - ((Employee) arg0).age; } public String who() { return new String("Employee"); }}
好了,这回再看看Employee.class的编译结果
其常量池为
Constant pool: #1 = Class #2 // Employee #2 = Utf8 Employee #3 = Class #4 // Person #4 = Utf8 Person #5 = Class #6 // java/lang/Comparable #6 = Utf8 java/lang/Comparable #7 = Utf8 age #8 = Utf8 I #9 = Utf8 country #10 = Utf8 Ljava/lang/String; #11 = Utf8 qi #12 = Utf8 Ljava/util/Queue; #13 = Utf8 Signature #14 = Utf8 Ljava/util/Queue<Ljava/lang/Integer;>; #15 = Utf8 <clinit> #16 = Utf8 ()V #17 = Utf8 Code #18 = String #19 // China #19 = Utf8 China #20 = Fieldref #1.#21 // Employee.country:Ljava/lang/String; #21 = NameAndType #9:#10 // country:Ljava/lang/String; #22 = Utf8 LineNumberTable #23 = Utf8 LocalVariableTable #24 = Utf8 <init> #25 = Methodref #3.#26 // Person."<init>":()V #26 = NameAndType #24:#16 // "<init>":()V #27 = Fieldref #1.#28 // Employee.age:I #28 = NameAndType #7:#8 // age:I #29 = Utf8 this #30 = Utf8 LEmployee; #31 = Utf8 compareTo #32 = Utf8 (Ljava/lang/Object;)I #33 = Utf8 arg0 #34 = Utf8 Ljava/lang/Object; #35 = Utf8 who #36 = Utf8 ()Ljava/lang/String; #37 = Class #38 // java/lang/String #38 = Utf8 java/lang/String #39 = String #2 // Employee #40 = Methodref #37.#41 // java/lang/String."<init>":(Ljava/lang/String;)V #41 = NameAndType #24:#42 // "<init>":(Ljava/lang/String;)V #42 = Utf8 (Ljava/lang/String;)V #43 = Utf8 SourceFile #44 = Utf8 Employee.java #45 = Utf8 LPerson;Ljava/lang/Comparable<Ljava/lang/Object;>;
这回就不再细细对了,只关注与前一个例子不同之处。
类索引,父类索引和接口集合
地址0x00000226
开始的3个4字节分别为
- 0001
: 标识类为Employee
- 0003
: 标识父类为Person
- 0001
: 标识接口个数为1个
而后接的接口集合为 0005
指向常量池中的java/lang/Comparable
字段表集合
地址0x0000022e
开始的2字节标识了字段表字段个数为0003
,即3个。
第一个字段信息为
access_flag
: 0001//pulic属性name_index
: 0007//索引常量池7,表示字段名为agedescriptor_index
: 0008//索引常量池8,表示int型变量attribute_count
: 0000
第二个字段信息为
access_flag
: 0009//static public 属性name_index
: 0009//表示变量名为countrydescriptor_index
: 000A//表示变量类型为Object Stringattribute_count
: 0000
第三个字段信息为
access_flag
: 0004//protected属性name_index
: 000B//表示变量名为qidescriptor_index
: 000C//表示变量类型为Object java.util.Queueattribute_count
: 0001//表示有一个属性信息
该属性信息为
attribute_name_index
: 000D//表示属性名称为Signatureattribute_length
: 00000002//表示属性长度为2字节signature_index
: 000E//表示泛型信息为Ljava/util/Queue<Ljava/lang/Integer;>;
也就是Object java.util.Queue<Integer>
看,对于泛型类型,会额外增加字段的泛型签名属性,以标识泛型类型。
方法表集合
地址0x00000250
开始的2字节标识了方法个数0004
。为什么是4个呢?构造函数,compareTo
,who
,还有一个编译器生成的static方法用来初始化static变量。
来看下方法概览
static {}; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #18 // String China 2: putstatic #20 // Field country:Ljava/lang/String; 5: return LineNumberTable: line 5: 0 LocalVariableTable: Start Length Slot Name Signature public Employee(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #25 // Method Person."<init>":()V 4: aload_0 5: bipush 20 7: putfield #27 // Field age:I 10: return LineNumberTable: line 3: 0 line 4: 4 line 3: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this LEmployee; public int compareTo(java.lang.Object); descriptor: (Ljava/lang/Object;)I flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: getfield #27 // Field age:I 4: aload_1 5: checkcast #1 // class Employee 8: getfield #27 // Field age:I 11: isub 12: ireturn LineNumberTable: line 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 13 0 this LEmployee; 0 13 1 arg0 Ljava/lang/Object; public java.lang.String who(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: new #37 // class java/lang/String 3: dup 4: ldc #39 // String Employee 6: invokespecial #40 // Method java/lang/String."<init>":(Ljava/lang/String;)V 9: areturn LineNumberTable: line 14: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this LEmployee;
可以看到几个好玩的地方。
- static变量contry在编译器生成的static方法中初始化
- 对象变量age在构造函数中被初始化。所以私以为代码中将对象属性的初始化放在声明中和放在构造方法中基本没有多大性能差别。(有兴趣的朋友可以看看C++相关编译器是怎么处理对象属性的初始化的,做个对比)
- 编译器为
Employee
生成的构造方法先调用了父类的构造方法,再处理本类属性的初始化 - 在方法who中,编译器对语句
return new String('Employee')
的处理是产生一个无名String类,初始化为Employee
然后返回。所以,编译器在这里并没有为这个类创建中间类名。这种代码风格比之String rst = new String('Employee'); return rst;
是效率高的。
- CLASS文件格式学习
- class文件格式
- class文件格式
- class 文件格式
- class文件格式
- Class文件格式
- Class文件格式
- Class文件格式
- Class文件格式
- 解读Java Class文件格式
- 解读Java Class文件格式
- java class文件格式解析
- Class文件格式解析
- JAVA class文件格式
- 解读 Java Class 文件格式
- JAVA Class文件格式
- Class文件格式解析
- 深入Java class文件格式
- line-height
- [bigdata-018] java spring 快捷入门
- jquery判断字符输入个数(数字英文长度记为1,中文记为2,超过长度自动截取)
- SpringMVC简单学习笔记
- 在C++中尽可能用const
- CLASS文件格式学习
- 【牛客网】网易2017内推笔试编程题合集(一)
- 跟着斯坦福白胡子老头学iOS app生命周期
- Java集合(HashMap)
- 欢迎使用CSDN-markdown编辑器
- 动态规划之最长递增子序列 最长不重复子串 最长公共子序列
- 物理地址和线性地址介绍
- mysql表的清空、删除和修改操作详解
- Java入门 一、类和对象