一、字节码文件概述
1.1 字节码文件是跨平台的吗?
Java虚拟机不和包括Java在内的任何语言绑定,他只与"Class文件"这种特定的二进制文件格式所关联。
无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。
想要让一个Java程序正确的运行在JVM中,Java源码就必须要被编译成符合JVM规范的字节码。
1.2 class文件里是什么?
源文件经过编译器编译之后便会生成一个字节码文件,字节码是一种二进制的类文件,他的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码。
1.3 生成class文件的编译器?
- 从位置上理解
前端编译器 vs 后端编译器
半编译半解释型语言! - 前端编译器的种类
Java源代码的编译结果是字节码,那么肯定需要有一种编译器能够将Java源代码编译成字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译成为字节码的前端编译器。
前端编译器的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。
1.4 前端编译器的局限性
前端编译器并不会直接涉及编译优化等方面的技术,而是将这些具体优化细节讲给HotSpot的JIT编译器负责。
1.5 哪些类型有对应的Class的对象
- class:
外部类,成员(成员内部类,静态内部类), 局部内部类,匿名内部类. - interface: 接口
- []: 数组
- enum: 枚举
- annotation: 注解@interface
- primitive type: 基本数据类型
- void
1.6 字节码指令
1.6.1 字节码指令是什么?
什么是字节码指令(byte code)?
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
比如:
1.6.2 为什么要懂字节码指令?
面试题: i++和++i有什么区别?
@Test
public void test1(){
int i = 10;
//i++;
++i;
System.out.println(i);
}
我们可以通过查看它的字节码文件发现其并没有区别。
面试题: 以下程序运行结果?
@Test
public void test2(){
int i = 10;
i = i++;
System.out.println(i);//
}
结果为10
我们通过查看它的字节码文件就可以知道原因.
bipush 10,将10压入栈底
istore_1,将栈中的10取出放到局部变量表的0索引处
iload_1,将局部变量表中的10放一份到栈中
ilinc 1 by 1,局部变量表中的10加1编程11
istore_1,将栈中的10再次放到局部变量表的0索引处(此时局部变量表中的11又变成了10)
iload_1,再次将局部变量表中的10放一份到栈中
return,返回栈中的数据。
包装类对象的缓存问题
@Test
public void test5(){
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2);//true
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4);//false
Boolean b1 = true;
Boolean b2 = true;
System.out.println(b1 == b2);//true
}
字节码文件
0 bipush 10
2 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
5 astore_1
6 bipush 10
8 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
11 astore_2
12 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
15 aload_1
16 aload_2
17 if_acmpne 24 (+7)
20 iconst_1
21 goto 25 (+4)
24 iconst_0
25 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
28 sipush 128
31 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
34 astore_3
35 sipush 128
38 invokestatic #4 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
41 astore 4
43 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
46 aload_3
47 aload 4
49 if_acmpne 56 (+7)
52 iconst_1
53 goto 57 (+4)
56 iconst_0
57 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
60 iconst_1
61 invokestatic #6 <java/lang/Boolean.valueOf : (Z)Ljava/lang/Boolean;>
64 astore 5
66 iconst_1
67 invokestatic #6 <java/lang/Boolean.valueOf : (Z)Ljava/lang/Boolean;>
70 astore 6
72 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
75 aload 5
77 aload 6
79 if_acmpne 86 (+7)
82 iconst_1
83 goto 87 (+4)
86 iconst_0
87 invokevirtual #5 <java/io/PrintStream.println : (Z)V>
90 return
程序的输出结果
@Test
public void test6(){
String str = new String("hello") + new String("world");
String str1 = "helloworld";
System.out.println(str == str1);//false
}
str会在堆空间创建一个helloworld,str1也会在堆空间创建一个helloworld,然后再放到常量池一份引用,str和str1都指向堆空间的一片区域,故为false。
@Test
public void test6(){
String str = new String("hello") + new String("world");
str.intern();
String str1 = "helloworld";
System.out.println(str == str1);//jdk8:true,jdk6:false
}
jdk8:str在的堆空间创建一个helloworld,str.intern()方法会从常量池中寻找与str等值的string,有则返回其引用,没有,则把当前str的引用放到常量池中。
创建str1时会去常量池中找是否存在与helloworld等值的string,此时已经存在,直接返回了str的引用。
故为true
jdk6:str在的堆空间创建一个helloworld,str.intern()方法会从常量池中寻找与str等值的string,没有会在常量池中创建一个helloworld,并返回其引用,然后str1创建时,常量池中已经有了,就直接返回了常量池中的引用,此时,str和str1的引用不同,一个指向堆空间,一个指向常量池,故返回false。
程序的输出结果
class Father {
int x = 10;
public Father() {
this.print();
x = 20;
}
public void print() {
System.out.println("Father.x = " + x);
}
}
class Son extends Father {
int x = 30;
public Son() {
this.print();
x = 40;
}
@Override
public void print() {
System.out.println("Son.x = " + x);
}
}
public class ByteCodeInterview1 {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.x);
}
}
首先父类变量初始化,然后调用父类构造方法,执行this.print(),print方法被子类重写,调用子类print方法,打印子类变量x,此时x还未初始化,为默认值0,故打印Son.x=0
然后给父类变量x赋值为20,然后子类变量初始化,然后调用子类构造方法,此时子类变量x被初始化为30,故print()方法打印Son.x=30,最后输出f.x为父类的变量x,为20。
1.7 如何解读class文件?
方式一:
Notepad++安装一个HEX-Editor插件。
方式二:
使用javap指令: jdk自带的反编译工具
方式三:
使用IDEA插件: jclasslib bytecode viewer
二、Class文件结构细节
2.1 class文件结构细节概述
2.1.1 面试题: class文件结构有几个部分?
class文件的总体结构如下:
- 魔数
- class文件版本
- 常量池
- 访问标识(或标志)
- 类索引,父类索引,接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
2.2 class文件的魔数是什么?
Magic Number(魔数): class文件的标志
- 每个class文件开头的4个字节的无符号整数称为魔数(Magic Number)
- 他的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的class文件。即: 魔数是class文件的标识符。
- 魔数值固定位0xCAFEBABE。不会改变。
2.3 如何确保高版本的JVM可执行低版本的class文件?
上面的主版本号是16进制的34,转为10进制为52
版本号和Java编译器的对应关系如下表:
- Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上兼容
2.4 常量池
存放所有常量
- 常量池是class文件中内容最为丰富的区域之一。常量池对于class文件中的字段和方法解析也有着至关重要的作用。
- 常量池: 可以理解为class文件之中的资源仓库,他是class文件结构中与其它项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。
- 常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
2.4.1 为什么需要常量池计数器?
constant_pool_count(常量池计数器)
- 由于常量池的数量不固定,时长时短,所以需要防止两个字节来表示常量池容量计数值。
- 常量池容量计数值(u2类型): 从1开始,表示常量池中有多少项常量。即constant_poll_count=1表示常量池中有0个常量项。
- Demo的值为:
其值为0x0016,就是22.
但是实际上只有21项常量。索引为范围1-21。
2.4.2 常量池表
2.4.2.1 字面量和符号引用
常量池主要存放两大类常量: 字面量(Literal)和符号引用(Symbolic References)。
如下表:
2.4.2.2 谈谈你对符号引用、直接引用的理解?
- 符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用和虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
- 直接引用: 直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
2.5 访问标识
访问标识(access_flag、访问标志、访问标记)
- 在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括: 这个class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
2.6 类索引、父类索引、接口索引集合
- 在访问标记后,会指定该类的类别、父类类别以及实现的接口,格式如下:
2.7 字段表集合
2.8 方法表集合
2.9 属性表集合
三、字节码指令集
3.1 概述
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)构成。由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。
由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条。
3.2 指令分类
- 加载与存储指令
- 算数指令
- 类型转换指令
- 对象的创建与访问指令
- 方法调用与返回指令
- 操作数栈管理指令
- 控制转移指令
- 异常处理指令
- 同步控制指令
3.3 方法调用指令
- invokevirtual: 指令用于调用对象的实例方法,根据对象的实际类型进行分派,支持多态。这也是Java语言中最常见的方法分派方式。
- invokeinterface: 指令用于调用接口方法,他会在运行时搜索由特定对象所实现的这个接口方法,并找出适合的方法进行调用。
- invokespecial: 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(构造器)、私有方法和父类方法。这些方法都是静态类型绑定的,不会再调用时进行动态派发。
- invokestatic 指令用于调用命名类中的类方法(static方法)。这时静态绑定的。
- invokedynamic: 调用动态绑定的方法,这个是JDK1.7后新加入的指令。用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。