Jvm(40),class文件结构----- 属性表集合(这里内容比较多,等jvm写完之后看这里还有什么补充的吗)...

 

属性表集合

属性表(attribute_info)在前面的讲解之中已经出现过数次,在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

例如在39,class文件结构----- 方法表集合 我们讲到的exception属性就属于方法表属性。

38,class文件结构----字段表集合讲到的ConstantValue类型的属性表就属于字段表属性。

在这里我们可以不管是class文件,还是字段表集合还是方法表集合,只要需要用到属性的地方,都会有属性表集合的。下面我们来看张表。

 

 

上面的表我们可以看到所有的属性表的集合,并且它属于哪个使用位置的。由于属性表的东西太多,所以这里我们重点来讲一下方法集合中的code属性,这个最难,也是最有代表性的。

属性表(attribute_info)在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有信息。对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf_info 类型

的常量来表示,而属性值的结构则是完全自定义的,只需要说明属性所占用的长度即可,一个规范的属性表应该满足下面所示结构。下面的图这个我们可以从前面的exception属性表结构中看出来

由于网上没有找到合适的教程,我们就以深入理解java虚拟机额这本书来管中窥豹一下。

Code属性

Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构

将如表6-15所示。

1,attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为"Code",它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。

2,max_stack代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧

(StackFrame)中的操作栈深度。

