面试最怕问到的 Java 虚拟机,是通往高级 Java 开发的必经之路
在 Java 的面试中,很多人最怕的就是被问到有关 Java 虚拟机的问题。
比如:
-
Class 文件的组成结构,Java 的单继承在 Class 文件结构里是如何体现的? -
反射机制是如何实现的,通过类名创建类的实例在虚拟机里是如何操作的? -
反编译的字节码指令在虚拟机里是如何实现的,ldc 与 ldc_w 有何区别? -
如何区分特性是语法糖还是虚拟机的实现,如何识别 try-with-resources, foreach 循环等语法糖? -
方法调用的实现,静态方法调用,私有方法调用,接口方法调用,重载方法调用在实现上的异同点? -
栈帧的操作数栈和本地变量表是什么,指令又是如何操作栈帧的数据的?
对于 Java 程序员来讲,Spring、Maven 都是外在的武功招式,而 JVM 就是内在的内功心法。只有熟悉 Java 虚拟机,了解最底层的原理,才能在遇到各种问题时轻松应对,不至于束手无策,或者一脸懵逼地上网找解决办法。
当线上出现性能问题时,JVM 调优更是不可回避的问题,因此对 JVM 的理解程度也是 Java 程序员水平高低的分水岭之一。所以很多公司在面试高级开发时,JVM 都是必考点之一。
了解 Java 虚拟机最好的方式,莫过于亲手开发一个!今天小编就带你用实战的方式,亲手从零编写 Java 虚拟机!
你将从 while 循环开始,构建基本的解释器,逐步的添加虚拟机功能,通过这种循序渐进的实践方式,领悟到 Java 虚拟机的底层原理。
实验介绍
在本节实验中,将会学习到 javac
,javap
的基本使用,了解到字节码,解释器的基本概念,以及实现一个基础的解释器。
知识点
解释器是什么
javap
命令机器语言与汇编语言
字节码对应的“汇编语言”
解释器的核心实现
解释器,是一种程序,能够把编程语言一行一行解释运行。解释器像是一位“中间人”,每次运行程序时都要先转成另一种语言再作运行,因此解释器的程序运行速度比较缓慢。它不会一次把整个程序翻译出来,而是每翻译一行程序就立刻运行,然后再翻译下一行,再运行,如此不停地进行下去。
从现实来看, CPU 就是个解释器,CPU 通过解释机器码,操作寄存器和内存,进而控制其他的硬件。
我们将要实现的解释器,解释器需要相对应的数据容器,需要什么样的容器是由指令来确定的,JVM 的字节码指令集根据操作数据的不同分为以下几类,具体的 操作数栈,本地变量表,堆,元空间,程序计数器 等概念,后续会进一步补充。
操作操作数栈,比如上方的 iconst_1 指令,后面会出现的 iadd 指令等。
操作本地变量表,如上方的 istore_0,iload_0 指令等。
操作堆,如 new,putfield 等指令。
操作元空间, 如 getstatic 指令。
程序计数器,每个指令都会操作,用来在当前指令结束后,让计数器指向下一个待执行的指令位置。
同 CPU 需要内存的配合一样, 解释器需要一个存取数据的容器,在 JVM 里,这个容器称为栈帧,栈帧内部有三个重要元素,程序计数器,本地变量表,操作数栈,这部分内容可参阅 Frames。
解释器的核心代码如下。
do {
//获取下一个指令
//解释指令
} while //(还有指令);
解释器的核心架构如下图。
下面介绍一下栈帧的三个重要元素。
程序计数器:之前介绍指令时提到的指令位置,就是由程序计数器来维护的,标志当前执行到哪一条字节码。在实现上对应为一个 int 类型的变量。
本地变量表:本地变量表可以理解是个 Map, key 为索引位置,value 为需要存取的值。在实现上可简化为数组,因为字节码里是以类似数组下标的方式进行存取的。
操作数栈:如名,存放数据的栈,只有
push
,pop
两种操作。
实现解释器原型
这一节会实现一个简单的解释器,用来解释之前生成的 test.bc
文件内部的 4 个指令。
实现数据结构,栈帧。
// 栈帧
class Frame {
// 程序计数器,默认值为 0
public int pc;
// 本地变量表
public final Map<Integer, Integer> localVars = new HashMap<>();
// 操作数栈
public final Stack<Integer> operandStack = new Stack<>();
}
如上面的代码,栈帧类为 Frame
, 内部包含三个实例变量,pc
,localVars
,operandStack
。对应为 程序计数器,本地变量表,操作数栈。
实现数据结构,指令。
// 指令接口
interface Instruction {
// offset, 字长, 因为字节码的长度不一致,一般情况下是 1,此处提供默认方法用来获取指定的字长。用来在指令结束时改变栈帧的程序计数器,使之指向下一条指令。
default int offset() {
return 1;
}
// 具体指令需要实现的方法,是指定自身的实现逻辑。
void eval(Frame frame);
}
如上,定义指令接口,以及指令的默认行为。
实现 test.bc
涉及到的 4 个指令。
// iconst_1
class IConst1Inst implements Instruction {
@Override
public void eval(Frame frame) {
frame.operandStack.push(1); //
frame.pc += offset();
}
}
// istore_0
class IStore0Inst implements Instruction {
@Override
public void eval(Frame frame) {
frame.localVars.put(0, frame.operandStack.pop());
frame.pc += offset();
}
}
// iload_0
class ILoad0Inst implements Instruction {
@Override
public void eval(Frame frame) {
frame.operandStack.push(frame.localVars.get(0));
frame.pc += offset();
}
}
// ireturn
// 返回指令涉及到栈帧的一些特殊操作,暂时简单实现,输出操作数栈顶的数值
class IReturnInst implements Instruction {
@Override
public void eval(Frame frame) {
int tmp = frame.operandStack.pop();
System.out.println(tmp);
frame.pc += offset();
}
}
如上,4 个指令的作用之前已经介绍过,此处结合代码和注释,加深理解。
实现解释器。
// 解释器
class Interpreter {
/**
* 解释器运行
*
* @param frame 栈帧
* @param instructions 指令集合
*/
public static void run(Frame frame, Map<Integer, Instruction> instructions) {
// 核心循环
do {
// 获取指令
Instruction instruction = instructions.get(frame.pc);
// 执行指令
instruction.eval(frame);
} while (instructions.containsKey(frame.pc));
}
}