2021年07月07日 第一版 本版本处于不可信阶段
突然想用这样发布一个现阶段对java运行流程的理解,持续学习以及更新文章,让之后的我不要忘了曾经的我有多傻多憨憨。
2022年03月01日 第二版 本版本仍然是处于不可信阶段
最近再次研究JVM,又有所获。主要完善了执行之前的一些细节。让整个流程更加通俗易懂一点(虽然现在也不通俗易懂吧)。
本次更新的地方都标注有 “完善: ”。
好的回归文章:
作为萌新程序员,有没有疑惑当我们使用idea或者eclipse运行一个java程序到底怎么运行呢?
总结其实就两步:
1.javac 命令 编译java文件 生成 .class文件
2.java 命令 解析字节码(.class)文件执行
这就是java基础运行流程,好了文章到此结束了~~!
开个玩笑,主要想稍微详细的介绍一下关于第二个步骤
编译java文件
其实这一步就是生成 target\classes中的class文件。这里主要编译器进行编译,具体就不细讲了。
完善:这一步就是我们点击idea或者eclipe的build。完成这一步我们项目中会出现一个target文件,里面有我们写的java代码编译后的所有的.class文件
解析字节码文件
解析字节码文件其实核心就是 JVM。
完善: .class文件其实就是JVM能识别的字节码文件,会按照 字节码指令手册的规则 执行字节码文件。
类加载
负责加载 字节码文件,如果调用系统类(String),或者其他的自己写的类 分别会调用不同的类加载器去加载。其实就将字节码文件从硬盘上利用Stream读取到内存中,放在元空间(1.8以前放在方法区永久代)中。
ps:这里牵扯到 根加载器,拓展类加载器,应用程序加载器以及双亲委派的知识。加载到内存中InstanceKlass 类的元信息,也就class在jvm的存储形式。
完善:如果指令用到了未曾加载的类,或者说 第一次实例化某一个类的时候(new ClassA()),我们会进行类加载。会将类中对应的 常量+静态变量+类信息 放在方法区(1.8后称之为 元空间)内。
因为JVM其实是懒加载 所以说只有当类被首次运行到才会加载.如果一个方法内有一个类C,但是这个方法从启动到现在未被调用,那么类C信息是没有被加载到方法区中。
执行
public class test{
int a = 1;
public int add(int a){
return ++a;
}
public static void main(String[] args){
String s = "你好/hello";
s.split("/");
int a = 2;
a= add(a);
}
}
加载所需要的类之后,就开始执行了。
关于虚拟机栈,每调用一个方法会产生一个栈帧。
栈帧结构
- 局部变量表
存储方法内的 变量
例如:int a,String s中的 a,s保存在局部变量表中
- 操作数栈
存储操作数的栈 字节码指令 iconst istore ldc astore等操作
方法内 int a = 2 = 这个操作iconst_2,istore_1的操作记录a的值复制为2并保存到局部变量表中
- 动态链接
Klass类元信息在方法区 一个Klass是一个InstanceKlass实例 类所有的方法都在vtable ,动态链接就是拿到table中具体哪个方法的地址。
调用的别的类的方法,通过动态链接拿到方法的地址放在常量池中。
一个线程会创建一个虚拟机栈结构,调用一个方法产生一个栈帧。故执行main方法会 创建一个栈并且栈中创建一个栈帧。
很显然该类应该是创建一个虚拟机栈结构,那么猜猜执行过程中会生成多少个栈帧呢?
公布答案:
首先会执行init 结束,然后执行main,之后调用add,结束add继续执行main,结束main结束程序。
下面具体讲讲main方法执行的过程。
- main 方法的字节码
0 iconst_2
1 istore_1
2 ldc #2 <你好/hello>
4 astore_2
5 aload_2
6 ldc #3 </>
8 invokevirtual #4 <java/lang/String.split>
11 pop
12 iload_1
13 invokestatic #5 <com/demo/fileupload/test.add>
16 istore_1
17 return
- add方法的字节码
0 iinc 0 by 1
3 iload_0
4 ireturn
JVM运行main方法
- 创建运行main方法需要的栈帧
- 将main方法的操作数栈 赋值给 线程的属性 :操作数栈
- 将main方法的局部变量表 赋值给 线程的属性 :局部变量表
- 创建add方法的栈帧
- 保存main方法的下一个字节码指令到程序计数器
- 线程的局部表的开始指针(main的)保存到add方法栈帧中
- 线程的操作数栈的开始指针(main的)保存到add方法栈帧中
- 将add方法的局部表指针以及操作数栈的指针 分别赋值给 线程的局部表的 开始指针以及线程的操作数栈的开始指针
- 执行add字节码指令并返回,销毁栈帧
- 从程序计数器中读取main方法该执行到哪一条指令,继续执行到结束
以上的过程中若调用到本地方法,会去本地方法栈调用native方法。调用其它类也会去类的元信息去找所需类。例如上述代码用到 String,那么类加载过程中就会将String.class加载到元空间中,我们在虚拟机栈操作指令时候需要用到String类的方法,就会去元空间找到类的元信息。