一、JVM 简介
1.1、什么是JVM
JVM(Java Virtual Machine)即Java虚拟机,是一种通过软件模拟完整硬件功能的计算机系统,运行在完全隔离的环境中。常见的虚拟机包括JVM、VMware和VirtualBox
1.2、JVM 的运行流程
JVM 是 Java 运行的基础(是Write once,run anywhere的关键),其执行流程主要包括以下几个关键步骤:
Java程序执行前需要先将源代码编译为字节码(class文件)。JVM 通过类加载器(ClassLoader)将这些字节码加载到运行时数据区(RuntimeDataArea)。由于字节码是JVM的指令规范,无法直接由操作系统执行,因此需要执行引擎(ExecutionEngine)将其转换为底层系统指令,最终交由CPU处理。在此过程中,JVM还会通过本地库接口(NativeInterface)调用其他语言的功能。这四大组件共同协作,确保Java程序能够顺利执行

总的来看,JVM主要通过类加载器、运行时数据区、执行引擎、本地方法库共同协作,确保Java程序能够顺利执行
二、JVM运行时数据区
又名JVM内存区域,主要有下图5大部分:

2.1、堆
作用:保存程序中创建的对象(所有对象实例和数组都在Heap堆区分配内存),也是JVM 中最大的空间区域

扩展:
Old 区:存放经过一定GC次数之后还存活的对象
Young 区:存放新创建的对象
Eden 伊甸区:新创建的对象首先会存放在 Young 区的该位置,此区域占据占 Young 区的大部分空间(约80%),当该区域满时,会触发年轻代的 GC,存活的对象就会存放在 S0 和 S1幸存区
S0 和 S1 幸存区:存放年轻代 GC 后幸存的对象,二者总有一方为空,GC时连同 Eden区幸存的对象一起存放在另一个幸存区(例:[ S0 + Eden ] --> S1)
2.2、Java虚拟机栈
作用:通俗来讲" 保存方法的调用关系 ",Java虚拟机栈的生命周期与线程相同,描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存放局部变量表、操作数栈、动态链接、方法出口等信息

扩展:
局部变量表:存储方法参数和方法内定义的局部变量,局部变量表所需的内存空间在编译期 就确定了,运行时不会改变
操作栈:给每个方法生成一个先进后出的操作栈(所有计算、逻辑判断、方法参数传递都通 过它来完成)
动态链接:运行时确定"要调用哪个具体方法"的过程(指向运行时常量池的方法引用)
方法返回地址:保存方法返回后需要继续执行的位置
2.3、本地方法栈
作用:给本地方法使用的(Java虚拟机栈是给JVM使用的)
2.4、程序计数器
作用:是一块比较小的内存空间,用于指示当前线程正在执行的字节码指令位置
类似于:每个线程的" 书签 ",记录着" 我这本书读到哪一页了 ",确保再次读时能够从正确的位置继续读下去
2.5、元数据区(方法区)
作用:" 保存当前类被加载好的数据 ",用于存储虚拟机加载的类信息、常量、静态变量以及编译器编译后生成的代码等数据

三、JVM类加载
3.1、类加载过程
整个JVM执行的流程中,与我们开发者最密切的就是类加载的执行流程,对于一个类来说,它的生命周期如下图:

