从一个简单的main方法执行谈谈JVM工作机制
本来JVM的工作原理浅到可以泛泛而谈,但如果真的想把JVM工作机制弄清楚,实在是很难,涉及到的知识领域太多。所以,本文通过简单的mian方法执行,浅谈JVM工作原理,看看JVM里面都发生了什么。
先上代码:
public class Test {
private int invar = 1;
public static String concat(String str1, String str2) {
return str1+str2;
}
public int f() {
return invar;
}
public static void main(String[] args) {
Test test = new Test();
test.f();
int localInt1 = 1;
int localInt2 = 2;
int sum = localInt1 + localInt2;
String str = concat("string ", "concat");
System.out.println(str);
}
}
再看看JVM内部结构:

上图是对《The Java® Virtual Machine Specification Java SE 7 Edition》中JVM内部结构的一个描述简图,“The Java® Virtual Machine Specification”是JVM的一个抽象规范,主流JVM实现的主要有Oracle HotSpot、IBM J9等。
现在来看看启动org.test.Test的main方法,JVM(本文涉及到的JVM implementation是针对JDK 7的HotSpot,下同)会做些什么:
1. JVM启动:
根据JVM的启动参数分配JVM runtime data area内存空间,如根据-Xms、-Xmx分配Heap大小;根据-XX:PermSize、-XX:MaxPermSize分配Method area大小;根据-Xss分配JVM Stack大小。注意,Method area、Heap是所有JVM线程都共享的,在JVM启动时就会创建且分配内存空间;JVM Stack、PC Register、Native Method Stack是每个线程私有的,都是在线程创建时才分配。在HotSpot中,没有JVM Stacks和Native Method Stacks之分,功能上已经合并。官方说明:Both Java programming language methods and native methods share the same stack.
2. Loading, Linking, and Initializing
所有这些工作都是Class Loader Subsystem来完成,对org.test.Test.class进行加载、链接、初始化。
2.1 Loading
将Test.class加载为Method area的Test Class data,Test.class是一个二进制文件,里面是JVM编译器对org.test.Test.java编译成的字节码,关于.class字节码的解读,请看《实例分析Java Class的文件结构》,讲得非常透彻。Test.class在Method area是如何存储的?这个问题的解答,首先还是要对Method area有一个认识。先看看《The Java® Virtual Machine Specification Java SE 7 Edition》中对ClassFile的定义:

也就是说Test.class里面的二进制码是按照ClassFile的结构一个一个字节来存储相应的Test.java编译后的信息。所有这些信息被类加载器加载会对应地存储到Method area中,尽管体现的方式不一样,例如:Test.class中的Constant pool对应Method area中的Runtime constant pool,Runtime constant pool中的Constant中的信息都是从Constant pool中获取到的,Runtime constant pool里面都是些符号引用、字符串字面值以及整形、浮点型常量。下面是“JVM Method Area Class Data结构示意图”:

