JVM之阅读一份java class文件

1.简介

要了解JVM的运行机理,从class文件入手最好不过了,下面就简单梳理了一下阅读一个简单的java 字节码文件的过程。个人感受,阅读这个难度到不是很大,重要的是要查阅好Oracle的JVM标准文件以及对工具的使用。

【Oracle官方文档】:https://docs.oracle.com/javase/specs/index.html

使用工具:IDEA

使用的插件:jclasslib 和 BinEnd

JDK:1.8

2.编译java文件

我使用的源代码是最简单的JAVA程序,仅用来梳理整个class文件的框架 。

package com.example.demo;

public class helloword {
}

这是一个巨简单的文件,主要为了说明编译后的class文件的格式。编译之后的helloword.class文件使用BinEnd工具打开,得到如下(我这里用16进制展示) :

cafe babe 0000 0034 0010 0a00 0300 0d07
000e 0700 0f01 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162
6c65 0100 0474 6869 7301 001c 4c63 6f6d
2f65 7861 6d70 6c65 2f64 656d 6f2f 6865
6c6c 6f77 6f72 643b 0100 0a53 6f75 7263
6546 696c 6501 000e 6865 6c6c 6f77 6f72
642e 6a61 7661 0c00 0400 0501 001a 636f
6d2f 6578 616d 706c 652f 6465 6d6f 2f68
656c 6c6f 776f 7264 0100 106a 6176 612f
6c61 6e67 2f4f 626a 6563 7400 2100 0200
0300 0000 0000 0100 0100 0400 0500 0100
0600 0000 2f00 0100 0100 0000 052a b700
01b1 0000 0002 0007 0000 0006 0001 0000
0003 0008 0000 000c 0001 0000 0005 0009
000a 0000 0001 000b 0000 0002 000c 

我使用jclasslib 解析的class文件整理成xmind参考

JVM之阅读一份java class文件_第1张图片

BinEnd处理的文件如下 :

JVM之阅读一份java class文件_第2张图片

3.阅读文件

3.1ClassFile文件结构

Class文件是一组以8位字节为基础单位的二进制流,包含多个数据项目(数据项目的顺序,占用的字节数均由规范定义)。

数据项目分为2种基本数据类型(以及由这两种基本数据类型组成的集合):

1,无符号数,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数

2,表,以“_info”结尾,由多个无符号数或其它表构成的复合数据类型

