JVM 是 Java Virtual Machine 的简称,意为 Java 虚拟机,虚拟机是指通过软件模拟的具有完整硬件功能的、运行在一个完全隔离的环境中的完整计算机系统。 常见的虚拟机有:JVM、VMwave、Virtual Box等。JVM 是一台被定制过的现实当中不存在的计算机;
一、JVM 执行流程
JVM 是 Java 运行的基础,也是实现一次编译到处执行的关键;程序在执行前,首先要将 Java 代码(.java 文件)编译为字节码(.class 文件),JVM 把编译后的字节码通过类加载器(ClassLoader)—— 把文件加载到内存中的 —— 运行时数据区(Runtime Data Area),而字节码文件是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解释器 —— 执行引擎(Execution Engine)将字节码翻译为底层系统指令再交给 cpu 去运行,而这个过程需要调用其他语言的接口 —— 本地库接口(Native Interface)来实现;
即 JVM 主要通过以下四部分来执行 Java 程序;
类加载器,运行时数据区,执行引擎,本地库接口;
二、JVM 内存区域划分
一个运行起来的 Java 进程,需要从操作系统中申请一块内存区域;
JVM 运行时数据区域也叫内存区域,由以下 5 大部分组成:
1. 方法区 / 元数据区(线程共享)
用来存储被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码等数据;
常量池存放字面量和符号引用:
字面量:字符串常量(JDK 8 移动到了堆中),final 常量,基本数据类型的值;
符号引用:类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符;
2. 堆区(线程共享)
程序中创建的所有对象都在保存在堆中,堆里面分为两个区域:新生代和老年代,新生代放新建的对象,当新生代的对象经过一定 GC 次数之后还存活的对象会放入老年代;
3. 虚拟机栈(线程私有)
Java 虚拟机栈的生命周期和线程相同,Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息;
1)局部变量表:存放了编译器可知的各种基本数据类型(8 大基本数据类型)、对象引用,局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间不会改变局部变量表大小;简单来说就是存放方法参数和局部变量;
2)操作栈:每个方法会生成一个先进后出的操作栈;
3)动态链接:指向运行时常量池的方法引用;
4)方法返回地址:PC 寄存器的地址;
4. 本地方法栈(线程私有)
本地方法栈和虚拟机栈类似,本地方法栈是给本地方法(用 native 关键字修饰,在 JVM 内部通过 C++ 实现)使用的,即存放了 JVM 内部 C++ 方法的调用关系;
5. 程序计数器(线程私有)
用来记录当前线程执行的行号的,程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器;
三、类加载过程
一个类的生命周期如下:
其中前五步都属于类加载的过程;
1. 加载
根据全限定类名找到该类的字节码文件,并在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口;
2. 验证
字节码文件是一个二进制格式的文件,需验证当前字节码格式是否符合 Java 虚拟机规范的全部要求,保证字节码中信息被当作代码运行后不会危害虚拟机自身的安全;
3. 准备
为类对象分配内存空间,但并不赋值(真正初始化赋值是在初始化阶段);例如下面的代码:
public static int num = 100;
这个阶段之后,num 的值为 0,而非 100;
但如果是 static final 修饰的基本数据类型的直接赋值方式,或 String 类的直接赋值方式,则会直接在该阶段初始化并赋值;例如
public static final int num = 100;
public static final String str = "abc";
此时 num 的值就是 100,str 的值为 "abc";
4. 解析
针对类对象中包含的字符串常量进行初始化操作;
String s = "abc";
s 的初始化语句,会先被设置为一个 "文件的偏移量",当类真正被加载到内存时,再把偏移量替换为真正的内存地址,这个过程也称为将符号引用替换为直接引用;
5. 初始化
Java 虚拟机真正开始执行类中编写的 Java 程序代码,真的类对象进行初始化,加载父类包括类对象的各个属性,static 成员,静态代码块;
会导致类的初始化的情况:
- 首次访问这个类的静态变量或静态方法时(由于 main 方法是程序的入口方法,并且main 方法是 static 的,所以 main 方法所在的类,会被首先初始化,但也遵循第 2 条);
- 子类初始化,如果父类还未初始化,会先初始化父类;
- 子类访问父类的静态变量,只会触发父类的初始化;
- new 会导致初始化;
不会导致类的初始化的情况:
- 访问类的 static final 静态常量(基本类型和字符型)不会触发初始化;
- 类对象.class 不会触发初始化;
- 创建该类的数组时不会触发初始化;
四、双亲委派模型
双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载;(查找类的优先级问题)
类加载器要做的就是根据给定类的全限定类名(包名+类名,例如 java.lang.String)找到其对应的字节码文件,对应类加载过程中的第一步;
JVM 内置了三个类加载器,分别是
启动类加载器:BootStrap ClassLoader;
扩展类加载器:Extension ClassLoader;
应用程序类加载器:Application ClassLoader;
这三个类由上到下是 "爷,父,子" 的关系,但并不是继承的关系,而是 ClassLoader 中,有一个 parent 属性,指向了它的 "父加载器";
若开发人员想要自己实现一个类加载器,需要继承 java.lang.ClassLoader 抽象类;
由一个类的全限定类名,找该类的字节码文件的过程大致如下:
1)从 Application ClassLoader 作为入口,开始查找,Application ClassLoader 负责搜索项目目录和第三方库目录,但是它不会立即寻找,而是交给它的父亲 Extension ClassLoader;
2)Extension ClassLoader 负责 JDK 中扩展的库的目录,但是它也不会立即寻找,而是交给它的父亲 BootStrap ClassLoader;
3)BootStrap ClassLoader 负责标准库的目录,此时,如果在标准库中找打了,则开始读取该字节码文件,若没有找到,则返回给它的孩子寻找;
若最终 Application ClassLoader 也没有找到该类,则会抛出 ClassNotFoundException;