JVM系列文章目录
Class文件结构及深入字节码指令
前言
本文基于JDK1.8,是博主个人的JVM学习记录,欢迎各位指正错误的地方
Class文件
JVM无关性
大家都知道Java一处编译,到处运行(平台无关性,归功于JVM)。JVM的语言无关性则归功于Class文件,因为JVM只与Class文件绑定,不是与Java文件绑定,换句话说任何能被编译成class文件的语言都可以被JVM解析。
Class类文件
各位有兴趣可以深入了解一下,在了解之前,各位得先准备点工具方便接下来查看Class类文件。
工具介绍
- Sublime:查看16进制Class源文件
- javap:这个是JDK自带的反编译工具,将Class文件解析成可读的文件格式。一般使用- v参数查看信息即可,当然你也可以使用-p 参数查看一些私有的字段和方法。
javap -v class文件名
- jclasslib:这是一个可视化的工具,它会将信息分门别类,方便更直观的查看Class文件信息。如果你是用的是idea的话,可以在插件市场(plugins)上搜索到它,将其作为插件安装即可。
Class文件格式
这里我先贴一张图,如果看不清的话我这边同时也在CSDN上传了该图的资源(Class文件结构)。
根据根据上面的图,在结合我下面提供的代码和Class文件解析来对照说明。
代码如下:
package ex6;
/**
* @author abfeathers
* 字节码分析
*/
public class ByteCode {
public ByteCode(){
}
}
在Sublime中打开编译好的Class文件,显示如下:
它使用一种类似于c语言的伪数据结构来存储数据,这种数据结构中只有两种类型:无符号数和表。
无符号数:属于基本数据类型,以U1(一个字节,一个字节是两位16进制数据)、U2(两个字节)、U4(四个字节)、U8(8个字节)。
结合上文的Class类结构图,我这个特别介绍以下几个类型:
-
magic:魔数,U4(占4个字节),如下图的cafe babe ,这个无符号数的意义在于告诉虚拟机这是一个Class文件。
-
minor_version和major_version:minor_version,U2(占两个字节),表示小版本号;major_version,U2(占两个字节),表示大版本号;这两个无符号数,共同表示了JDK版本号,0000 0034 换算成十进制就是52,表示的是JDK1.8。
-
constant_pool_count:常量池计数器,U2(占两个字符),表示这个类文件中有多少个常数项,常量池计数器是从1开始的,1表示没有常量,2表示有一个常量,以此类推。这里0010换算成十进制是16,表示有15项常量。
我们可以用javap校验一下
-
constant_pool:常量池,主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。 字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。 而符号引用则属于编译原理方面的概念,包括了下面三类常量:
类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。 -
access_flags:访问标志,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。
-
this_class、super_class、interfaces_count、interfaces:类索引、父类索引与接口索引集合,这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承, 所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。接口索引 集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序 从左到右排列在接口索引集合中。
-
fields:字段表集合,描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。 而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。 字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本 Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问 性,会自动添加指向外部类实例的字段。
-
methods:方法表集合
描述了方法的定义,但是方法里的 Java 代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。 与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“”。 -
attributes:属性表集合
存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中。
Class类文件(字节码)小结
- Class文件是一组以8位字节为基础单位的二进制流。
- 类似于结构体的伪结构来存储数据。
- 只有两种数据类型:无符号数和表。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型。
字节码指令
这一块各位也是有兴趣可以看一看,我这里挑几个出来讲一讲,想了解详细指令的话可以看这个链接:字节码助记码解释地址。
异常处理
异常机制
如果你熟悉Java开发,一定对下图不陌生。其中Error和RuntimeException是非检查型异常,不需要catch去处理。但是其他异常就需要开发人员自己区处理了。
异常表
正常方法执行的时候,是封装成栈帧,压入虚拟机栈,在虚拟机栈进行一系列运算,最后根据完成出口出栈。那么异常的话呢?异常的话就需要依靠异常表了。
异常表,就是当程序出现异常的时候,告诉计算机该怎么处理的表。这里我举一个例子。
代码如下:
/**
* @author: abfeathers
* @date: 2021/3/13
* @description: 在 synchronized 生成的字节码中,其实包含两条 monitorexit 指令,是为了保证所有的异常条件,都能够退出
* 这就涉及到了 Java 字节码的异常处理机制
*/
public class SynchronizedDemo {
synchronized void m1(){
System.out.println("m1");
}
static synchronized void m2(){
System.out.println("m2");
}
final Object lock=new Object();
void doLock(){
synchronized (lock){
System.out.println("lock");
}
}
}
我们通过jclasslib来查看字节码信息,也可以通过javap校验一下(以javap的结果为准,毕竟javap才是官方的)。
这里介绍几个名词:
- from :指定字节码索引开始的位置,如上图from 是7 表示的就是这一行。
- to:字节码索引的结束位置,如上图是17,则表示这一行。
- target:如果出现异常,跳转到这里,如上图的20。
- type:异常类型
再结合我上面的Java代码,就可以看出来了,被synchronized修饰的方法,正常执行就走自己的 monitorexit退出,如果异常就跳转到异常处理,最后走异常的 monitorexit退出。
Finally
在平时开发的过程中,我们有时候做try-catch处理的时候无论是否会出现异常都要做一些处理,比如说操作流数据的时候无论如何都要关闭流,这个时候就会使用Finally,那么Finally在字节码中是怎么样的呢?
我们先写一个小demo
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author: abfeathers
* @date: 2021/3/13
* @description: finally字节码的处理
*/
public class StreamDemo {
public void read(){
InputStream in = null;
try {
in = new FileInputStream("Box.java");
}catch(FileNotFoundException e){
e.printStackTrace();
} finally {
if (null != in) {
try {
in.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
}
}
在代码中我们捕获了FileNotFoundException,在finally中关闭流的时候我们又捕获了IOException。在查看字节码的时候我发现一个很有意思的地方,IOException足足出现了三次。
我们再看看异常表
这里就可以看出来Java底层直接将异常处理复制了2份,分别塞到try和catch里,这就很硬核了。
我们也从代码来进行分析:
/**
* @author: abfeathers
* @date: 2021/3/13
* @description: 装箱拆箱字节码层面分析
*/
public class Box {
public Integer cal() {
Integer a = 1000;
int b = a * 10;
return b;
}
}
拆箱装箱
拆箱装箱这一个很简单的概念了,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
再看看jclasslib中的代码
注意看我红色方框,在对照代码,是不是就恍然大悟了?
原来在Java编译的时候,直接封装成了这种指令,1000先读入操作数栈中,再将1000直接调用Integer.valueOf方法进行装箱变成Integer,对照代码int b = a * 10,这里先将a调用Integer.intValue进行拆箱转换成int,在进行计算,最后要返回的时候又将b调用Integer.valueOf转换成Integer。
Integer拓展-IntegerCache
根据上文的拆箱装箱的讲解,我再提供一个代码示例,这个示例特别有意思,大家都知道Integer是一个对象,当int自动装箱成Integer的时候会new一个Integer对象,那么下面这段代码的执行结果是什么?
/**
* @author: abfeathers
* @date: 2021/3/13
* @description: IntegerCache及修改 -XX:AutoBoxCacheMax=256
*
*/
public class BoxCache {
public static void main(String[] args) {
Integer n1 = 123;
Integer n2 = 123;
Integer n3 = 128;
Integer n4 = 128;
System.out.println(n1 == n2);
System.out.println(n3 == n4);
}
}
这个结果是不是特别意外?来我们看看字节码
根据字节码,发现可能问题处在Integer.valueOf中,我们来看看jdk的源码实现:
嗯?IntegerCache?Integer对象还有缓存?我们同上面的注释了解到,原来对于数值有一个缓存范围,下限(low)是定死的-128,上限(heigh)是可变的,如果没有设置的话默认是127,所以在这个范围内的数值都是会被缓存的。再看看上面的代码123明显在这个范围内,所以n1、n2实际上是引用的缓存里的同一个对象,而128明显不在这个范围内,所以n3、n4是引用的两个不同的对象。
既然是可以参数设置的,我们来通过参数-XX:AutoBoxCacheMax
设置一下,再来看看运行结果。
是不是很有意思?
数组
数组实际上是JVM的一个内置对象,这个对象同样是继承的 Object 类。我们使用代码来理解一下:
/**
* @author: abfeathers
* @date: 2021/3/13
* @description: 数组的字节码解析
*/
public class ArrayDemo {
int getValue() {
int[] arr = new int[]{1111, 2222, 3333, 4444};
return arr[2];
}
int getLength(int[] arr) {
return arr.length;
}
}
主要看一下这个getLength方法,这个方法实际上不是JDK的方法,它其实是JVM底层的方法arraylength
foreach
forech这个的话,我们来对照list和数组的实现来看,从Java代码上来看是一样的。
import java.util.List;
/**
* @author: abfeathers
* @date: 2021/3/13
* @description: foreach 底层实现的差异
*/
public class ForDemo {
void loop(int[] arr) {
for (int i : arr) {
System.out.println(i);
}
}
void loop(List<Integer> arr) {
for (int i : arr) {
System.out.println(i);
}
}
}
从Class文件来看的确差异很大,但是不太容易看明白,我们对Class文件来进行一次反编译看看。
这是我从idea中直接查看的,区别很明显吧。(当然也可以通过java指令进行反编译查看)
注解
注解我们直接看方法的注解和类上的注解的区别吧。
我这里先定义一个注解
public @interface AbfeathersAnnotation {
}
然后在写一个测试类
@AbfeathersAnnotation
public class AnnotationDemo {
@AbfeathersAnnotation
public void test(@AbfeathersAnnotation int a){
}
}
这次我们通过javap指令来比较(在jclasslib看不太明显)。
javap -v AnnotationDemo.class
- 无论是类的注解,还是方法注解,都是由一个叫做 RuntimeInvisibleAnnotations 的结构来存储的。
- 参数的存储,是由 RuntimeInvisibleParameterAnotations 来保证的
上一篇:细谈JVM垃圾回收与部分底层实现
下一篇:玩转类加载和类加载器