您的当前位置:首页正文

一文搞懂 java -jar 发生了什么

2024-11-24 来源:个人技术集锦

之前一直就很好奇 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 文件

下面开始执行

关于 shell

关于进程

执行 java -jar

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 文件不危害虚拟机自身的安全。

准备

加载验证结束后,开始进入准备阶段,主要做两件事情

解析

准备阶段完成之后,开始解析,主要做一件事

  1. 将常量池中的符号引用转化为直接引用
    主要针对类或接口、字段、类方法、接口方法等

凡是在此阶段可以解析的方法引用都成为静态解析,调用的时候就叫静态调用
静态解析一般都是静态方法和私有方法,并且在运行期间是不变的

初始化

我更喜欢类的初始化,因为我们调用了 main() 方法,实际上是 静态调用 invokestatic 。
类初始化的几种情况:

  1. 遇到 new、getstatic、putstatic或 invokestatic 时,如果未初始化则先初始化( 1. new 2.读取或设置一个类的静态字段 (被 final 修饰、已经在编译期把结果放入常量池的静态字段除外) 3. 调用一个类的静态方法 )
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时,如果未初始化则先初始化
  3. 当初始化类时,如果其父类未初始化则先触发其父类初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类
  5. 当使用 动态语言支持时,如果 java.lang.invoke.MethodHandle 的解析结构为 REF_static、REF_new句柄,并且这个句柄对应类没有进行初始化,需要先初始化
  6. 当有 默认方法 接口的实现类发生了初始化,则该接口要在其初始化之前初始化
    接口并不要求父接口全都完成初始化,只有在真正使用到 父接口 的时候才会初始化
    类初始化其实就是调用类构造器() 方法的过程,而() 是由编译器 Javac 自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成的( 顺序不变 ),并且 JVM 会保证在子类 () 执行前,父类的()已经被执行完毕 ( 第一个被执行的 一定是 java.lang.Object ),并且 JVM 会保证一个类的 () 线程安全,被正确的加锁同步,并且有且仅会有一个线程去执行 () ( 同一个类加载器下,一个类型只会被加载一次 ),其他线程会阻塞直到 () 执行完毕

当然了类初始化完了之后如果需要会进行对象的初始化,调用对象的构造器 () ,调用之前会先调用父类的。

main 方法调用

执行 main 方法也就需要方法调用,对于方法调用 JVM 是通过几条指令来实现的

  1. 一部分在 类加载解析阶段或者第一次使用转为直接引用 ( 静态解析 方法在真正运行前就有一个可确定的调用版本,并且在运行期是不变的 。主要有静态方法和私有方法 ( 不可能通过继承或别的方式重写 ))
  2. 一部分在 每一次运行期间都转化为直接引用 ( 动态链接 invokevirtual )

只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在类加载的解析阶段转化为直接引用 ( 静态方法、私有方法、实例构造器、父类方法( super. )、被final 修饰的方法),对应的方法称为非虚方法,其他的都是虚方法 ( 在运行期间根据实际类型确定方法执行版本 )。
虚方法主要揭示了 java 多态的一些特征,像多态、方法重写。

main 方法执行

我们都知道方法是在栈中执行的,方法的执行过程其实就是不断的出栈入栈的过程
我们以 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 一起
===================================================
===================================================

打印输出

打印输出会从用户态进入内核态,操作系统会调用 IO 操作输出相应的结果。

退出

发生系统调用,JVM 退出

补充

  1. 在电脑中,系统调用(英语:system call),指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态运行。如设备IO操作或者进程间通信。

2. 操作系统的进程空间可分为用户空间和内核空间,它们需要不同的执行权限。其中系统调用运行在内核空间。

3.常见系统调用

显示全文