虚拟机就是一款用来执行虚拟计算机指令的计算机软件。它相当于一台虚拟计算机。大体上,虚拟机分为系统虚拟机和程序虚拟机。系统虚拟机就相当于一台物理电脑,里面可以安装操作系统;程序虚拟机是为了执行单个计算机程序而设计出来的虚拟机。其中 Java 虚拟机就是执行 Java 字节码指令的虚拟机。
java 虚拟机是运行在各大平台的执行字节码文件的虚拟计算机。如下图所示
这样的设计可以让编译后的代码在各个操作平台上运行。
JVM 在 Java 体系结构中的位置
从用户操作角度看 JVM 所处的位置
JVM 与实际的计算机硬件没有交互,它们中间还有个操作系统,调用硬件需要通过操作系统来实现。
JVM 的整体运行结构
本文主要针对于 Hotspot VM 来进行,其结构如下所示:
JVM 的指令结构
JVM 是基于栈的指令集架构,跟基于寄存器的指令集不同。它是一个步骤一个一条指令。
//基于栈的指令集
iconst_2 //常量2入栈
istore_1
iconst_3 // 常量3入栈
istore_2
iload_1
iload_2
iadd //常量2/3出栈,执行相加
istore_0 // 结果5入栈
//基于寄存器的指令集
mov eax,2 //在eax寄存器中的值设为2
add eax,3 //将eax寄存器中的值加3
举例说明:
/**
* @author wjw
* @date 11/3/2021
*/
public class StackStruTest {
public static void main(String[] args) {
int i = 2 + 3;
/**
*0: iconst_5
*1: istore_1
*2: return
*/
int i = 2;
int j = 3;
int k = i + j;
/**
* 0: iconst_2
* 1: istore_1
* 2: iconst_3
* 3: istore_2
* 4: iload_1
* 5: iload_2
* 6: iadd
* 7: istore_3
* 8: return
*/
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello");
}
}
JVM 的启动
Java 的启动是通过引导类加载器(BootStrap ClassLoader)创建一个初始类(java.lang.class)来实现的,也就是后面要提到的子类加载器过程。
JVM 的执行
执行 Java 的字节码文件中的各个方法和类的过程。
JVM 的退出
让我们先看看一个 java 程序执行的流程是怎样的:
java.lang.class
对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。而类加载子系统涉及到的过程有类加载器、字节码校验器和翻译字节码这几个过程:
加载阶段(Loading)是类加载阶段(Class Loading)的一部分。在加载阶段中,.class 文件将经过三个类加载器的加载后才能进入下一个阶段。该阶段主要的作用(也就是上面 java 程序执行中的类加载器阶段)有:
在类加载器中,主要分为两类:引导类加载器和自定义类加载器
使用的是 C/C++ 来实现的,无法访问到,调用getClassLoader() 函数是为null。
加载的类是一些核心类库,lib/home 下的类库。比如说是 Object 、String 等等 JVM 自身运行需要的类
是其他加载器的父类吗?是的,但是无父类加载器
派生于抽象类 ClassLoader 的类加载器
(1)拓展类加载器(Extension ClassLoader)
父类加载器为 BootStrap ClassLoader
主要加载的类是ext 文件夹下的类库
(2)应用程序类加载器(AppClassLoader)
父类加载器为 Extension ClassLoader
主要加载的是环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
程序中默认的类加载器
(3) 自定义类加载器
为了防止虚拟机内部的核心类库的修改和非法调用,JVM 的设计者们在类加载过程中设计出了双亲委派机制和沙箱安全机制。
举例
双亲委派机制的优点:
引导类加载器在加载时会首先加载 JDK 中自带的文件。报错信息中没有相应方法时的机制。保证对 java 核心源代码的保护。
验证字节流文件中的相关信息,确认当前文件符合虚拟机的相关格式(文件、元数据、符号等等)。
在方法区中为类变量分配内存并设置类变量初始值
虚拟机将常量池中的符号引用转换成直接引用(class 文件很小,先存放指向所需要类的符号,解析时再引用)
针对类、接口、字段方法等7类符号引用进行转换
没有规定解析阶段发生的具体时间,也可能在初始化阶段的后面
初始化过程实际上就是执行类构造器方法 ( ) 的过程
public class ClassInitTest {
private static int num = 1;
static {
num = 2;
number = 20;
System.out.println(num);//因为在linking 阶段中的 Prepare 阶段已经给静态变量赋过初始值了。
//非法的前向引用;在声明变量之前,可以去给这些变量进行赋值,但是不能够调用它。
//System.out.println(number);//会报错误
}
/**也就是在 linking 中的 prepare 中对变量进行默认赋值为0
* 此时initial 给予其一个覆盖从 20 到 10
*
*/
private static int number = 10;
public static void main(StringDemo[] args)
{
System.out.println(ClassInitTest.num);
System.out.println(ClassInitTest.number);
}
}
构造器方法执行是按语句顺序来执行的。
类构造器方法 ( ) 和类构造器( ) 并不是一回事