JVM三剑客:内存模型、类加载机制与垃圾回收精讲

一、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堆分为新生代和老年代:新生代中每次垃圾回收时大部分对象会被清除,仅少量存活,因此采用复制算法;而老年代中对象存活率高且缺乏额外空间进行分配担保,必须使用"标记-清理"或"标记-整理"算法

相关推荐
青云计划6 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿6 小时前
Jsoniter(java版本)使用介绍
java·开发语言
探路者继续奋斗7 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
消失的旧时光-19437 小时前
第十九课:为什么要引入消息队列?——异步系统设计思想
java·开发语言
A懿轩A7 小时前
【Java 基础编程】Java 面向对象入门:类与对象、构造器、this 关键字,小白也能写 OOP
java·开发语言
乐观勇敢坚强的老彭8 小时前
c++寒假营day03
java·开发语言·c++
biubiubiu07068 小时前
谷歌浏览器无法访问localhost:8080
java
大黄说说8 小时前
新手选语言不再纠结:Java、Python、Go、JavaScript 四大热门语言全景对比与学习路线建议
java·python·golang
烟沙九洲8 小时前
Java 中的 封装、继承、多态
java
识君啊9 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端