JVM(六)Class 文件结构及字节码指令
一、class 文件结构
Java 技术之所以能保持非常好的向后兼容性,这点原因和 Class 文件结构有很大的关系。虽然 Java 到目前位置以及发展了很多的版本了,但是 Class 文件结构的内容在 JDK 1.2 的时代就已经定义好了,即使现在已经经历了很多的版本也只是在原来的基础上新增内容、扩充功能,并没有修改定义的内容。
首先使用 Sublime Text 软件打开一个 Class 文件,可以看到一堆看不懂的内容,其实这个就是一个 Class 的真实内容——都是以二进制字节流所组成的一个文件。
Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节(一个字节是由两位 16 进制数组成)、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。
Class 文件格式详解
Class 文件不存在间隔符号那么,JVM 是怎么识别其中的内容的呢?其实 Class 文件读取的时候是被严格的限制的,位置、数量、哪个字节代表什么含义都是不允许改变的。
-
魔数
每个 Class 文件的开头四个字节ca fe ba be
被称为魔数,它的唯一作用就是确定这个文件是不是能被 JVM 所接受。
之后的四个字节00 00 00 34
则代表着 Class 文件的版本号:00 00
字节是次版本号(MinorVersion),00 34
字节是主版本号(Major Version)。 Java 的版本号是从 45 开始的,JDK 1.1 之后的每个 JDK 大版本发布主版本号向上加 1 高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版 本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。 代表 JDK1.8(16 进制的 34,换成 10 进制就是 52)。
-
常量池
常量池中的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池中的常量个数(constant_pool_count),这里十六进制转成十进制后需要减一才是常量池中的总个数,而且计数是从 1 开始的。
常量池里面主要存放两个大类:字面量(Literal)和符号引用(Symbolic References)。
字面量就是常量里具体的值(包括 final 修饰的)。
符号引用就是在之前文章中讲到的类的权全限定名、字段的名称和描述符等等。。
用 jclasslib 插件可以看到。
-
访问标志
用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口、是否定义为 public 类型、是否定义为 abstract 类型,如果是类的话,是否被声明为 final 等。
-
类索引、父类索引与接口索引集合
这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承, 所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序 从左到右排列在接口索引集合中。
-
字段表集合
描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。
而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本 Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问 性,会自动添加指向外部类实例的字段。
-
方法表集合
描述了方法的定义,但是方法里的 Java 代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。 与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”
-
属性集合
存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中。
二、异常表
JVM 是如何做到当代码出现错误的时候能抛出对应的错误的呢?为什么 finally 中的代码不管如何都会被执行呢? 因为在 JVM 中是存在一个异常表的存在,会根据哪一行到哪一行代码错误后会直接跳转到对应的字节码指令集处顺利的执行下去。
具体代码:
public class ExceptionTest {
synchronized void test01(){
System.out.println("test01");
}
synchronized void test02(){
System.out.println("test02");
}
final Object lock = new Object();
void doLock() {
synchronized (lock) {
System.out.println("lock!");
}
}
}
编译之后:
从字节码指令中可以看到,其中包含了三条monitorexit
指令,这个是为了保证所有的异常条件可以成功的退出。
往下看可以看到一个 Exception table:
form 指定字节码指令索引开始位置
to 指定字节码索引的结束位置。
target 异常处理的起始位置。
type 异常类型。
其实,从字面意思来也可以看出来,例如:7~17 行直接发生了异常则会直接跳转到 20 行。
finally实例:
代码如下:
public int getNum(){
try {
int a = 100/0;
return a;
}finally {
return 100;
}
}
编译后:
从实例中可以看出来其实 finally 的实现就是异常表控制的。
三、字节码指令——装箱拆箱
Java 中是有 8 种基本数据类型的以及对应的包装类,比如 int 和 Integer ,因为包装类是可以赋值 null 而有的时候就需要用到 null 值,而且数据库中是有 null 值的,所以一般用在类的属相上面。
具体代码:
package com.test.demo;
public class JVMTest {
public int getNum() {
Integer num = 9999;
int num2 = num * 3;
return num2;
}
public static void main(String[] args) {
}
}
cmd
中执行 javap -v .\JVMTest.class
方法的字节码指令集里面可以看到相关注释,先是调用了Integer.ValueOf()
的方法之后才进行的一个运算,从这里我们可以看出来确实是进行了自动的拆箱了。
之后我们看下Integer.ValueOf()
的源代码:
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
从源代码里面我们可以看到是有一个IntegerCache
的一个存在而它的最小值写死为 -128,而最大值却没有写死。
其实Integer的最大值我们是可以通过虚拟机参数的方式进行一个设置-XX:AutoBoxCacheMax
。
public class BoxCache {
public static void main(String[] args) {
Integer num1 = 123;
Integer num2 = 123;
Integer num3 = 123;
Integer num4 = 123;
System.out.println(num1 == num2);
System.out.println(num3 == num4);
}
}
//那么上面如果在没有设置 -XX:AutoBoxCacheMax 参数的情况下为: true,false
//但是设置的数字大于等于 128 那么都会是true
2、数组
其实数组这个数据类型是 JVM 内置的一种对象类型,在 Java 中是不存在数组的源代码的。
public class ArrayDemo{
int getValue() {
int[] arr = new int[]{1111,2222,3333,4444};
return arr[2];
}
int getLength(int[] arr) {
return arr.length;
}
}
经过反编译之后:
可以看到在新建数组的时候是采用的newarray + 数据类型
的指令进行创建的,之后数组内的元素都是一个个压入进去的。
而数组元素的访问则是在 28 ~ 30 行代码进行实现的。 aload_1
将第二个引用类型推送到栈中 ,之后在iconst_2
再往栈中压入常量 2,最后在iaload
获取数组对应下标的元素并返回。
而数组长度的获取就更简单了:
直接调用arraylength
之后返回就可以了。
3、foreach
增强 for 循环在代码中是比较常见的一个循环,其实集合使用 foreach 则在编译后会被编译成迭代器的方式进行一个循环。
手动编写的代码:
public void foreach(int[] arr) {
for (int i : arr) {
System.out.println(i);
}
}
public void foreach2(List<Integer> list) {
for (int i : list) {
System.out.println(i);
}
}