文章首发于微信公众号「Tomcat那些事儿」,欢迎关注一起交流。
在面试的时候,在问到关于JVM相关的问题,会发现不少的面试者都是机械的在记忆,稍一细问就戛然而止。属于死记硬背型的,估计是看书里记个大概的概念或者图,并没有理解含义。
实际上这块内容,看概念的时候能对照一个简单的程序分析,可以更好的理解。下面咱们开始。
市面上常见的JVM书籍里,关于JVM的体系结构,一般划分成以下几个部分:
- 类加载器
- 程序计数器(Program Counter Register,简称PC Register)
- Java 虚拟机栈(Java Virtual Machine Stacks)
- 堆(Heap)
- 方法区(Method Area)
- 运行时常量池(Run-time Constant Pool)
- 本地方法栈(Native Method Stack)
- 栈帧(Stack Frame)
- 执行引擎(Execution Engine)
其中2~7项,又称为运行时数据区
,毕竟这些东西只有JVM 跑起来才会创建。这个分类,基本都是参照 Java 虚拟机规范。
如果干巴巴的记概念没啥意思,吃个饭可能就忘了。接下来用 1+2这个程序来试着理解它。
我们来看个初学Java 编程的时候都基本都写过的,类似 Hello World的程序。
public class HelloWorld {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
}
}
先javac
编译之后,再用javap -verbose HelloWorld
来观察一下, 你会看到类似下面的输出内容:
public class HelloWorld
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #2 // HelloWorld
super_class: #3 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // HelloWorld
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 SourceFile
#11 = Utf8 HelloWorld.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 HelloWorld
#14 = Utf8 java/lang/Object
{
public HelloWorld();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
LineNumberTable:
line 3: 0
line 4: 2
line 5: 4
line 6: 8
}
好嘞。咱们都知道,上面这些就是Java的字节码。有了上面这个输出的内容,你把自己想像成虚拟机,来运行它,就理解了 Java 虚拟机里各个部分了。
首先,这部分内容,要执行,一定得先读到内存里,负载读这些内容的,就是虚拟机的类加载器
。
加载进来的其实是个二进制流,然后呢,需要把它整理成对应格式的内容才方便使用嘛。比如这个类叫啥名字,继承了谁,都有什么方法,方法名字叫啥,内容是什么这些东西要找个地方放着。放哪好呢?方法区
就是干这个的。
所谓的运行时常量池
也是方法区
里的一块区域。往上看Constant Pool
在运行时会被解析成 Run-time Constant Pool
。如果涉及到对其他类的引用等等,会在加载之后再链接的时候,把这里面的符号引用转化成直接引用。
另外一些部分呢?概括来讲就是Java虚拟机栈,就是咱们常说的栈
,是用来执行方法里的具体内容的。这一部分其实可以这样理解。Java 虚拟机,和我们真实的物理机类似,都会把程序提供的指令执行,只不过虚拟机是一个提供了一套有限指令集
的软件。物理机基本都是基于寄存器
执行,而 Java 虚拟机的对于指令的执行实现是基于栈
的。
既然是栈,那栈里要放点什么?没错,是栈帧
,英文是 Frames,就是咱们在使用 IDE debug 的时候看到的那一层一层的内容。
每个方法调用的时候,都会出现一帧,每一帧也是个结构,方法执行用到的东西都在里面。比如在 Debug 的时候,一般都会看到每个变量和值, 这些变量称为本地变量(local variables)
,在上面的输出内容里也有stack=2, locals=4, args_size=1
我们看到locals
就是本地变量,args_size
是方法参数的长度,还有一个就是操作数(stack)
,数值是栈的最大深度。
每个class 的任意一个方法里,都会有 frame ,它们都有自己的 local variables
本地变量表, 自己的operand stack
操作数栈,以及到run-time constant pool
运行时常量池的引用。当然,也可以有一些扩展信息,例如debug info。
那具体上面简单的一个 1+2
这个操作,对应到 jvm 指令有这些:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
具体当前执行到第几条指令,需要有个标识,这个活儿让程序计数器
给干了。这小子一直指向下一条即将执行的指令。基本栈的实现,上面的指令大意是把常量1赋值给第一个变量,常量2赋值给第二个变量
,之后,变量一入栈,变量二入栈,执行iadd操作的时候,这两个数据出栈
,完成求和,再赋值给变量3,入栈,再返回。下次咱们细说JVM指令的时候,再详细说说。这些指令的执行,当然离不开执行引擎
。
因为不需要执行Native
方法,所以我们一般不用本地方法栈
,这是给类似JNI这些本地方法实现准备的。
你看,观察了1+2
的过程,基本Java 虚拟机的结构是不是就理解了?:-) 如果还是记不住的话,你可以这样想啊,Java 的世界里,经常会说到堆
和栈
。那栈用来存啥呢?想想你 debug 时候看到的那一层层的帧
, 然后再想想今天的1+2
的执行,应该就齐了。