实际上,在JVM的method area中,一个class或interface是由其全限定名称和真正加载该类或接口的类加载器(即the defining classloader)来唯一确定的,因为一个类(如果无特殊说明,“类”表示class或interface,下同)可以由不同的类加载器加载。
这里要弄清楚《The Java® Virtual Machine Specification Java SE 7 Edition》中关于类加载器的两个概念:the defining loader和the initiating loader。假设有两个类加载器classloader1和classloader2,classloader1是classloader2的parent classloader(委派的),一个待加载的类A,现在classloader2加载类A,首先委派classloader1去加载类A,结果classloader1加载类A成功了,那这里classloader1就是类A的“the defining loader”即,classloader2就是类A的the initiating loader即
,因为类加载动作由classloader2发起的。关于类的可见性:对于类A、类B,类A是否对类B可见(即类B能找到类A)取决于类B的“the defining loader”能否自己或通过委派加载类A成功。
所以在method area中,首先根据“the defining loader”来划分所加载的类的范围,这是逻辑意义上的概念,而对于每一个Class Data里面存储什么内容,上图已经比较清晰给出答案。这里需要注意的是Class的static变量是存储在Field data中,而final static则存储在Run-time constant pool中,Code的JVM指令存储在Method data中,另外,Class Data中还有两个引用:一个是指向“the defining loader”的引用,该引用在Class的动态链接解析时需要用到,如果该Class中引用了其他类,那么在动态链接解析时,就用该类的“the defining loader”去找其他类,完成其加载、链接、初始化动作;另一个是指向Class实例(即java.lang.Class对象)的引用,而JDK提供java.lang.Class出来,这样java开发者就可以通过该引用获取到存储在method area中的Class Data,即有哪些Field、哪些Method,这就是大家熟悉的反射。
JVM在加载Test.class到Method area后,在使用其之前,还需要做一些工作,就是接下来的Linking和Initializing。
2.2 Linking
链接一个类包括对该类、其直接超类、其直接superinterfaces进行验证(verifying)、准备(preparing),如果该类为数组类型,还包括对数组元素类型的链接。Loading, Linking, and Initializing的时间顺序遵循以下两个原则:
(1)一个类一定要在链接之前加载完成;
(2)一个类的验证和准备一定要在初始化之前完成。
关于类中的符号引用什么时候进行解析,这个要看JVM的实现。例如有的JVM实现采用懒解析("lazy"or "late" resolution),即在需要进行该符号引用的解析时才去解析它,这样的话,可能该类都已经初始化完成了,如果其他的类链接到该类中的符号引用,需要进行解析,这个时候才会去解析;还有的JVM实现采用静态解析("eager" or "static" resolution),即在该类在进行验证时一次性将其中的符号引用全部解析完成。在JVM的实现中,一般采用懒解析。

2.2.1 verifying
验证.class文件在结构上满足JVM规范的要求,验证工作可能会引起其他类的加载但不进行验证和准备。
2.2.2 preparing
给Class static变量分配内存空间,初始化为默认值即将内存空间清零。
关于对类的方法声明,需要满足以下加载约束条件:
假设为类C的“the defining loader”即
,
为类D的“the defining loader”即
,D为C的超类或superinterface,对于C中所Override D的方法m,m的返回类型为
,参数类型为
,如果
不是数组类型,假设
为
,否则
为
的元素类型,对于i=1,...,n,如果
不是数组类型,假设
为
,否则
为
的元素类型,则对于i=0,...,n满足:
,即
、
都能自己或通过委派成功加载
。
更进一步,假设为C的superinterface,
为C的superclass,
中声明了方法m,
中声明且实现了方法m,则对于i=0,...,n满足:
。
2.2.3 resolution
详细见:Resolution in《The Java® Virtual Machine Specification Java SE 7 Edition》
2.3 Initializing
详细见:Initialization in《The Java® Virtual Machine Specification Java SE 7 Edition》
3. 启动main线程
在JVM实现中,线程为Execution Engine的一个实例,main函数是JVM指令执行的起点,JVM会创建main线程来执行main函数,以触发JVM一系列指令的执行,真正地把JVM run起来。在创建main线程时,会为其分配私有的PC Register、JVM Stack、Native Method Stack,当然在HotSpot的实现中,JVM Stack、Native Method Stack功能上已经合并,下面以HotSpot为例来说说main函数的执行。
4. 执行main函数
先用javap -c Test.class,通过反汇编把Test.class对应的JVM机器码弄出来:
Compiled from "Test.java"
public class org.test.Test {
public org.test.Test();
Code:
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #12 // Field invar:I
9: return public static java.lang.String concat(java.lang.String, java.lang.String);
Code:
0: new #20 // class java/lang/StringBuilder
3: dup
4: aload_0
5: invokestatic #22 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
8: invokespecial #28 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
11: aload_1
12: invokevirtual #31 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #35 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn public int f();
Code:
0: aload_0
1: getfield #12 // Field invar:I
4: ireturn public static void main(java.lang.String[]);
Code:
0: new #1 // class org/test/Test
3: dup
4: invokespecial #46 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #47 // Method f:()I
12: pop
13: iconst_1
14: istore_2
15: iconst_2
16: istore_3
17: iload_2
18: iload_3
19: iadd
20: istore 4
22: ldc #49 // String string
24: ldc #51 // String concat
26: invokestatic #52 // Method concat:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
29: astore 5
31: getstatic #54 // Field java/lang/System.out:Ljava/io/PrintStream;
34: aload 5
36: invokevirtual #60 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: return
}
JVM指令后面的#index表示ClassFile中常量池“数组”的索引,实际上线程中每一个函数的执行都对应一个帧在JVM Stack中的压入弹出,帧包含局部变量数组(用来存储函数参数、局部变量等)、操作数栈(用于配合JVM指令的执行,存储来自局部变量数组和类属性的值及中间结果,其中操作数栈中的值可以为直接引用)、一个指向当前方法所在类的runtime constant pool(用于符号引用解析):

