深入理解Java Class文件格式(九)

经过前八篇关于class文件的博客, 关于class文件格式的内容也基本上讲完了。 本文是关于class文件格式的最后一篇。 在这篇博客中, 将会讲解关于方法的几个属性。 理解这篇博客的内容, 对于理解JVM执行引擎起着重要作用。 关于虚拟机执行引擎有关的内容, 会在本专栏后面的博客中涉及。 


在前面几篇博客中, 我们知道在class文件中描述一个方法, 会使用一个method_info 。 这个method_info中存放了方法的修饰符标志位,还引用了常量池中的项, 这些常量池数据项描述了在当前类中定义的某个方法的方法名, 方法的描述符。 关于这部分的内容, 请参考我之前的博客:深入理解Java Class文件格式(七)  。 


但是method_info中并没有存放方法的字节码, 也就是指令。 我们知道, 对于一个方法来说, 只要它不是抽象的(抽象类中的抽象方法或者接口中的方法), 那么肯定就会存在指令。 那么这些指令存放在哪里呢? 还有, 方法中的异常处理器(try-catch块)是如何在class文件中表述的? 方法声明抛出的异常是如何描述的呢? 如果你对这几个问题感兴趣, 或许你会在这篇博客中找到答案, 或者受到一些启发。 


为了知识的连贯性, 我们首先简单回顾一下method_info的结构, 因为method_info与本文有着密切的关系。method_info 的结构如下:



深入理解Java Class文件格式(七)这篇博客中已经讲解过access_flags , name_index, descriptor_index 。 他们分别描述方法的访问修饰符, 方法名和方法描述符。 从上图可以看出, method_info中还有attributes_count和attributes。 也就是说每个方法可以有另个或多个属性。 本文要讲解的方法中的字节码指令, 异常处理器和方法声明抛出的异常, 都存放在这些属性中。



Code属性


code属性是方法的一个最重要的属性。 因为它里面存放的是方法的字节码指令, 除此之外还存放了和操作数栈,局部变量相关的信息。 所有不是抽象的方法, 都必须在method_info中的attributes中有一个Code属性。下面是Code属性的结构, 为了更直观的展示Code属性和method_info的包含关系, 特意画出了method_info:




下面依次介绍code属性中的各个部分。

和上一篇博客中介绍的其他属性一样,attribute_name_index指向常量池中的一个CONSTANT_Utf8_info , 这个CONSTANT_Utf8_info 中存放的是当前属性的名字 “Code” 。

attribute_length给出了当前Code属性的长度(不包括前六字节)。

max_stack 指定当前方法被执行引擎执行的时候, 在栈帧中需要分配的操作数栈的大小。

max_locals指定当前方法被执行引擎执行的时候, 在栈帧中需要分配的局部表量表的大小。注意, 这个数字并不是局部变量的个数, 因为根据局部变量的作用域不同, 在执行到一个局部变量以外时, 下一个局部变量可以重用上一个局部变量的空间(每个局部变量在局部变量表中占用一个或两个Slot)。 方法中的局部变量包括方法的参数, 方法的默认参数this, 方法体中定义的变量, catch语句中的异常对象。 关于执行引擎的相关内容会在后面的博客中讲到。

code_length指定该方法的字节码的长度, class文件中每条字节码占一个字节。

code存放字节码指令本身, 它的长度是code_length个字节。

exception_table_length 指定异常表的大小

exception_table就是所谓的异常表, 它是对方法体中try-catch_finally的描述。 exception_table可以看做是一个数组, 每个数组项是一个exception_info结构, 一般来说每个catch块对应一个exception_info,编译器也可能会为当前方法生成一些exception_info。 exception_info的结构如下(为了直观的显示exception_info, exception_table和Code属性的关系, 画出了Code属性,的话读者就会更清楚各个数据项之间的位置关系和包含关系):



下面讲解exception_info中的各个字段的意思。

start_pc是从字节码(Code属性中的code部分)起始处到当前异常处理器起始处的偏移量。