3,max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、 boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而 double和long这两种64位的数据类型则需要两个Slot来存放。方法参数(包括实例方法中的隐藏参数"this")、显式异常处理器的参数(Exception Handler Parameter,就是

try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小

4,code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要跟随参数,以及参数应当如何理解。我们知道一个u1数据类型的取值范围为0x00~0xFF,对应十进制的0~255,也就是一共可以表达256条指令,目前,Java虚拟机规范已经定义了其中约200条编码值对应的指令含义,编码与指令之间的对应关系可查阅本书的附录B"虚拟机字节码指令表"。

关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达到232-1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度,如果超过这个限制,Javac编译器也会拒绝编译。一般来讲,编写Java代码时只要不是刻意去编写一个超长的方法来为难编译器,是不太可能超过这个最大值的限制。但是,某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败。

5,Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码

Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学习后面关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能,因此笔者准备了一个比较详细的实例来讲解虚拟机是如何使用这个属性的。

package org.fenixsoft.clazz; public class TestClass{ private int m;

public int inc(){

return m+1;

}}

继续以代码清单6-1的TestClass.class文件为例,如图6-10所示,这是上一节分析过的实例构造器"<init>"方法的Code属性。它的操作数栈的最大深度和本地变量表的容量都为

0x0001,字节码区域所占空间的长度为0x0005。虚拟机读取到字节码区域的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译"2A

B700 0A B1"的过程为

  1. 读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
  2. 读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、 private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方

法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。

  1. 读入00 0A,这是invokespecial的参数,查常量池得0x000A对应的常量为实例构造器"<init>"方法的符号引用。
  2. 读入B1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。

这条指令执行后,当前方法结束。

这段字节码虽然很短,但是至少可以看出它的执行过程中的数据交换、方法调用等操作都是基于栈(操作栈)的。我们可以初步猜测:Java虚拟机执行字节码是基于栈的体系结构。但是与一般基于堆栈的零字节指令又不太一样,某些指令(如invokespecial)后面还会带有参数,关于虚拟机字节码执行的讲解是后面两章的重点,我们不妨把这里的疑问放到第8章去解决。

我们再次使用javap命令把此Class文件中的另外一个方法的字节码指令也计算出来,结果如代码清单6-4所示。代码清单6-4 用javap命令计算字节码指令

//原始Java代码

public class TestClass{ private int m; public int inc(){

return m+1;

}}C

:\>javap-verbose TestClass

//常量表部分的输出见代码清单6-1,因版面原因这里省略掉

{public org.fenixsoft.clazz.TestClass();

Code:

Stack=1,Locals=1,Args_size=1

0:aload_0

1:invokespecial#10;//Method java/lang/Object."<init>":()V

4:return

LineNumberTable:

line 3:0

LocalVariableTable:

Start Length Slot Name Signature

0 5 0 this Lorg/fenixsoft/clazz/TestClass;

public int inc();

Code:

Stack=2,Locals=1,Args_size=1

0:aload_0

1:getfield#18;//Field m:I

4:iconst_1

5:iadd

6:ireturn

LineNumberTable:

line 8:0

LocalVariableTable:

Start Length Slot Name Signature

0 7 0 this Lorg/fenixsoft/clazz/TestClass;

如果大家注意到javap中输出的"Args_size"的值,可能会有疑问:这个类有两个方法

——实例构造器<init>()和inc(),这两个方法很明显都是没有参数的,为什么

Args_size会为1?而且无论是在参数列表里还是方法体内,都没有定义任何局部变量,那

Locals又为什么会等于1?如果有这样的疑问,大家可能是忽略了一点:在任何实例方法里面,都可以通过"this"关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实

现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果代码清单6-1中的inc()方法声明为static,那Args_size就不会等于1而是等于0了。

就是相当于可以调用this的关键字,在任何非静态的方法中。

在字节码指令之后的是这个方法的显式异常处理表(下文简称异常表)集合,异常表对于 Code属性来说并不是必须存在的,如代码清单6-4中就没有异常表生成。异常表的格式如表6-16所示,它包含4个字段,这些字段的含义为:如果当字节码在第start_pc行[1]到第 end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其子类的异常

(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。

异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常及finally处理机制[2]。代码清单6-5是一段演示异常表如何运作的例子,这段代码主要演示了在字节码层面中try-catch-finally是如何实现的。在阅读字节码之前,大家不妨先看看下面的Java源码,想一下这段代码的返回值在出现异常和不出现异常的情况下分别应该是多少?代码清单6-5 异常表运作演示

//Java源码

public int inc(){ int x; try{

x=1;

return x;

}catch(Exception e){ x=2;

return x;

}finally{ x=3;

}}/

/编译后的ByteCode字节码及异常表

public int inc();

Code:

Stack=1,Locals=5,Args_size=1

0:iconst_1//try块中的x=1

1:istore_1

2:iload_1//保存x到returnValue中,此时x=1

3:istore 4

5:iconst_3//finaly块中的x=3

6:istore_1

7:iload 4//将returnValue中的值放到栈顶,准备给ireturn返回

9:ireturn

10:astore_2//给catch中定义的Exception e赋值,存储在Slot 2中

11:iconst_2//catch块中的x=2

12:istore_1

13:iload_1//保存x到returnValue中,此时x=2

14:istore 4

16:iconst_3//finaly块中的x=3

17:istore_1

18:iload 4//将returnValue中的值放到栈顶,准备给ireturn返回

20:ireturn

21:astore_3//如果出现了不属于java.lang.Exception及其子类的异常才会走到这里

22:iconst_3//finaly块中的x=3

23:istore_1

24:aload_3//将异常放置到栈顶,并抛出

25:athrow

Exception table: from to target type

0 5 10 Class java/lang/Exception

0 5 21 any

10 16 21 any

编译器为这段Java源码生成了3条异常表记录,对应3条可能出现的代码执行路径。从Java 代码的语义上讲,这3条执行路径分别为:

如果try语句块中出现属于Exception或其子类的异常,则转到catch语句块处理。

如果try语句块中出现不属于Exception或其子类的异常,则转到finally语句块处理。

如果catch语句块中出现任何异常,则转到finally语句块处理。

返回到我们上面提出的问题,这段代码的返回值应该是多少?对Java语言熟悉的读者应该很容易说出答案:如果没有出现异常,返回值是1;如果出现了Exception异常,返回值是2;如果出现了Exception以外的异常,方法非正常退出,没有返回值。我们一起来分析一下字节码的执行过程,从字节码的层面上看看为何会有这样的返回结果。

字节码中第0~4行所做的操作就是将整数1赋值给变量x,并且将此时x的值复制一份副本到最后一个本地变量表的Slot中(这个Slot里面的值在ireturn指令执行前将会被重新读到操作栈顶,作为方法返回值使用。为了讲解方便,笔者给这个Slot起了个名字: returnValue)。如果这时没有出现异常,则会继续走到第5~9行,将变量x赋值为3,然后将之前保存在returnValue中的整数1读入到操作栈顶,最后ireturn指令会以int形式返回操作栈顶中的值,方法结束。

如果出现了异常,PC寄存器指针转到第10行,第10~20行所做的事情是将2赋值给变量x,然后将变量x此时的值赋给returnValue,最后再将变量x的值改为3。方法返回前同样将

returnValue中保留的整数2读到了操作栈顶。从第21行开始的代码,作用是变量x的值赋为

3,并将栈顶的异常抛出,方法结束。

异常表

异常表就是方法中的异常处理内容,try catch代码块。它的结构如下所示:

它表示,如果字节码从start_pc行到end_pc行之间出现类型为catch_type或其子类的异常,则转到handler_pc行继续处理。具体字节码指令不再分析。

Exceptions属性

Exceptions是在方法表中与Code属性平级的一项属性,与异常表不一样,异常表是Code的下级属性。Exceptions属性的作用是列举出方法中可能抛出的受查异常,也就是 throws 关键字后列表的异常,它的结构表如下:

number_of_exceptions项表示方法有可能抛出多少种异常,每一种异常由一个 exception_index_table项表示,exception_index_table是一个指向常量池中 utf8 类型的索引。

LineNumberTable属性

LineNumberTable属性用于描述 Java 源码行号与字节码行号 之间的对应关

系。它不是必须属性,如果不生成它,那么产生异常后,堆栈中将不会显示出错的行号,并且在调试时也无法在源码中设置断点。

line_number_table是一个数量为line_number_table_length、类型为 line_number_info的集合,line_number_info表包括了start_pc和

line_number两个u2数据项,前者是字节码行号,后者 是Java源码行号。

查看javap内容:

javap中显示 line 8:0,第8行正好是return所在的行号。

LocalVariableTable属性

LocalVariableTable用于描述栈桢中局部变量表中的变量与Java源码中定义的变量之间的关系。也不是必须的,它的结构如下:

其中local_variable_info项目代表了一个栈桢与源码中局部变量的关联,结构如下:

start_pc和length属性分别代表这个局部变量的生命周期的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

name_index和descriptor_index都是指向常量池中 utf8 类型的常量,分别代表局部变量的名称及局部变量的描述符。

index是这个局部变量在栈桢局部变量表中slot的位置,如果是64位类型,则它占用的slot为index 和 index+1的两个位置。

如果将inc方法中添加一个参数,如下图所示:

publicintinc(int a){ return m + 1;}

查看javap内容 LocalVariableTable说明了方法内局部变量的名字,占用空间以及描述符(参数是什么类型等等)。

SourceFile属性

SourceFile属性,用于记录Class文件的源码文件名称。

其它

属性表还有几个属性,比如 ConstantValue和 Inner Class。可以自己写代码,查看javap内容学习。值得一提的是,对于类变量也就是static变量,如果变量被static和final同时修饰并且是基本数据类型或者String类型的话,那么此变量使用 ConstantValue 属性进行初始化,如果此变量没有被final修饰,或者并非基本类型及字符串,则选择在 方法中进行初始化。

总结

Class文件结构真心复杂,很多都是要记忆的,也很枯燥,但是要学习虚拟机原理,必须清楚Class文件的结构,幸好有javap这个工具,多看看javap信息,有助于理解Class文件内容。

 

假如你现在还在为自己的技术担忧,假如你现在想提升自己的工资,假如你想在职场上获得更多的话语权,假如你想顺利的度过35岁这个魔咒,假如你想体验BAT的工作环境,那么现在请我们一起开启提升技术之旅吧,详情请点击http://106.12.206.16:8080/qingruihappy/index.html

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为什么要学JVM1、一切JAVA代码都运行在JVM之上,只有深入理解虚拟机才能出更强大的代码,解决更深层次的问题。2、JVM是迈向高级工程师、架构师的必备技能,也是高薪、高职位的不二选择。3、同时,JVM又是各大软件公司笔试、面试的重中之重,据统计,头部的30家互利网公司,均将JVM作为笔试面试的内容之一。4、JVM内容庞大、并且复杂难学,通过视频学习是最快速的学习手段。课程介绍本课程包含11个大章节,总计102课时,无论是笔试、面试,还是日常工作,可以让您游刃有余。第1章 基础入门,从JVM是什么开始讲起,理解JDK、JRE、JVM的关系,java的编译流程和执行流程,让您轻松入门。第2章 字节码文件,深入剖析字节码文件的全部组成结构,以及javap和jbe可视化反解析工具的使用。第3章 类的加载、解释、编译,本章节带你深入理解类加载器的分类、范围、双亲委托策略,自己手类加载器,理解字节码解释器、即时编译器、混合模式、热点代码检测、分层编译等核心知识。第4章 内存模型,本章节涵盖JVM内存模型的全部内容,程序计数器、虚拟机栈、本地方法栈、方法区、永久代、元空间等全部内容。第5章 对象模型,本章节带你深入理解对象的创建过程、内存分配的方法、让你不再稀里糊涂。第6章 GC基础,本章节是垃圾回收的入门章节,带你了解GC回收的标准是什么,什么是可达性分析、安全点、安全区,四种引用类型的使用和区别等等。第7章 GC算法与收集器,本章节是垃圾回收的重点,掌握各种垃圾回收算法,分代收集策略,7种垃圾回收器的原理和使用,垃圾回收器的组合及分代收集等。第8章 GC日志详解,各种垃圾回收器的日志都是不同的,怎么样读懂各种垃圾回收日志就是本章节的内容。第9章 性能监控与故障排除,本章节实战学习jcmd、jmx、jconsul、jvisualvm、JMC、jps、jstatd、jmap、jstack、jinfo、jprofile、jhat总计12种性能监控和故障排查工具的使用。第10章 阿里巴巴Arthas在线诊断工具,这是一个特别小惊喜,教您怎样使用当前最火热的arthas调优工具,在线诊断各种JVM问题。第11章 故障排除,本章会使用实际案例讲解单点故障、高并发和垃圾回收导致的CPU过高的问题,怎样排查和解决它们。课程资料课程附带配套项目源码2个159页高清PDF理论篇课件1份89页高清PDF实战篇课件1份Unsafe源码PDF课件1份class_stats字段说明PDF文件1份jcmd Thread.print解析说明文件1份JProfiler内存工具说明文件1份字节码可视化解析工具1份GC日志可视化工具1份命令行工具cmder 1份学习方法理论篇部分推荐每天学习2课时,可以在公交地铁上用手机进行学习。实战篇部分推荐对照视频,使用配套源码,一边练习一遍学习。课程内容较多,不要一次性学太多,而是要循序渐进,坚持学习。      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值