之前一直就很好奇 java -jar 到底发生了什么,为什么执行 java -jar 代码就自动运行了。今天我们来说明一下,尽量覆盖操作系统、编译原理、JVM 的一些东西。( 本文将处于一个不断更新的状态,知道上面这些东西覆盖的差不多了为止,如果可以的话,也会加上硬件方面的东西 ),主要的目的就是为了能以最简单的 java 代码来串一些相对来说比较底层的东西,让自己以及让每个读者对计算机能有一个相对全局的了解。
我们先约定如下:
1.操作系统仅仅指的是unix 或类unix
2. 64 位机器
3. 64位 jDK
我们把下面这个类,打成一个 jar 包然后执行。
/**
* Created by shengjk1 on 2020/8/30.
*/
public class Test {
public static int b;
private int a;
public static void main(String[] args) {
Test test = new Test();
test.test();
System.out.println("b = " + b);
System.out.println("执行完毕");
}
public void test() {
byte i = 15;
int j = 8;
int k = i + j;
}
}
学过 java 的同学应该都知道这个 Test 类的每行代码都是干嘛的,就不一一解释了。
首先会编译成 class 文件
关于 java 的编译器
编译的 class 内容
有 cafe babe 魔数,还有什么常量池呀之类的,稍后补充
下面开始执行
执行的时候我们启动了一个 命令行客户端 进程,可以理解为 shell 的一种。所谓的 shell 在操作系统中的位置
当然此 shell 非彼 shell,操作系统中的 shell 更加宽泛一下,像图形界面也是 shell 的一种。
我们刚才仅仅用鼠标那么轻轻的一点就创建了一个 命令行客户端 进程,而对于操作系统而言进程是如何创建的呢?
会由用户态进入到内核态,然后由操作系统执行 fork 命令,此时进程开始创建,
会包括 虚拟地址空间、修改进程表、会占用寄存器、会有打开文件的清单等等信息,创建完成之后就可以执行了。我们的 命令行客户端 也就起来了
等待用户输入,用户的每次输入,然后回车,其实对于操作系统而言都是创建一个新的进程。
同理会 fork 一个 JVM 进程出来,JVM 创建的过程中会启动 Bootstrap ClassLoader 加载 Java 的核心类库 ( JAVA_HOME/jre/lib/rt.jar、resource.jar 或者是 sun.boot.class.path 路径下的内容 ),供 JVM 自身需要。( 关于 JDK、JRE、JVM 可以参考 读 Differences between JDK, JRE and JVM)
JVM 的准备工作完成之后,JVM会调用我们的 main() 方法,可是内存里面并没有 main 方法,这就是所说的 页面故障,操作系统会从磁盘上读取相应的指令。也就进入了 JVM 的类加载。
要加载 main() 方法所在的 Test 类,会首先判断有没有没有加载的父类,若有未加载的父类则会先加载其父类。在这里我们的 Test 类并没有明确的父类 ,JVM就把 Test 类加载到 JVM 的内存中形成一个 java.lang.Class 对象
而对象在JVM 中的内存布局如下:
所以说未压缩的情况下 class 对象至少占用 8 byte( 32 位 JVM ) 16byte ( 64 位 JVM )
这个过程中,会把类的版本、字段、方法、等描述信息以及代码缓存放入 Metaspace,把常量池表中的各种字面常量符号引用等放入方法区的运行时常量池。
同时会对 class 文件进行验证,包括文件格式、元数据等,以保证 class 文件不危害虚拟机自身的安全。
加载验证结束后,开始进入准备阶段,主要做两件事情
准备阶段完成之后,开始解析,主要做一件事
凡是在此阶段可以解析的方法引用都成为静态解析,调用的时候就叫静态调用
静态解析一般都是静态方法和私有方法,并且在运行期间是不变的
我更喜欢类的初始化,因为我们调用了 main() 方法,实际上是 静态调用 invokestatic 。
类初始化的几种情况:
当然了类初始化完了之后如果需要会进行对象的初始化,调用对象的构造器 () ,调用之前会先调用父类的。
执行 main 方法也就需要方法调用,对于方法调用 JVM 是通过几条指令来实现的
方法对应的符号引用主要有两种
只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在类加载的解析阶段转化为直接引用 ( 静态方法、私有方法、实例构造器、父类方法( super. )、被final 修饰的方法),对应的方法称为非虚方法,其他的都是虚方法 ( 在运行期间根据实际类型确定方法执行版本 )。
虚方法主要揭示了 java 多态的一些特征,像多态、方法重写。
我们都知道方法是在栈中执行的,方法的执行过程其实就是不断的出栈入栈的过程
我们以 test() 方法为例来具体分析一下
0: bipush 将 15 放入栈中
2: istore_1 将栈顶元素方入局部变量表第 1 个位置
3: bipush 将 8 放入栈中
5: istore_2 将栈顶元素方入局部变量表第 2 个位置
6: iload_1 将局部变量表的第 1 个位置元素放入栈
7: iload_2 将局部变量表的第 2 个位置元素放入栈
8: iadd 相加
9: istore_3 将栈顶元素(也就是相加的结果)方入局部变量表第 3 个位置
===================================================
===================================================
===================================================
===================================================
6,7 一起
===================================================
===================================================
然后 return ,主方法( 调用该方法的方法 )的 PC寄存器的值可以作为返回地址,然后继续执行。
打印输出会从用户态进入内核态,操作系统会调用 IO 操作输出相应的结果。
发生系统调用,JVM 退出