end_pc是从字节码起始处到当前异常处理器末尾的偏移量。

handler_pc是指当前异常处理器用来处理异常(即catch块)的第一条指令相对于字节码开始处的偏移量。

catch_type是一个常量池索引, 指向常量池中的一个CONSTANT_Class_info数据项, 该数据项描述了catch块中的异常的类型信息。这个类型必须是java.lang.Throwable的或其子类。

所以可以总结, 一个异常处理器(exception_info)的意思是: 如果偏移量从start_pc到end_pc之间的字节码出现了catch_type描述的类型的异常, 那么就跳转到偏移量为handler_pc的字节码处去执行。如果catch_type为0, 就代表不引用任何常量池项(再回顾一下, 常量池中的项是从1开始计的), 那么这个exception_info用于实现finally子句。

我们一直在介绍Code属性, 只不过刚才进行了一个小插曲, 介绍了Code属性中的exception_table中的exception_info的详细信息。 下面我们继续介绍Code 属性中的其他信息, 希望读者不要被绕晕了 : )

attributes_count 表示当前Code 属性中存在的其他属性的个数。 现在我们知道, class中的属性, 不仅会出现在顶层的class中, 会存在field_info中, 会存在method_info中, 甚至还会出现在属性中。 

attributes可以看做是一个数组, 里面存放了Code属性中的其他属性。 Code 属性中可以出现的其他属性有LineNumberTable和LocalVariableTable 。 这两个属性会在下面介绍。



LineNumberTable属性


LineNumberTable属性存在于Code属性中, 它建立了字节码偏移量到源代码行号之间的联系。 这个属性是可选的, 编译器可以选择不生成该属性。下面是该属性的结构(同样给出了全局的位置关系,LineNumberTable在图的右下角部分):



由于这个属性并不是重点, 我们在此简单的讲述。 

每个LineNumberTable中的line_number_table部分, 可以看做是一个数组, 数组的每项是一个line_number_info , 每个line_number_info 结构描述了一条字节码和源码行号的对应关系。 其中start_pc是这个line_number_info 描述的字节码指令的偏移量, line_number是这个line_number_info 描述的字节码指令对应的源码中的行号。可以看出, 方法中的每条字节码都对应一个line_number_info , 这些line_number_info 中的line_number可以指向相同的行号, 因为一行源码可以编译出多条字节码。



LocalVariableTable属性 


LocalVariableTable 属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系。 这个属性存在于Code属性中。 这个属性是可选的, 编译器可以选择不生成这个属性。该属性的结构如下:(同样给出了全局的位置关系图,LocalVariableTable 在该图的右下角 )



由于这个属性相对不那么重要, 这里只是大概讲解一下。

每个LocalVariableTable 的local_variable_table部分可以看做是一个数组, 每个数组项是一个叫做local_variable_info的结构, 该结构描述了某个局部变量的变量名和描述符, 还有和源代码的对应关系。下面讲解local_variable_info的各个部分:

start_pc是当前local_variable_info所对应的局部变量的作用域的起始字节码偏移量;

length是当前local_variable_info所对应的局部变量的作用域的大小。 也就是从字节码偏移量start_pc 到start_pc+length就是当前局部变量的作用域范围;

name_index指向常量池中的一个CONSTANT_Utf8_info, 该CONSTANT_Utf8_info描述了当前局部变量的变量名;

descriptor_index指向常量池中的一个CONSTANT_Utf8_info, 该CONSTANT_Utf8_info描述了当前局部变量的描述符;

index描述了在该方法被执行时,当前局部变量在栈帧中局部变量表中的位置。 

由此可知, 方法中的每个局部变量都会对应一个local_variable_info 。 


Exceptions属性


首先需要说明, Exceptions属性不是存在于Code属性中的, 它存在于method_info中的attributes中。 和Code属性是平级的。 这个属性描述的是方法声明的可能会抛出的异常, 也就是方法定义后面的throws声明的异常列表, 请不要和上面提到的异常处理器混淆。 异常处理器描述了方法的字节码如何处理异常, 而Exceptions属性描述方法可能会抛出哪些以异常。下面讲解Exceptions属性的结构(左下角为Exceptions属性):