其中前 5 步是固定的顺序也是类加载的过程,有:加载、连接、初始化。其中 连接 又分为三个步骤(验证、准备、解析),下面我们来了解一下每个步骤的具体执行流程:
3.1.1、加载
在加载的过程中,Java虚拟机会根据类的全限定名(java.lang.String)找到该类对应的二进制 .class 文件,然后将该文件内容读取到内存中(同时将字节流代表的静态存储结构转化为方法区的运行时数据结构,并在堆中创建一个代表这个类的 Class 对象作为访问入口)
3.1.2、连接
3.1.2.1、验证
校验 .class 文件读到的内容是否为合法的(有文件格式验证、字节码验证、符号引用验证等),即判断这些内容是否符合 JVM 规范
3.1.2.2、准备
为类变量分配内存并设置初始值
java
public class PreparationStage {
class Sample {
// 准备阶段处理:
static int intValue; // → 初始化为0
static long longValue; // → 初始化为0L
static boolean flag; // → 初始化为false
static Object obj; // → 初始化为null
static final int CONST = 100; // → 初始化为100(常量)
static int assignedValue = 123;
// 准备阶段:初始化为0
// 初始化阶段:赋值为123
}
}
3.1.2.3、解析
将类、接口、字段和方法的符号引用解析为直接引用,即内存地址,包括将常量池内的符号引用解析为直接引用
java
class Demo {
public void method() {
// 编译时:符号引用
String str = "Hello"; // 符号引用:java/lang/String
// 解析后:直接引用
// 符号引用:"java/lang/String"
// 直接引用:内存地址 0x7f3c5a8b(String类的实际位置)
System.out.println(str); // 方法调用也需要解析
}
}
3.1.3、初始化
真正开始执行Java代码的地方,会执行类的静态初始化代码,对类对象的各项属性进行填充(包括静态成员)
3.2、双亲委派模型
如果一个类加载器收到类加载的请求,这个类加载器会把该请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到最顶层的启动类加载器中,只有父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载

三、垃圾回收(GC)
垃圾回收的过程主要分为两步:寻找垃圾(不再使用的对象)和释放垃圾(释放其内存)
3.1、可回收对象的判断算法
3.1.1、引用计数算法
该算法为:给对象加一个引用计数器,当有引用指向该对象时,计数器就+1,引用失效时,计数器就-1,当该对象的计数器为0时,证明该对象不再被使用1,其内存可以内回收

缺点:
(1)消耗更多的内存:尤其是对象本身比较小,引用计数消耗的空间相对对象来说就比较大
(2)循环引用问题:两对象互相引用,造成" 死锁 "情况
3.1.2、可达性分析算法
上述引用计数算法由于无法解决循环引用问题,所以Java采用了" 可达性分析算法 "来判断对象是否需要回收
该算法的核心思想为:将GC Roots作为起始点,开始向下搜索,走过的路径称为" 引用链 ",当一个对象与 GC Roots 没有任何引用链相连时,证明该对象是不可达的,就会被回收

上图中 Object 5-7 之间虽然有引用链相连接,但是他们与 GC Roots 是不可达的,所以会被判定为可回收对象,也就解决了引用计数算法无法解决的循环引用问题
Java语言中,能够作为GC Roots的对象包括:
(1)虚拟机栈中局部变量引用的对象
(2)方法区中类静态属性引用的对象
(3)方法区中常量池引用的对象
(4)本地方法栈中引用的对象
3.2、垃圾回收算法
3.2.1、标记-清除算法
标记-清除算法将垃圾回收分为两个阶段:标记阶段 和清除阶段 。首先会标记出需要回收的对象,在标记完成后统一回收所有被标记的对象,缺点是会造成内存碎片问题。

3.2.2、复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当需要进行垃圾回收时,从根集合节点开始扫描,标记所有存活对象,并将它们复制到另一块新的内存 ,之后回收原内存,能够确保空闲的内存是连续的,但是内存利用率较低,而且对于数据量较大的情况下回收成本较高

3.2.3、标记-整理算法
标记阶段与"标记-清除"算法相同,但在后续处理中,并非直接回收可回收对象,而是将所有存活对象向内存空间的一端移动,然后直接清理掉边界以外的内存区域

3.2.4、分代收集算法
分代算法通过将内存划分为不同区域,并针对各区域特性采用差异化的垃圾回收策略,从而优化整体回收效率
目前JVM垃圾收集普遍采用"分代收集Generational Collection)"算法,该算法并非创新概念,而是根据对象存活周期的差异将内存划分为不同区域。通常将Java堆分为新生代和老年代:新生代中每次垃圾回收时大部分对象会被清除,仅少量存活,因此采用复制算法;而老年代中对象存活率高且缺乏额外空间进行分配担保,必须使用"标记-清理"或"标记-整理"算法