main线程调用main函数时,先创建一个main帧,根据编译时期就已经确定的局部变量数组和操作数栈的大小分配内存空间,将内存空间清零,将main帧压入main线程 Stack中:

看看main中的指令:
0: new #1 // class org/test/Test
3: dup
0: new #1:#1表示Test.class中常量池“数组”的索引,该索引位置为CONSTANT_Class_info常量,表示Test class,这里的new指令表示new一个org/test/Test对象,且将其引用压入操作数栈中;3: dup:在操作数栈中,复制栈顶的操作数,同时将其压入栈顶。
在执行JVM指令的过程中,main线程的PC register会记录当前所执行的JVM指令的地址。执行完这两条指令后,只有操作数栈有变化:
4:invokespecial #46 // 调用Method "<init>":()V
这条指令操作是:弹出操作数栈顶的元素,在该元素所指向的Test对象上,调用<init>方法,其实就是对该对象进行初始化,<init>的参数为空,返回为V的类型即为空,执行完这条指令后,main帧如下:

7: astore_1 // 弹出操作数栈顶元素,将该引用存放到局部变量“数组”索引1的位置 8: aload_1 // 将局部变量“数组”索引1的位置的引用压入操作数栈
执行完后,main帧如下:
9: invokevirtual #47 // 在Test对象上,调用Method f:()I
在执行invokevirtual指令时,在main函数中又调用了函数f(),所以main线程会创建一个帧即f帧,为f帧分配相应的内存空间,保存main帧的状态,即保存局部变量数组、操作数栈中的值,以便f()调用 完成后再恢复,,将f帧压入main线程的Stack,将f帧切换成当前帧,执行f()函数的字节码,main线程PC register记录当前执行的指令地址:
f帧中局部变量“数组”索引0的位置为什么是reference 1?这是因为在invokevirtual指令执行过程中,首先会将main帧中操作数栈中的栈顶元素弹出,将其传给f帧,f帧将其存放到局部变量”数组“中,下面是执行f()函数中的指令:
0: aload_0 // 将局部变量“数组”索引0位置的引用压入操作数栈中