整体文件结构如下(官方提供):

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info constant_pool[constant_pool_count-1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

下面是一个表格形式说明

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count-1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

由于不包含任何分隔符,故表中的数据项,无论是数量还是顺序,都是被严格限定的

3.2阅读过程

这里的阅读过程按照上面的文件结构,从上到下,从外到内嵌套阅读,要注意阅读顺序

3.2.1 magic(魔数)

每个Class文件的头四个字节称为魔数,它的唯一作用是用来确定该文件是否为一个能被虚拟机接受的Class文件。

JVM之阅读一份java class文件_第3张图片

3.2.2 minor_version (次版本)

次版本占用u2(两字节),这里minor_version=0x0000

JVM之阅读一份java class文件_第4张图片

3.2.3 major_version (主版本)

主版本占用u2(两字节),这里major_version=0x0034(翻译成十进制就是52)

(JDK1.5:0x0031,JDK1.6:0x0032,JDK1.7:0x0033,JDK1.8:0x0034)

JVM之阅读一份java class文件_第5张图片

3.2.4 constant_pool_count (常量池数量)

由于常量池中常量的数目不是固定的,所以在常量池入口首先使用一个2字节长的无符号数constatn_pool_count来代表常量池计数值。

constant_pool_count:占2字节,0x0010,转化为十进制为15,即说明常量池中有15个常量(常量池的计数是从1开始的,其它集合类型均从0开始),索引值为1~15。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示

JVM之阅读一份java class文件_第6张图片

参照jclasslib生成的结果:

JVM之阅读一份java class文件_第7张图片

3.2.5 constant_pool[constant_pool_count-1] (常量池常量)

常量池中的常量索引最大为constant_pool_count-1,即例如上面有16个常量,最大索引是15

表类型数据集合,即常量池中每一项常量都是一个表,共有14种结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型,常量类型及其数据结构参见:

类型 标志位
CONSTANT_Class_info 7
CONSTANT_Fieldref_info 9
CONSTANT_Methodref_info 10
CONSTANT_InterfaceMethodref_info 11
CONSTANT_String_info 8
CONSTANT_Integer_info 3
CONSTANT_Float_info 4
CONSTANT_Long_info 5
CONSTANT_Double_info 6
CONSTANT_NameAndType_info 12
CONSTANT_Utf8_info 1
CONSTANT_MethodHandle_info 15
CONSTANT_MethodType_info 16
CONSTANT_InvokeDynamic_info 18

可以看到,字节码中接下来对应的tag是0a(十进制是10),标志位tag对应的类型是:CONSTANT_Methodref_info

JVM之阅读一份java class文件_第8张图片

CONSTANT_Methodref_info对应的文件结构如下:

CONSTANT_Methodref_info {
	u1 tag;  #0a
	u2 class_index; #00 03
	u2 name_and_type_index; #00 0d
}

然后开始对CONSTANT_Methodref_info文件进行class文件的解析,如上面所示:class_indexname_and_type_index都是引用常量池,可以从jclasslib的处理结果看出

JVM之阅读一份java class文件_第9张图片

然后再分别取对应,即可以发现刚好印证了我们的想法。

关于常量池这里不再赘述过多,可以参考:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4

进行后面的解析,这里贴出关于常量池的跟踪结果:

0a #表开始的u1类型标志位 tag=10,对应CONSTANT_Methodref_info表  
00 03 #里面具体的引用#3
00 0d #里面具体的引用#13
07 #表开始的u1类型标志位 tag=7,对应CONSTANT_Class_info表
000e #里面的具体引用#14
07 #表开始的u1类型标志位 tag=7,对应CONSTANT_Class_info表
00 0f #里面的具体引用#15
01 00 06 3c69 6e69 743e #[04]表开始的u1类型标志位 tag=1,对应CONSTANT_Utf8_info表 内容:
01 00 0328 2956  #[05]内容:()V
01 00 0443 6f64 65 #[06]内容:Code
01 00 0f 4c69 6e65 4e75 6d62 6572 5461 626c 65 #[07]内容:LineNumberTable
01 00 12 4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65  #[08]内容:LocalVariableTable
01 00 0474 6869 7301 0 #[09]内容:this
01 c 4c63 6f6d 2f65 7861 6d70 6c65 2f64 656d 6f2f 6865 6c6c 6f77 6f72 643b  #[10]内容:Lcom/example/demo/helloword;
01 00 0a53 6f75 7263 6546 696c 65 #[11]内容:SourceFile
01 000e 6865 6c6c 6f77 6f72 642e 6a61 7661  #[12]内容:helloword.java
0c 00 04 00 05 #[13]里面的具体引用#4 #5
01 00 1a 636f 6d2f 6578 616d 706c 652f 6465 6d6f 2f68 656c 6c6f 776f 7264  #[14]内容:com/example/demo/helloword
01 00 106a 6176 612f 6c61 6e67 2f4f 626a 6563 74 #[15]内容:java/lang/Object

3.2.6 access_flags (访问标志位)

访问标志位占用u2(2个字节),关于访问标志位的对应表如下

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令,JDK1.2以后编译出来的类这个标志为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口和抽象类,此标志为真,其它类为假
ACC_SYNTHETIC 0x1000 标识别这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

查看字节码中的标志位如下:

JVM之阅读一份java class文件_第10张图片

访问标志位是:0x0021,从上面的标志位映射表中找不到对应的数值,这是什么原因的?先使用javap -v xx.class文件

E:\projs\demo\target\classes\com\example\demo>javap -v helloword.class
Classfile /E:/projs/demo/target/classes/com/example/demo/helloword.class
  Last modified 2020-5-30; size 286 bytes
  MD5 checksum b2654afcd52ddbe82eecd1b919012f4e
  Compiled from "helloword.java"
public class com.example.demo.helloword
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#13         // java/lang/Object."":()V
   #2 = Class              #14            // com/example/demo/helloword
   #3 = Class              #15            // java/lang/Object
   #4 = Utf8               
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/example/demo/helloword;
  #11 = Utf8               SourceFile
  #12 = Utf8               helloword.java
  #13 = NameAndType        #4:#5          // "":()V
  #14 = Utf8               com/example/demo/helloword
  #15 = Utf8               java/lang/Object
{
  public com.example.demo.helloword();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/example/demo/helloword;
}
SourceFile: "helloword.java"

从这里可以看出对应的flags: ACC_PUBLIC, ACC_SUPER是两个权限,结果就来了,这里使用运算进行计算:测试类的访问标志为ACC_PUBLIC | ACC_SUPER = 0x0001 | 0x0020 =1 | 32 =33 = 0x0021

3.2.7 this_class

官方文档解释如下:

The value of the this_class item must be a valid index into the
constant_pool table. The constant_pool entry at that index must be a
CONSTANT_Class_info structure (§4.4.1) representing the class or interface
defined by this class file.

这里是引用常量池的,对应的值如下:

JVM之阅读一份java class文件_第11张图片

对应的值是:0x0002,从jclasslib结果中可以看到 ,结果就是自己

JVM之阅读一份java class文件_第12张图片

3.2.8 super_class

官方文档解释如下:

For a class, the value of the super_class item either must be zero or
must be a valid index into the constant_pool table.

也是从常量池中的引用过来的

JVM之阅读一份java class文件_第13张图片

应用的是常量池中的0x0003,查看jclasslib结果

JVM之阅读一份java class文件_第14张图片

3.2.9 interfaces_count (接口数量)

接口数量为:0x0000,这个为0,后面的interfaces[interfaces_count];也就不存在了,直接看fields_count

JVM之阅读一份java class文件_第15张图片

3.2.10 fields_count (字段数量)

JVM之阅读一份java class文件_第16张图片

字段数量也是0x0000,跳过fields[fields_count]直接看方法数量methods_count

3.2.11 methods_count (方法数量)

从反编译的字节码可以看出里面包含一个默认构造方法

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.example.demo;

public class helloword {
    public helloword() {
    }
}

所以数量应该是1

JVM之阅读一份java class文件_第17张图片

3.2.12 method_info (方法详情)

方法表的结构如下:

method_info {
        u2 access_flags; #00 01
        u2 name_index;#00 04
        u2 descriptor_index;#00 05
        u2 attributes_count;#00 01
        attribute_info attributes[attributes_count];
}

方法表的access_flag对应关系如下:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否为public
ACC_PRIVATE 0x0002 字段是否为private
ACC_PROTECTED 0x0004 字段是否为protected
ACC_STATIC 0x0008 字段是否为static
ACC_FINAL 0x0010 字段是否为final
ACC_SYNCHRONIZED 0x0020 字段是否为synchronized
ACC_BRIDGE 0x0040 方法是否是由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 字段是否为native
ACC_ABSTRACT 0x0400 字段是否为abstract
ACC_STRICTFP 0x0800 字段是否为strictfp
ACC_SYNTHETIC 0x1000 字段是否为编译器自动产生

name_indexdescriptor_index都是引用常量池的内容,这里不赘述,验证方法同上

attributes_count属性有一个,接下来看属性详情attribute_info,

attribute_info {
	u2 attribute_name_index;
	u4 attribute_length;
	u1 info[attribute_length];
}

从上面可以看到只有一个属性,接下来应该是属性attribute_name_index

JVM之阅读一份java class文件_第18张图片

引用常量池#6,即只有Code属性

JVM之阅读一份java class文件_第19张图片

接下来attribute_length有4个字节00 0000 2f即长度47,可以从jclasslib结果看出

JVM之阅读一份java class文件_第20张图片

然后查看Code属性Code_attribute

Code_attribute {
	u2 attribute_name_index;
	u4 attribute_length;
	u2 max_stack;
	u2 max_locals;
	u4 code_length;
	u1 code[code_length];
	u2 exception_table_length;
	{ 	u2 start_pc;
		u2 end_pc;
		u2 handler_pc;
		u2 catch_type;
	} exception_table[exception_table_length];
	u2 attributes_count;
	attribute_info attributes[attributes_count];
}

对应的结果如下:

00 06 #attribute_name_index #6, 即Code
		00 0000 2f #code_length  47

		00 01 #操作数栈深度为1
		00 01 #方法局部变量占用的空间为1
		00 0000 05 #接下来又5个字节的指令,见下


		2a #指令:aload_0
		b7 0001 #指令:invokespecial 引用#1
		b1 #指令:return
		0000 #Code属性异常表集合为空
		0002 #说明Code属性带有2个属性

code通过jclasslib可以看到有2个属性LineNumberTablelocal_variable_info

#LineNumberTable 属性结构
LineNumberTable_attribute {
	u2 attribute_name_index;
	u4 attribute_length;
	u2 line_number_table_length;
		{ u2 start_pc;
		u2 line_number;
		} line_number_table[line_number_table_length];
}


#LocalVariableTable属性结构
LocalVariableTable_attribute {
	u2 attribute_name_index;
	u4 attribute_length;
	u2 local_variable_table_length;
		{ u2 start_pc;
		u2 length;
		u2 name_index;
		u2 descriptor_index;
		u2 index;
} local_variable_table[local_variable_table_length];
}

对应的分析结果如下:

0007 #Code属性第一个属性的属性名称,指向常量池中第7个常量:LineNumberTable
			0000 0006 #表示LineNumberTable属性值所占字节长度为6
			0001 #即该line_number_table中只有一个line_number_info表
			0000 #start_pc为0x0000
			0003 #line_number为0x0003  ===到这里line_number属性结束
			0008 #Code属性第二个属性的属性名,指向常量池中第8个常量
			0000 000c #该属性值所占的字节长度为12
			0001 #说明local_variable_table中只有一个local_variable_info表
			0000 #start_pc为0x0000
			0005 #length为0x0005
			0009 #name_index为0x0009,指向常量池中第9个常量 this
			000a #descriptor_index为0x000a,指向常量池中第10个常量:Lcom/example/demo
			0000 #index

3.2.13 attributes_count 和attribute_info (属性数量)

JVM之阅读一份java class文件_第21张图片

可以看到只有一个属性,属性的结构如下:

attribute_info {
	u2 attribute_name_index;
	u4 attribute_length;
	u1 info[attribute_length];
}

分析结果如下:

		000b #attribute_name_index #11 SourceFile
		0000 0002 #attribute_length  长度为2
		000c #sourcefile_index  #12  helloword.java

从jclasslib的结果可以印证这一点

JVM之阅读一份java class文件_第22张图片

 

完整的解读结果

整个文件的解读结果如下:
cafe babe #魔数
0000 #小版本
0034 #大版本
0010 #counstant_pool_count 15个
0a #表开始的u1类型标志位 tag=10,对应CONSTANT_Methodref_info表  
00 03 #里面具体的引用#3
00 0d #里面具体的引用#13
07 #表开始的u1类型标志位 tag=7,对应CONSTANT_Class_info表
000e #里面的具体引用#14
07 #表开始的u1类型标志位 tag=7,对应CONSTANT_Class_info表
00 0f #里面的具体引用#15
01 00 06 3c69 6e69 743e #[04]表开始的u1类型标志位 tag=1,对应CONSTANT_Utf8_info表 内容:
01 00 0328 2956  #[05]内容:()V
01 00 0443 6f64 65 #[06]内容:Code
01 00 0f 4c69 6e65 4e75 6d62 6572 5461 626c 65 #[07]内容:LineNumberTable
01 00 12 4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65  #[08]内容:LocalVariableTable
01 00 0474 6869 7301 0 #[09]内容:this
01 c 4c63 6f6d 2f65 7861 6d70 6c65 2f64 656d 6f2f 6865 6c6c 6f77 6f72 643b  #[10]内容:Lcom/example/demo/helloword;
01 00 0a53 6f75 7263 6546 696c 65 #[11]内容:SourceFile
01 000e 6865 6c6c 6f77 6f72 642e 6a61 7661  #[12]内容:helloword.java
0c 00 04 00 05 #[13]里面的具体引用#4 #5
01 00 1a 636f 6d2f 6578 616d 706c 652f 6465 6d6f 2f68 656c 6c6f 776f 7264  #[14]内容:com/example/demo/helloword
01 00 106a 6176 612f 6c61 6e67 2f4f 626a 6563 74 #[15]内容:java/lang/Object
0021 # ACCESS_FLAG:0021
0002 # THIS_CLASS:引用cp #2
0003 # SUPER_CLASS:引用cp #3
00 00 #Interface_count:0 
00 00 #Field_count
00 01 #method_count
	00 01 #access_flag
	00 04 #name_index
	00 05 #descriptor_index
	00 01 #attribute_count
		00 06 #attribute_name_index #6, 即Code
		00 0000 2f #code_length  47

		00 01 #操作数栈深度为1
		00 01 #方法局部变量占用的空间为1
		00 0000 05 #接下来又5个字节的指令,见下


		2a #指令:aload_0
		b7 0001 #指令:invokespecial 引用#1
		b1 #指令:return
		0000 #Code属性异常表集合为空
		0002 #说明Code属性带有2个属性
			0007 #Code属性第一个属性的属性名称,指向常量池中第7个常量:LineNumberTable
			0000 0006 #表示LineNumberTable属性值所占字节长度为6
			0001 #即该line_number_table中只有一个line_number_info表
			0000 #start_pc为0x0000
			0003 #line_number为0x0003  ===到这里line_number属性结束
			0008 #Code属性第二个属性的属性名,指向常量池中第8个常量
			0000 000c #该属性值所占的字节长度为12
			0001 #说明local_variable_table中只有一个local_variable_info表
			0000 #start_pc为0x0000
			0005 #length为0x0005
			0009 #name_index为0x0009,指向常量池中第9个常量 this
			000a #descriptor_index为0x000a,指向常量池中第10个常量:Lcom/example/demo
			0000 #index
		0001 #attributes_count=1
		000b #attribute_name_index #11 SourceFile
		0000 0002 #attribute_length  长度为2
		000c #sourcefile_index  #12  helloword.java

官方文档

你可能感兴趣的:(JAVA虚拟机)