下面讲解Exceptions属性中的信息。 

attribute_name_index和attribute_length就不多说了, 和其他属性是一样的。 

number_of_exceptions是该方法要抛出的异常的个数。 

exceptions_index_table可以看做一个数组, 这个数组中的每一项占两个字节, 这两个字节是对常量池的索引, 它指向一个常量池中的CONSTANT_Class_info。 这个CONSTANT_Class_info描述了一个被抛出的异常的类型。 


总结

到此为止, 和方法相关的属性就介绍完了。 这篇博客讲解的内容相对比较复杂。 下面以一个实例进行验证, 实例代码:

  1. package com.jg.zhang;  
  2.   
  3. public class Test {  
  4.   
  5.     public void test() throws Exception{  
  6.           
  7.         int localVar = 0;  
  8.           
  9.         try{  
  10.               
  11.             Class.forName("com.jg.zhang.Person");  
  12.               
  13.         }catch(ClassNotFoundException e){  
  14.               
  15.             throw e;  
  16.         }finally{  
  17.             System.out.println(localVar);  
  18.         }  
  19.           
  20.     }  
  21. }  

反编译后的test方法部分(省略了常量池等信息):

  1. public void test() throws java.lang.Exception;  
  2.   flags: ACC_PUBLIC  
  3.   Exceptions:  
  4.     throws java.lang.Exception  
  5.   Code:  
  6.     stack=2, locals=4, args_size=1  
  7.        0: iconst_0  
  8.        1: istore_1  
  9.        2: ldc           #18                 // String com.jg.zhang.Person  
  10.        4: invokestatic  #20                 // Method java/lang/Class.forName:(Ljava/lang/String;)Ljava/lang/Class;  
  11.        7: pop  
  12.        8goto          24  
  13.       11: astore_2  
  14.       12: aload_2  
  15.       13: athrow  
  16.       14: astore_3  
  17.       15: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;  
  18.       18: iload_1  
  19.       19: invokevirtual #32                 // Method java/io/PrintStream.println:(I)V  
  20.       22: aload_3  
  21.       23: athrow  
  22.       24: getstatic     #26                 // Field java/lang/System.out:Ljava/io/PrintStream;  
  23.       27: iload_1  
  24.       28: invokevirtual #32                 // Method java/io/PrintStream.println:(I)V  
  25.       31return  
  26.     Exception table:  
  27.        from    to  target type  
  28.            2     8    11   Class java/lang/ClassNotFoundException  
  29.            2    14    14   any  
  30.     LineNumberTable:  
  31.       line 70  
  32.       line 112  
  33.       line 138  
  34.       line 1512  
  35.       line 1614  
  36.       line 1715  
  37.       line 1822  
  38.       line 1724  
  39.       line 2031  
  40.     LocalVariableTable:  
  41.       Start  Length  Slot  Name   Signature  
  42.              0      32     0  this   Lcom/jg/zhang/Test;  
  43.              2      30     1 localVar   I  
  44.             12       2     2     e   Ljava/lang/ClassNotFoundException;  


结合上面的讲解和图解, 再分析反编译的结果, 就一目了然了: 所有的结果是一个method_info, method_info开始处是访问标志信息。 然后是method_info的 Exceptions属性 , Exceptions属性属性下面是Code属性, Code属性中又包括字节码, 异常处理器 ,LineNumberTable属性和LocalVariableTable 属性。 

由于这篇博客讲解的内容大多和方法有关, 所以会直接或间接的和method_info有联系, 最后给出一张全局图, 这样的话, 读者就比较明确, 一个完整的方法, 是如何在class文件中描述的,由于考虑到复杂性, 这些属性或其他数据项中, 对常量池的引用均未画出:




  • 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、付费专栏及课程。

余额充值