1: getfield #12 // 弹出操作数栈顶元素即Test对象引用,在该引用指向的对象上获取属性Field invar:I的值,再将该值压入操作数栈中
4: ireturn // 弹出f帧中操作数栈顶元素即1,将其压入main帧中的操作数栈
执行ireturn指令,实际上表明f()函数调用完成返回,main线程会释放f帧及其内存空间,将main帧切换成当前帧,恢复main帧的状态,main线程的PC register记录main帧中当前执行指令的地址,继续执行完main帧后面的指令。
5. JVM退出
释放main线程所占用资源及内存空间,如PC register、JVM Stack等,释放JVM所占用的内存空间,如Heap、Method area,JVM退出。
虽然只是一个简单的main方式执行,但通过这个简单的示例可以看到JVM完整的工作流程。
从一个简单的main方法执行谈谈JVM工作机制的更多相关文章
- main方法执行之前,做什么事
1.我们知道程序的入口是main方法,那么在执行main方法之前,需要做些什么准备工作呢? 2.main方法执行之前,必须要把non-local static对象构造完成.static对象有:全局对象 ...
- js new一个对象的过程,实现一个简单的new方法
对于大部分前端开发者而言,new一个构造函数或类得到对应实例,是非常普遍的操作了.下面的例子中分别通过构造函数与class类实现了一个简单的创建实例的过程. // ES5构造函数 let Parent ...
- 只是一个用EF写的一个简单的分页方法而已
只是一个用EF写的一个简单的分页方法而已 慢慢的写吧.比如,第一步,先把所有数据查询出来吧. //第一步. public IQueryable<UserInfo> LoadPagesFor ...
- 【JUnit】Junit命令行执行、参数化执行、Main方法执行
参考资料: main方法执行:http://stackoverflow.com/questions/2543912/how-do-i-run-junit-tests-from-inside-my-ja ...
- 重读《深入理解Java虚拟机》五、虚拟机如何执行字节码?程序方法如何被执行?虚拟机执行引擎的工作机制
Class文件二进制字符流通过类加载器和虚拟机加载到内存(方法区)完成在内存上的布局和初始化后,虚拟机字节码执行引擎就可以执行相关代码实现程序所定义的功能.虚拟机执行引擎执行的对象是方法(均特指非本地 ...
- Java实现一个简单的缓存方法
缓存是在web开发中经常用到的,将程序经常使用到或调用到的对象存在内存中,或者是耗时较长但又不具有实时性的查询数据放入内存中,在一定程度上可以提高性能和效率.下面我实现了一个简单的缓存,步骤如下. 创 ...
- 简单的main方法调用一个加减法函数背后的细节
测试程序 /* * AddTest.c * * Created on: 2019年10月13日 * Author: appweb */ #include <stdio.h> int add ...
- 一个简单的解决方法:word文档打不开,错误提示mso.dll模块错误。
最近电脑Word无故出现故障,无法打开,提示错误信息如下: 问题事件名称: APPCRASH应用程序名: WINWORD.EXE应用程序版本: 11.0.8328.0应用程序时间戳: 4c717ed1 ...
- java main方法执行sql语句
public static void main(String[] args) throws Exception{ String driver = "oracle.jdbc.driver.Or ...
随机推荐
- vijos p1027休息中的小呆
休息中的小呆 描述 当大家在考场中接受考验(折磨?)的时候,小呆正在悠闲(欠扁)地玩一个叫“最初梦想”的游戏.游戏描述的是一个叫pass的有志少年在不同的时空穿越对抗传说中的大魔王chineseson ...
- docker社区的geodata/gdal镜像dockerfile分析
对应从事遥感与地理信息的同仁来说,gdal应该是所有工具中使用频度最高的库了,那么在docker中使用gdal时,面临的第一步就是构建gdal基础镜像,社区中引用最多的就是geodata提供的gdal ...
- 「日常训练」 Yukari's Birthday(ZOJ-3665)
题意与分析 二分题.考虑到n的范围是\(10^{12}\),注意到等比公式\(S=a_1\frac{1-q^n}{1-q} (q\ne 1)\),可以看出,不论q有多大(1除外,这个时候\(r=1,k ...
- 怎样通过Qt编写C/C++代码查询当前Linux的版本号?
遇到一个问题:如题. 我的开发环境是:嵌入式ARM + Linux系统 + Qt 4.5 + C/C++ 现在需要查询 当前Linux系统的版本号. 问题: 1)Qt 4.5 提供怎样的API来获取? ...
- Siki_Unity_2-1_API常用方法和类详细讲解(上)
Unity 2-1 API常用方法和类详细讲解(上) 任务1&2:课程前言.学习方法 && 开发环境.查API文档 API: Application Programming I ...
- 简单的switch嵌套
//添加list数据 1 public static void main(String[] args) { List<String> al = new ArrayList<Strin ...
- SqlServer的两种插入方式效率对比
protected void button1_Click(object sender, EventArgs e) { DataTable dtSource = new DataTable(); dtS ...
- Codeforces-A. Shortest path of the king(简单bfs记录路径)
A. Shortest path of the king time limit per test 1 second memory limit per test 64 megabytes input s ...
- NO.01---今天聊聊Vuex的简单入门
作为一款个人认为非常牛x的框架,个人使用起来得心应手,所以近期就记录一下这款框架吧. 首先说一说 Vuex 是什么? 官方给出的解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它 ...
- 82. Single Number [easy]
Description Given 2*n + 1 numbers, every numbers occurs twice except one, find it. Example Given [1, ...