博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入理解JVM类文件格式
阅读量:2220 次
发布时间:2019-05-08

本文共 19021 字,大约阅读时间需要 63 分钟。

点击上方 ""关注, 置顶或星标一起学习

每天晚上10点00分, 我们不见不散

导读

Java最有名的宣传口号就是:

“一次编写,到处运行(Write Once,Run Anywhere)。”

每天鸡汤

人一定要经得起假话,受得起敷衍,忍得住欺骗,忘得了诺言,放得下一切。失去的东西,其实从来未曾真正地属于你,也不必惋惜。

责任编辑:涛哥

链接:https://www.jianshu.com/p/6950542b3e1f

正文

我们知道Java最有名的宣传口号就是:“一次编写,到处运行(Write Once,Run Anywhere)”,而其平台无关性则是依赖于JVM, 所有的java文件都被编译成字节码(class)文件,而虚拟机只需要认识字节码文件就可以了。想要弄懂虚拟机以及类加载机制,这部分内容是不可不知的。

Class文件是一组以8字节为基础单位的二进制流,所有数据无间隔的排列在Class文件之中,多字节数据以大端(big-endian order)的方式存储。Class文件以一种接近于C中结构体的伪代码形式存储数据结构,并且只包含无符号数和表两种数据结构:

  • 无符号数:u1、u2、u4、u8分别表1、2、4、8字节的无符号数

  • :由多个无符号数或者其他表组成的复合数据类型, Class文件本身也是一张表。

Class表结构:

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];}

参照上面的数据结构,Class文件由10个部分组成:

1 . 魔数
2 . Class文件主次版本号
3 . 常量池
4 . 访问标记
5 . 当前类名
6 . 父类名
7 . 继承的接口名
8 . 包含的所有字段的数量+字段
9 . 包含的所有方法的数量+方法
10 . 包含的所有属性的数量+属性

下面我们依次对每个部分进行分析:

1. 魔数

魔数(Magic number)用来确定文件类型,这里就是检测文件是否是能够被虚拟机接受的Class文件。很多文件都使用魔数来确定文件类型,而不是扩展名(因为扩展名可以任意修改)。可以参看我的深入理解程序构造(一)。

Class文件的魔数是“0xcafebabe”,咖啡宝贝?Java本身也是一种爪哇咖啡,真是挺有缘的。

这里我也写个小的测试程序,来看看它的二进制码流:

package com.shuqing28;public class TestClass {    private int m;    public int inc() {        return m+1;    }}

我们使用javac编译成.class文件,Windows下可以使用WinHex打开,Linux下则可以使用hexdump打开二进制,命令如下:

1$ hexdump -C TestClass.class  200000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........| 300000010  00 03 00 13 07 00 14 07  00 15 01 00 01 6d 01 00  |.............m..| 400000020  01 49 01 00 06 3c 69 6e  69 74 3e 01 00 03 28 29  |.I...
...()| 500000030  56 01 00 04 43 6f 64 65  01 00 0f 4c 69 6e 65 4e  |V...Code...LineN| 600000040  75 6d 62 65 72 54 61 62  6c 65 01 00 12 4c 6f 63  |umberTable...Loc| 700000050  61 6c 56 61 72 69 61 62  6c 65 54 61 62 6c 65 01  |alVariableTable.| 800000060  00 04 74 68 69 73 01 00  19 4c 63 6f 6d 2f 73 68  |..this...Lcom/sh| 900000070  75 71 69 6e 67 32 38 2f  54 65 73 74 43 6c 61 73  |uqing28/TestClas|1000000080  73 3b 01 00 03 69 6e 63  01 00 03 28 29 49 01 00  |s;...inc...()I..|1100000090  0a 53 6f 75 72 63 65 46  69 6c 65 01 00 0e 54 65  |.SourceFile...Te|12000000a0  73 74 43 6c 61 73 73 2e  6a 61 76 61 0c 00 07 00  |stClass.java....|13000000b0  08 0c 00 05 00 06 01 00  17 63 6f 6d 2f 73 68 75  |.........com/shu|14000000c0  71 69 6e 67 32 38 2f 54  65 73 74 43 6c 61 73 73  |qing28/TestClass|15000000d0  01 00 10 6a 61 76 61 2f  6c 61 6e 67 2f 4f 62 6a  |...java/lang/Obj|16000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|17000000f0  05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|1800000100  09 00 00 00 2f 00 01 00  01 00 00 00 05 2a b7 00  |..../........*..|1900000110  01 b1 00 00 00 02 00 0a  00 00 00 06 00 01 00 00  |................|2000000120  00 03 00 0b 00 00 00 0c  00 01 00 00 00 05 00 0c  |................|2100000130  00 0d 00 00 00 01 00 0e  00 0f 00 01 00 09 00 00  |................|2200000140  00 31 00 02 00 01 00 00  00 07 2a b4 00 02 04 60  |.1........*....`|2300000150  ac 00 00 00 02 00 0a 00  00 00 06 00 01 00 00 00  |................|2400000160  06 00 0b 00 00 00 0c 00  01 00 00 00 07 00 0c 00  |................|2500000170  0d 00 00 00 01 00 10 00  00 00 02 00 11           |.............|260000017d

看第一行的前4个字节的十六进制就是0xcafebabe,所以文件类型确实为.class文件。

2. 版本号

第5和第6字节是次版本号(Minor Version),第7和第8字节是主版本号(Major Version)。这里看出我们的主版本号是0x0034,也就是52,下面是JDK与其对应的版本号关系:

JDK 1.8 = 52

JDK 1.7 = 51
JDK 1.6 =50
JDK 1.5 = 49
JDK 1.4 = 48
JDK 1.3 = 47
JDK 1.2 = 46
JDK 1.1 = 45

可以看出我使用的是Java8编译的代码。

3. 常量池

我们继续看二进制文件的第一行:

00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|

在主版本号0x0034后的是0x0016,这个值表示常量池的容量。常量池可以理解为Class文件的资源仓库,常量池中包含的数据结构是这样的:

cp_info {    u1 tag;    u1 info[];}

常量池中的每个项目都包含一个tag开头的cp_info对象,代表着常量类型,info则根据不同的类型各有各的结构。目前一共有14种常量类型:

Constant Type

Value

CONSTANT_Class

7

CONSTANT_Fieldref

9

CONSTANT_Methodref

10

CONSTANT_InterfaceMethodref

11

CONSTANT_String

8

CONSTANT_Integer

3

CONSTANT_Float

4

CONSTANT_Long

5

CONSTANT_Double

6

CONSTANT_NameAndType

12

CONSTANT_Utf8

1

CONSTANT_MethodHandle

15

CONSTANT_MethodType

16

CONSTANT_InvokeDynamic

18

上面的0x0016翻译成十进制是22,那么常量池中有21个常量,因为常量池中索引是从1开始计数的,所以常量索引范围是1~21。

00000000  ca fe ba be 00 00 00 34  00 16 0a 00 04 00 12 09  |.......4........|

接下看常量池的第一个常量, tag是0x0a, 查上面的常量表就是CONSTANT_Methodref,表示接下来定义的是一个方法,知道类型后,我们可以查一下CONSTANT_Methodref的结构,这里可以参考Oracle的官方文档The class File Format,

CONSTANT_Methodref_info {    u1 tag;    u2 class_index;    u2 name_and_type_index;}

由于.class文件是无间隔的二进制文件,所以接着读:

  • tag: 0x0a,上面已经说了指代CONSTANT_Methodref常量

  • class_index:指向常量池中CONSTANT_Class_info类型的常量,代表上面方法的名称

  • name_and_type_index : 指向常量池中CONSTANT_NameAndType_info常量,是对方法的描述

因为class_index占两个字节,所以紧接着读到了0x0004,也就是4,指向常量池中的第4个常量,name_and_type_index是0x0012,指向第18个常量。后面会分析到第4和第18个常量。

继续往下读,到第一行的最末了,是个0x09,指示的是CONSTANT_Fieldref,表示接下来是对一个域的定义, 查官方文档,格式为:

CONSTANT_Fieldref_info {    u1 tag;    u2 class_index;    u2 name_and_type_index;}

结构和CONSTANT_Methodref_info一样,这时候读到了第二行:

00000010 00 03 00 13 07 00 14 07 00 15 01 00 01 6d 01 00 |.............m..|

class_index为0x0003,指向第3个常量,name_and_type_index为0x0013指向第13个常量。这时候继续往后读,终于读到第3个常量了。此时tag是0x07,查表可得为CONSTANT_Class类型,此类型的常量代表一个类或者接口的符号引用,CONSTANT_Class的结构:

CONSTANT_Class_info {    u1 tag;    u2 name_index;}

tag是7, name_index是0x0014,十进制就是20,指向第20个常量,这样我们已经读了很多个字节了。但是这样解析下去很累,还好java自带的javap工具可以帮我们分析出字节码的内容。

执行下面语句:

javap -verbose TestClass.class

我们可以得到:

Last modified Nov 14, 2017; size 381 bytes  MD5 checksum 102d643185c4823ef103931ff3e34462  Compiled from "TestClass.java"public class com.shuqing28.TestClass  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant pool:   #1 = Methodref #4.#18 // java/lang/Object."
":()V   #2 = Fieldref #3.#19 // com/shuqing28/TestClass.m:I   #3 = Class #20 // com/shuqing28/TestClass   #4 = Class #21 // java/lang/Object   #5 = Utf8 m   #6 = Utf8 I   #7 = Utf8
   #8 = Utf8 ()V   #9 = Utf8 Code  #10 = Utf8 LineNumberTable  #11 = Utf8 LocalVariableTable  #12 = Utf8 this  #13 = Utf8 Lcom/shuqing28/TestClass;  #14 = Utf8 inc  #15 = Utf8 ()I  #16 = Utf8 SourceFile  #17 = Utf8 TestClass.java  #18 = NameAndType #7:#8 // "
":()V  #19 = NameAndType #5:#6 // m:I  #20 = Utf8 com/shuqing28/TestClass  #21 = Utf8 java/lang/Object{  public com.shuqing28.TestClass();    descriptor: ()V    flags: ACC_PUBLIC...//省略

这里我们可以看到Constant pool字段,后面依次列出了21个常量,可以看出第一个是Methodref型的常量,class_index指向第4个常量,第4个常量呢是CONSTANT_Class类型,name_index又指向第20个常量,可知是一个CONSTANT_Utf8类型的常量,前面没说到CONSTANT_Utf8,下面是它的结构:

CONSTANT_Utf8_info {    u1 tag;    u2 length;    u1 bytes[length];}

第一位tag为1,length指示字符数组的长度,bytes[length]是使用UTF-8缩略编码表示的字符串,这里解析出来是com/shuqing28/TestClass,即类的全限定名。

继续回到第一个Methodref常量,它的name_and_type_index值是18, 继续找到第18个常量,是CONSTANT_NameAndType_info类型,代表的是一个方法的信息:

CONSTANT_NameAndType_info {    u1 tag;    u2 name_index;    u2 descriptor_index;}

name_index指向了常量7, 即#7 = Utf8 <init>, 是一个CONSTANT_Utf8_info类型,值为<init>,这个是方法的名称,descriptor_index指向了常量8,即#8 = Utf8 ()V,是方法的描述,下文会说这个表达式是什么意思。

这样我们就可以一一把这21个常量分析清楚了。

其实Class文件就是在一开始列出了一堆常量,后面的各种描述都是各种index,指向前面常量池中的各种常量,来描述整个类的定义。就像有一本字典,我们使用字典中的字来造我们的句子,只不过Class文件中造句是有严格格式规定的,下面的内容基本都按照固定格式,无间隔的描述一个类的内容。

4. 访问标志

常量池结束后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口的访问信息,包括这个Class是类还是接口,是否是public的,是否是abstract,是否是final的。

访问标记含义如下表:

标志名称

标志值

含义

ACC_PUBLIC

0x0001

Declared public; may be accessed from outside its package.

ACC_FINAL

0x0010

Declared final; no subclasses allowed.

ACC_SUPER

0x0020

Treat superclass methods specially when invoked by the invokespecial instruction.

ACC_INTERFACE

0x0200

Is an interface, not a class.

ACC_ABSTRACT

0x0400

Declared abstract; must not be instantiated.

ACC_SYNTHETIC

0x1000

Declared synthetic; not present in the source code.

ACC_ANNOTATION

0x2000

Declared as an annotation type.

ACC_ENUM

0x4000

Declared as an enum type.

access_flags中一共有16个标志位可用,当前只定义了8个,别的都为0,TestClass是public类型的,且使用JDK1.2以后的编译器进行编译的(使用JDK1.2以后的编译器编译,这个值都为真),别的标志都为假。所以access_flags的值应为:0x0001|0x0020 = 0x0021。我们找到刚才常量池最后一行的地方:

000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........|

65 63 74分别对应ect,紧接着是0x0021,与我们的分析结果一致。

5.类索引、父类索引与接口索引集合

引用文章开头的ClassFile的数据结构,这三项定义为:

    u2             this_class;    u2             super_class;    u2             interfaces_count;    u2             interfaces[interfaces_count];

类索引和父类索引都是u2类型的数据,而接口索引首先给出了接口的数量,然后才是一个包含接口的数组。这三个值揭示了一个类的继承关系。

000000e0 65 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 |ect.!...........|

接着前面的0x0021看,类索引为0x0003,指示常量池第3个常量,查上文可得#3 = Class #20 // com/shuqing28/TestClass,第3个常量又指向第20个常量,而第20个常量是一个CONSTANT_Utf8变量,其值为com/shuqing28/TestClass,表示类的全限定名字符串。

接下来的是0x0004是父类索引,指向常量池中第4个常量,即#4 = Class #21 // java/lang/Object, 又指向第21个变量,即java/lang/Object,我们知道Object是所有类的父类。

接下来的是0x0000,可见TestClass没有实现任何接口。

6.字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级别的变量以及实例级的变量,但是不包括方法内的局部变量。一个Java字段可以包括以下信息:字段的作用域、是实例变量还是类变量、是否是final、并发可见性(volatile),是否可以被序列化(transient)、字段数据类型。下面是字段表具体结构:

field_info {    u2             access_flags;    u2             name_index;    u2             descriptor_index;    u2             attributes_count;    attribute_info attributes[attributes_count];}

再看access_flags可以取以下值:

标志名称

标志值

含义

ACC_PUBLIC

0x0001

Declared public; may be accessed from outside its package.

ACC_PRIVATE

0x0002

Declared private; usable only within the defining class.

ACC_PROTECTED

0x0004

Declared protected; may be accessed within subclasses.

ACC_STATIC

0x0008

Declared static.

ACC_FINAL

0x0010

Declared final; never directly assigned to after object construction (JLS §17.5).

ACC_VOLATILE

0x0040

Declared volatile; cannot be cached.

ACC_TRANSIENT

0x0080

Declared transient; not written or read by a persistent object manager.

ACC_SYNTHETIC

0x1000

Declared synthetic; not present in the source code.

ACC_ENUM

0x4000

Declared as an element of an enum.

一般来说,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能存在一个,其它标志都按照Java语言本身的性质来。

在access_flags标志的后面是两项索引值name_index,descriptor_index,两个都是指向常量池的索引,分别代表字段的简单名称以及字段和方法的描述符。

这里我们梳理下简单名称、描述符以及全限定名这三个词对应的概念:

全限定名:前面提到的com/shuqing28/TestClass就是全限定名,它把java代码中所有的"."替换成了"/",一般使用";"结尾。
简单名称:不带类型和修饰的方法或者字段名,上文中的代码里就是"inc"和"m"
至于方法描述符,描述的是数据类型、方法的参数列表和返回值。我们知道在C++中重载函数时函数实际上是换了名字的,包含了函数的参数,例如add(int x, int y),在编译后可能是Add_Int_Int, 但是在Java中我们把基本数据类型都用一个大写字符来表示,而对象类则是使用L+对象的全限定名来表示。

描述符标识字符含义:

标识字符

含义

B

byte

C

char

D

double

F

float

I

int

J

long

S

short

Z

boolean

V

void

L

Object, 例如 Ljava/lang/Object

对于数组,前面加[就行,如java.lang.String[][],表达为[[java/lang/Stringint[]就被记录为[I

用描述符描述方法时,按照参数列表,返回值的顺序描述,参数列表还需要放在括号内。比如前文提及的"() V" 就表示一个参数为空,返回值为void的方法,即代码中的void inc()方法。

举个复杂点的, int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex),其描述符为([CII[CIII) I

继续分析我们前文中提及的程序的二进制代码:

000000e0  65 63 74 00 21 00 03 00  04 00 00 00 01 00 02 00  |ect.!...........|000000f0  05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|

上一小节我们分析到第一行的0x0000了,接下来的是0x01,这个值其实代表了字段表的个数,我们的代码里只包含一个字段。接下来的是0x0002,这个字段是access_flags标志,查询后可知为ACC_PRIVATE,再接下来是0x0005, 从常量表清单上可以查到是#5 = Utf8 m, 再接着是descriptor_index, 其值为0x0006,查一下常量池为#6 = Utf8 I,可知这一句为private int m;

一般来说,在decriptor_index后,还有个属性集合用于存储一些额外信息,而0x0000代表没有属性字段。

如果把m字段声明为private static int m = 123;则可能多一个ConstantValue属性,指向常量值123。

7.方法表集合

方法表集合和字段表集合非常相似,结构也是:

method_info {    u2             access_flags;    u2             name_index;    u2             descriptor_index;    u2             attributes_count;    attribute_info attributes[attributes_count];}

只不过在访问标志和属性表集合的可选项有所不同。例如access_flags有以下可选值:

标志名称

标志值

含义

ACC_PUBLIC

0x0001

Declared public; may be accessed from outside its package.

ACC_PRIVATE

0x0002

Declaredprivate; accessible only within the defining class.

ACC_PROTECTED

0x0004

Declaredprotected; may be accessed within subclasses.

ACC_STATIC

0x0008

Declaredstatic.

ACC_FINAL

0x0010

Declaredfinal; must not be overridden

ACC_SYNCHRONIZED

0x0020

Declaredsynchronized; invocation is wrapped by a monitor use.

ACC_BRIDGE

0x0040

A bridge method, generated by the compiler.

ACC_VARARGS

0x0080

Declared with variable number of arguments.

ACC_NATIVE

0x0100

Declarednative; implemented in a language other than Java.

ACC_ABSTRACT

0x0400

Declaredabstract; no implementation is provided.

ACC_STRICT

0x0800

Declaredstrictfp; floating-point mode is FP-strict.

ACC_SYNTHETIC

0x1000

Declared synthetic; not present in the source code.

可以看出,方法里增加了像ACC_SYNCHRONIZED,ACC_NATIVE,ACC_STRICTACC_ABSTRACT, 分别对应着synchronizednativestrictfpabstract这些只能修饰方法的关键字。

现在我们就可以继续分析我们程序的二进制代码了。

000000f0 05 00 06 00 00 00 02 00  01 00 07 00 08 00 01 00  |................|00000100  09 00 00 00 2f 00 01 00  01 00 00 00 05 2a b7 00  |..../........*..|

上一小节我们刚刚分析到000000f0行的0x0000,接下来的是0x0002,代表有两个方法,接下来的几个字节是

  • 0x0001:访问标记是ACC_PUBLIC

  • 0x0007:名称索引指向第7个常量:<init>

  • 0x0008:描述符索引指向第8个常量:()V

  • 0x0001:属性有一个

  • 0x0009:属性指向第9个常量,Code

我们正好有疑问,方法定义有了,方法体在哪呢,答案就是上面分析的最后一个Code。下一节就说说属性表集合的各种可能。

8.属性表集合

属性表(attribute_info)在前面已经多次提及,Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景转有的信息。

属性表并没有严格限制顺序,只要不与已有属性名重复,任何人实现的编译器都可以添加自己定义的属性信息,以下是一些预定义的属性:

属性名称

使用位置

含义

SourceFile

ClassFile

记录源文件的名称

InnerClasses

ClassFile

内部类列表

EnclosingMethod

ClassFile

内部类才有这个属性,用于标识这个类所在的外围方法

SourceDebugExtension

ClassFile

用于存储额外的调试信息,JDK1.6中新增

BootstrapMethods

ClassFile

用于保存invokeddynamic指令引用的引导方法限定符,JDK1.7中新增

ConstantValue

field_info

final关键字定义的常量值

Code

method_info

Java代码编译成的字节码指令

Exceptions

method_info

方法抛出的异常

RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations

method_info

指明哪些参数是运行时可见的,哪些是运行时不可见的,JDK1.5中新增

AnnotationDefault

method_info

记录注解类元素的默认值,JDK1.5中新增的

MethodParameters

method_info

记录方法的参数信息,比如它们的名字,访问级别,JDK1.8新增

Synthetic

ClassFile, field_info, method_info

表示方法或字段是编译器自动生成的

Deprecated

ClassFile, field_info, method_info

被声明为deprecated的字段

Signature

ClassFile, field_info, method_info

用于支持泛型情况下的方法签名,在Java语言中,如果任何类、接口、初始化方法或者成员的泛型签名包含了类型变量或者参数化类型,则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息。JDK1.5中新增

RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations

ClassFile, field_info, method_info

为动态注解提供支持,指明哪些是注解是运行时可见的,哪些是运行时不可见的,JDK1.5中新增

LineNumberTable

Code

Java源码的行号与字节码指令的对应关系

LocalVariableTable

Code

方法的局部变量描述

LocalVariableTypeTable

Code

使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加,JDK1.5中新增

StackMapTable

Code

供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作栈所需要的类型是否匹配,JDK1.6新增

RuntimeVisibleTypeAnnotations, RuntimeInvisibleTypeAnnotations

ClassFile, field_info, method_info, Code

记录运行时类型上注解的可见性,也包括运行时类型参数的注解的可见性

下面具体说一说一些比较重要的属性:

Code属性

首先来看Code属性的结构:

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];}
  • attribute_name_index:占两个字节,指向CONSTANT_Utf8_info常量,表示属性名,这里固定是"Code"

  • attribute_length:属性值的长度,由于attribute_name_index和attribute_length占6个字节,所以attribute_length为属性表长度减6

  • max_statck: 操作数栈深度的最大值,在方法执行时,操作数栈不能超过这个值

  • max_locals:局部变量所需的存储空间。max_locals的单位是Slot,Slot是虚拟机为局部变量分配的最小单位,对于byte,char,float,int,short,boolean和returnAddress等长度不超过32位的数据类型,都只占一个slot,而double和long 这种64为的数据都是需要占用两个slot。方法参数(包括隐藏的this)、异常处理器的参数、方法体定义的局部变量都需要局部变量表来存放。但是max_locals并不是所有局部变量所占的slot之和,因为slot可以重用,当一个变量超出作用域了,该slot又会给别的局部变量使用,编译器会根据作用域计算max_locals。

  • code_length: 编译器编译后的字节码长度

  • code: 用于存储字节码指令的一系列字节流,每个指令是一个u1类型的单字节,当虚拟机读到该字节时,就可以知道是什么指令,知道是什么指令,就知道指令需要什么操作数,继续读就可以了,这里类似于汇编,u1的取值范围是0~255,可以表达256条指令。Java虚拟机规范中定义了约200条指令,参看Instructions。关于这部分内容以后再写博客介绍了。

  • exception_table_length:异常表的长度

  • exception_table:异常表对于Code来说并不是必须存在的,所以上述长度也可以为0,异常表有4个属性,代表着如果在start_pc到end_pc之间出现catch_type类型的异常,就跳转到handler_pc所指向的行处理。

Exceptions属性

Exceptions属性在方法表中与Code属性平级,注意和上面Code中的异常表不同,Exceptions属性的作用是列出方法可能抛出的异常,Exceptions属性表的结构:

Exceptions_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 number_of_exceptions;    u2 exception_index_table[number_of_exceptions];}
  • number_of_exceptions: 可能抛出的异常种类的个数

  • exception_index_table:指向常量池中CONSTANT_Class_info的索引

LineNumberTable属性

LineNumber用来记录Java源码与字节码行号之间的对应关系,我们在编译代码时也可以使用-g: none-g: line来取消生成这个属性,不过在调试代码时就看不到行号了,也无法打断点。

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];}

我们主要看line_number_table,start_pc是字节码行号,line_number是Java源码行号。

LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,我们在编译代码时也可以使用-g: none-g: vars来取消生成这个属性,但是如果取消的话,IDE会用arg0,arg1这样的参数来取代原有的参数名,导致调试时不清晰。

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];}

主要介绍local_variable_table:

  • start_pc和length:分别代表了这个局部变量的生命周期开始的字节码偏移量以及作用范围覆盖的长度

  • name_index和descriptor_index:分别指向代表局部变量名称和局部变量描述符的常量

  • index:是该局部变量在局部变量表中的slot位置,如果变量时double 或者long类型的,占用的slot为index和index+1两个。

ConstantValue属性

ConstantValue是一个定长属性,用来通知虚拟机为静态变量赋值,如果同时定义了int x=3;static int y=3;则虚拟机为x,y赋值的时机不同,对于x,是在实例构造器<init>中进行的,而static类型的变量,则会在类构造器<clinit>方法中或者使用ConstantValue属性。

目前javac编译器的规则是,如果同时有final和static修饰,则是使用ConstantValue属性,只有static时,并且变量类型是基本类型或者String时,就会在<clinit>中进行初始化。

InnerClasses属性

如果类中定义了内部类,则会使用InnerClasses属性来记录内部类和宿主的关系。

InnerClasses的数据结构如下:

InnerClasses_attribute {    u2 attribute_name_index;    u4 attribute_length;    u2 number_of_classes; //记录有多少个内部类    { u2 inner_class_info_index;        u2 outer_class_info_index;        u2 inner_name_index;        u2 inner_class_access_flags;    } classes[number_of_classes];}

还是只看classes字段,inner_class_info_index指向内部类的符号引用,outer_class_info_index指向宿主类的符号引用,inner_name_index指向内部类的名称,如果是匿名内部类,则为0,inner_class_access_flags是内部类的访问标志,见下表:

标志名称

标志值

含义

ACC_PUBLIC

0x0001

Marked or implicitly public in source.

ACC_PRIVATE

0x0002

Marked private in source.

ACC_PROTECTED

0x0004

Marked protected in source.

ACC_STATIC

0x0008

Marked or implicitly static in source.

ACC_FINAL

0x0010

Marked final in source.

ACC_INTERFACE

0x0200

Was an interface in source.

ACC_ABSTRACT

0x0400

Marked or implicitly abstract in source.

ACC_SYNTHETIC

0x1000

Declared synthetic; not present in the source code.

ACC_ANNOTATION

0x2000

Declared as an annotation type.

ACC_ENUM

0x4000

Declared as an enum type.

还有其它的一些属性,如果想了解,可以看一下参考资料。

参考资料:

  1. Java Virtual Machine Specification

  2. Java虚拟机:JVM高级特性与最佳实践

扫码关注最新动态

关键时刻,第一时间送达

- END -

点个在看是最大的支持 

转载地址:http://ifjfb.baihongyu.com/

你可能感兴趣的文章
深入理解JVM虚拟机6:深入理解JVM类加载机制
查看>>
深入了解JVM虚拟机8:Java的编译期优化与运行期优化
查看>>
深入理解JVM虚拟机9:JVM监控工具与诊断实践
查看>>
深入理解JVM虚拟机10:JVM常用参数以及调优实践
查看>>
深入理解JVM虚拟机12:JVM性能管理神器VisualVM介绍与实战
查看>>
深入理解JVM虚拟机13:再谈四种引用及GC实践
查看>>
Spring源码剖析1:Spring概述
查看>>
Spring源码剖析2:初探Spring IOC核心流程
查看>>
Spring源码剖析5:JDK和cglib动态代理原理详解
查看>>
Spring源码剖析6:Spring AOP概述
查看>>
Spring源码剖析8:Spring事务概述
查看>>
Spring源码剖析9:Spring事务源码剖析
查看>>
重新学习Mysql数据库1:无废话MySQL入门
查看>>
探索Redis设计与实现2:Redis内部数据结构详解——dict
查看>>
探索Redis设计与实现3:Redis内部数据结构详解——sds
查看>>
探索Redis设计与实现4:Redis内部数据结构详解——ziplist
查看>>
探索Redis设计与实现6:Redis内部数据结构详解——skiplist
查看>>
探索Redis设计与实现5:Redis内部数据结构详解——quicklist
查看>>
探索Redis设计与实现8:连接底层与表面的数据结构robj
查看>>
探索Redis设计与实现7:Redis内部数据结构详解——intset
查看>>