深入理解 JVM:Java 虚拟机的核心原理与调优指南

目录

前言

[一、JVM 简介](#一、JVM 简介)

[二、JVM 发展史](#二、JVM 发展史)

[1. Sun Classic VM(1996 年,JDK1.0)](#1. Sun Classic VM(1996 年,JDK1.0))

[2. Exact VM(JDK1.2)](#2. Exact VM(JDK1.2))

[3. HotSpot VM(当前主流)](#3. HotSpot VM(当前主流))

[4. JRockit(专注服务器端)](#4. JRockit(专注服务器端))

[5. J9 VM(IBM)](#5. J9 VM(IBM))

[6. Taobao JVM(国产深度定制)](#6. Taobao JVM(国产深度定制))

[关键补充:JVM 与《Java 虚拟机规范》](#关键补充:JVM 与《Java 虚拟机规范》)

[三、JVM 执行流程](#三、JVM 执行流程)

[1. 核心组件](#1. 核心组件)

[2. 完整执行流程](#2. 完整执行流程)

四、运行时数据区(内存布局)

[1. 堆(线程共享)](#1. 堆(线程共享))

[2. 虚拟机栈(线程私有)](#2. 虚拟机栈(线程私有))

[3. 本地方法栈(线程私有)](#3. 本地方法栈(线程私有))

[4. 程序计数器(线程私有)](#4. 程序计数器(线程私有))

[5. 方法区(线程共享)](#5. 方法区(线程共享))

[6. 内存溢出异常(OOM)详解](#6. 内存溢出异常(OOM)详解)

[(1)Java 堆溢出(java.lang.OutOfMemoryError: Java heap space)](#(1)Java 堆溢出(java.lang.OutOfMemoryError: Java heap space))

[(2)虚拟机栈 / 本地方法栈溢出](#(2)虚拟机栈 / 本地方法栈溢出)

[(3)元空间溢出(java.lang.OutOfMemoryError: Metaspace)](#(3)元空间溢出(java.lang.OutOfMemoryError: Metaspace))

五、类加载机制

[1. 类的生命周期](#1. 类的生命周期)

[2. 类加载核心流程](#2. 类加载核心流程)

(1)加载(Loading)

(2)验证(Verification)

(3)准备(Preparation)

(4)解析(Resolution)

(5)初始化(Initialization)

[3. 双亲委派模型](#3. 双亲委派模型)

(1)类加载器层次结构

(2)双亲委派模型的工作流程

(3)双亲委派模型的核心优势

[4. 破坏双亲委派模型的案例(SPI 机制:JDBC)](#4. 破坏双亲委派模型的案例(SPI 机制:JDBC))

(1)问题背景

(2)解决方案:线程上下文类加载器

(3)核心源码(DriverManager)

(4)总结

六、垃圾回收(GC)

[1. 对象存活判断算法](#1. 对象存活判断算法)

(1)引用计数法(淘汰)

(2)可达性分析算法(主流)

(3)引用的扩展分类(JDK1.2)

[2. 垃圾回收算法](#2. 垃圾回收算法)

[(1)标记 - 清除算法(基础)](#(1)标记 - 清除算法(基础))

(2)复制算法(新生代首选)

[(3)标记 - 整理算法(老年代首选)](#(3)标记 - 整理算法(老年代首选))

(4)分代收集算法(实际应用)

[面试题:Minor GC 与 Full GC 的区别](#面试题:Minor GC 与 Full GC 的区别)

[3. 垃圾收集器(HotSpot)](#3. 垃圾收集器(HotSpot))

(1)核心概念

(2)收集器分类与特性

(3)重点收集器详解

[① CMS 收集器(低停顿首选)](#① CMS 收集器(低停顿首选))

[② G1 收集器(全区域首选)](#② G1 收集器(全区域首选))

[4. 一个对象的一生(总结)](#4. 一个对象的一生(总结))

[七、Java 内存模型(JMM)](#七、Java 内存模型(JMM))

[1. 核心概念:主内存与工作内存](#1. 核心概念:主内存与工作内存)

[2. 内存间交互操作(8 种原子操作)](#2. 内存间交互操作(8 种原子操作))

[3. JMM 的三大核心特性](#3. JMM 的三大核心特性)

(1)原子性

(2)可见性

(3)有序性

[4. volatile 关键字详解](#4. volatile 关键字详解)

(1)核心特性

(2)不保证原子性的示例

[(3)volatile 的适用场景](#(3)volatile 的适用场景)

(4)禁止指令重排序的作用

[5. 双重检查锁定单例模式(volatile 应用)](#5. 双重检查锁定单例模式(volatile 应用))

[(1)问题代码(未用 volatile)](#(1)问题代码(未用 volatile))

[(2)修复方案(volatile 修饰 instance)](#(2)修复方案(volatile 修饰 instance))

总结


前言

作为 Java 开发者,我们每天都在编写代码、运行程序。但你是否曾想过,你的代码是如何从.java文件变成可执行程序的?为什么有时程序会突然变慢甚至崩溃?这些问题背后,都离不开 Java 虚拟机的支持。

JVM 就像一位幕后导演,它不直接参与表演(不执行你的代码),但决定了表演的规则、舞台的布置、演员的调度。掌握 JVM 的工作原理,不仅能让你写出更高效的代码,还能在遇到性能问题时快速定位并解决。


一、JVM 简介

JVM(Java Virtual Machine,Java 虚拟机)是通过软件模拟的、具备完整硬件功能的计算机系统,运行在完全隔离的环境中,是 Java 程序跨平台运行的核心基础。

与 VMware、VirtualBox 等通用虚拟机不同,JVM 是一款 "定制化的虚拟计算机",核心差异体现在指令集模拟上:

  • VMware/VirtualBox:模拟物理 CPU 的完整指令集,包含大量寄存器,可运行任意操作系统。
  • JVM:仅模拟 Java 字节码的指令集,仅保留 PC 寄存器(程序计数器),其他寄存器均被裁剪,专门为执行 Java 程序设计。

JVM 的核心价值是 "一次编译,到处执行"------Java 源代码编译为字节码(.class 文件)后,无需针对不同操作系统重新编译,可在任何安装了对应 JVM 的平台上运行,屏蔽了底层硬件和操作系统的差异。

二、JVM 发展史

自 1996 年 Java 1.0 发布以来,JVM 经历了多代演进,不同厂商推出了各具特色的实现,主流版本如下:

1. Sun Classic VM(1996 年,JDK1.0)

  • 世界上第一款商业 JVM,伴随 Java 1.0 诞生,是 Java 早期生态的核心支撑。
  • 核心特性:内部仅提供解释器,无内置编译器(JIT);若需使用 JIT 编译器,需额外外挂,且一旦启用 JIT,将完全接管执行系统,解释器不再工作(二者无法协同)。
  • 命运:JDK1.4 时被完全淘汰,目前 HotSpot 虚拟机中仍内置了该版本的兼容实现。

2. Exact VM(JDK1.2)

  • 为解决 Classic VM 的性能问题而生,是现代高性能 JVM 的雏形。
  • 核心特性:支持热点探测(识别高频执行的 "热点代码",编译为字节码加速执行);实现了解释器与编译器的混合工作模式,兼顾启动速度和运行效率。
  • 命运:仅在 Solaris 平台短暂使用,其他平台仍沿用 Classic VM,最终因兼容性和生态问题被 HotSpot 替代。

3. HotSpot VM(当前主流)

  • 历史渊源:最初由 Longview Technologies 公司设计,1997 年被 Sun 收购,2009 年随 Sun 被 Oracle 收购;JDK1.3 时成为默认虚拟机,至今占据绝对市场地位。
  • 核心特性:名称源于 "热点代码探测技术"------ 通过计数器识别最具编译价值的代码,触发即时编译(JIT)或栈上替换;编译器与解释器协同工作,在程序响应时间和执行性能间取得最佳平衡。
  • 应用场景:Sun/Oracle JDK 和 OpenJDK 的默认虚拟机,覆盖服务器、桌面、移动端、嵌入式等全场景。

4. JRockit(专注服务器端)

  • 核心定位:专为服务器端应用优化,不关注启动速度,仅保留 JIT 编译器,无解释器实现,所有代码均通过 JIT 编译后执行。
  • 核心优势:运行速度快,被称为 "世界上最快的 JVM",部分场景性能提升超 70%;提供 JRockit Real Time(面向延迟敏感型应用,响应时间达毫秒 / 微秒级)和 MissionControl(低开销监控分析工具)。
  • 命运:2008 年随 BEA 被 Oracle 收购,Oracle 在 JDK8 中完成其与 HotSpot 的整合,将 JRockit 的优秀特性移植到 HotSpot 中。

5. J9 VM(IBM)

  • 全称:IBM Technology for Java Virtual Machine(IT4J),内部代号 J9。
  • 核心定位:与 HotSpot 类似,是多用途 JVM,适用于服务器端、桌面应用、嵌入式等场景,广泛用于 IBM 的各类 Java 产品。
  • 核心优势:号称 "世界上最快的 Java 虚拟机",在 IBM 自有产品上稳定性极强。
  • 命运:2017 年 IBM 将其开源,命名为 OpenJ9,交由 Eclipse 基金会管理,更名为 Eclipse OpenJ9。

6. Taobao JVM(国产深度定制)

  • 研发背景:由阿里 AliJVM 团队基于 OpenJDK HotSpot 开发,是国内首个开源的高性能服务器级 JVM,为阿里云计算、金融、电商等高并发场景量身定制。
  • 核心特性:
    1. GCIH(GC invisible heap)技术:实现 "堆外存储",将长生命周期对象移至堆外,GC 无法管理,降低 GC 回收频率、提升回收效率。
    2. 跨进程对象共享:GCIH 中的对象可在多个 JVM 进程间共享,节省内存开销。
    3. 优化 JNI 调用:通过 crc32 指令实现 JVM intrinsic,降低 JNI 调用开销。
    4. 硬件适配:针对 Intel CPU 优化,牺牲部分兼容性换取极致性能。
    5. ZenGC:专为大数据场景设计的垃圾收集器。
  • 应用场景:已全面应用于淘宝、天猫等阿里核心产品,替换了 Oracle 官方 JVM。

关键补充:JVM 与《Java 虚拟机规范》

上述所有 JVM 实现(HotSpot、J9、Taobao JVM 等)均需遵循《Java 虚拟机规范》------Oracle 发布的 Java 领域权威著作,详细定义了 JVM 的组成、指令集、内存模型等核心规范,确保不同厂商的 JVM 能正确执行 Java 字节码。本文后续内容均以 HotSpot(Oracle JDK 默认虚拟机)为核心展开。

三、JVM 执行流程

JVM 的核心职责是将 Java 字节码转换为底层系统指令并执行,完整流程涉及四大核心组件,执行步骤如下:

1. 核心组件

  • 类加载器(ClassLoader):负责加载字节码文件(.class)到内存。
  • 运行时数据区(Runtime Data Area):存储类信息、对象实例、线程状态等数据。
  • 执行引擎(Execution Engine):将字节码翻译为底层系统指令,交由 CPU 执行。
  • 本地库接口(Native Interface):调用 C/C++ 等本地方法库,实现 Java 无法直接完成的底层功能(如操作系统交互、硬件操作)。

2. 完整执行流程

  1. 编译:Java 源代码(.java)通过 javac 编译器编译为字节码文件(.class),字节码是 JVM 的指令集规范,不依赖底层操作系统。
  2. 加载:类加载器根据类的全限定名,读取字节码文件,将其转换为方法区的运行时数据结构,并在内存中生成代表该类的 java.lang.Class 对象(访问入口)。
  3. 存储:类信息、静态变量、常量等存入方法区;对象实例存入堆;线程执行方法时,生成栈帧存入虚拟机栈;本地方法执行时使用本地方法栈;程序计数器记录线程执行位置。
  4. 执行:执行引擎通过解释器或 JIT 编译器,将字节码翻译为 CPU 可执行的机器指令;若涉及底层操作(如文件 IO),通过本地库接口调用本地方法库完成。
  5. 收尾:程序执行完毕后,垃圾收集器回收堆中无用对象,线程终止后释放虚拟机栈、本地方法栈等线程私有资源。

四、运行时数据区(内存布局)

运行时数据区是 JVM 在运行过程中分配和管理内存的区域,与 "Java 内存模型(JMM)" 是完全不同的概念 ------ 前者是 JVM 的物理内存划分,后者是并发编程的内存访问规范。运行时数据区分为 5 个部分,按 "线程共享 / 私有" 可分为两类:

类型 包含区域 核心特点
线程共享 堆、方法区(元空间) 所有线程共用,生命周期与 JVM 一致,需垃圾回收
线程私有 虚拟机栈、本地方法栈、程序计数器 每个线程独立拥有,生命周期与线程一致,无需垃圾回收

1. 堆(线程共享)

  • 核心作用:存储 Java 程序中创建的所有对象实例(包括数组),是 JVM 内存中最大的区域,也是垃圾回收的主要目标。
  • 内存参数:通过 JVM 参数配置大小,-Xms指定最小启动内存,-Xmx指定最大运行内存(如-Xms20m -Xmx20m表示堆内存固定为 20MB)。
  • 区域划分:
    • 新生代(Young 区):存储新建对象,分为 1 个 Eden 区和 2 个 Survivor 区(S0/S1,也叫 From/To 区),默认比例为 Eden:S0:S1=8:1:1。
    • 老年代(Old 区):存储经过多次垃圾回收后仍存活的对象、大对象(超过新生代阈值的对象)。
  • 垃圾回收逻辑:新生代触发 Minor GC(复制算法),老年代触发 Full GC(标记 - 整理算法);Minor GC 时,Eden 区存活对象复制到空闲的 Survivor 区,清理 Eden 和已使用的 Survivor 区;对象在 Survivor 区复制次数达到阈值(默认 15 次,由MaxTenuringThreshold参数控制)后,晋升到老年代。

2. 虚拟机栈(线程私有)

  • 核心作用:描述 Java 方法的执行内存模型,每个方法执行时都会创建一个 "栈帧",栈帧入栈(方法调用)和出栈(方法返回)的过程即方法的执行过程。
  • 生命周期:与线程一致,线程启动时创建,线程终止时销毁,不存在线程安全问题。
  • 栈帧组成:
    1. 局部变量表:存储方法参数和局部变量,包含 8 大基本数据类型、对象引用(地址指针),内存空间在编译期间确定,执行时不可修改。
    2. 操作数栈:方法执行时的临时数据存储区,通过入栈、出栈操作完成运算(如a+b需先将 a、b 入栈,执行加法后将结果入栈)。
    3. 动态连接:指向运行时常量池的方法引用,用于将符号引用转换为直接引用(支持方法重写等动态特性)。
    4. 方法返回地址:存储方法执行完毕后返回的 PC 寄存器地址,确保线程能恢复到之前的执行位置。
  • 关键概念:"线程私有" 指每个线程拥有独立的虚拟机栈,线程切换时无需担心栈数据冲突,因为处理器同一时刻仅执行一条线程的指令。

3. 本地方法栈(线程私有)

  • 核心作用:与虚拟机栈功能类似,区别在于虚拟机栈为 Java 方法服务,本地方法栈为 Native 方法(由 C/C++ 实现,被 Java 调用的方法)服务。
  • 生命周期:与线程一致,线程终止时销毁。
  • 异常:若本地方法执行时栈深度超过限制,会抛出 StackOverflowError;若扩展栈时无法申请到足够内存,会抛出 OutOfMemoryError。

4. 程序计数器(线程私有)

  • 核心作用:记录当前线程执行的字节码行号指示器,相当于线程的 "执行进度条"。
  • 具体逻辑:
    • 若线程执行 Java 方法,计数器存储当前执行的字节码指令地址。
    • 若线程执行 Native 方法,计数器值为空(Undefined)。
  • 核心特点:JVM 规范中唯一没有规定 OutOfMemoryError 的区域 ------ 内存占用极小,且生命周期与线程一致,无需额外内存分配。

5. 方法区(线程共享)

  • 核心作用:存储被虚拟机加载的类信息(类名、父类、接口、字段、方法)、常量、静态变量、即时编译器编译后的代码等数据。
  • 实现差异:
    • JDK7 及之前:称为 "永久代(PermGen)",属于 JVM 内存的一部分,大小通过-XX:PermSize-XX:MaxPermSize配置。
    • JDK8 及之后:改为 "元空间(Metaspace)",使用本地内存(操作系统内存),大小不再受 JVM 内存限制,仅受本地内存大小约束。
  • 关键变化(JDK8):
    1. 字符串常量池从永久代移至堆中(减少永久代内存溢出风险)。
    2. 静态变量、类元信息仍存储在元空间。
  • 运行时常量池:方法区的一部分,存储字面量(字符串、final 常量、基本数据类型值)和符号引用(类全限定名、字段 / 方法名称及描述符),是字节码文件中 "常量池表" 的运行时体现。

6. 内存溢出异常(OOM)详解

运行时数据区的各区域均可能出现内存溢出,不同区域的异常场景和排查方式不同:

(1)Java 堆溢出(java.lang.OutOfMemoryError: Java heap space)
  • 触发条件:不断创建对象,且 GC Roots 到对象存在可达路径(避免被 GC 回收),当对象数量超过堆最大容量时触发。

  • 测试配置:JVM 参数-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError(堆固定 20MB,溢出时生成内存快照)。

  • 测试代码:

    java 复制代码
    public class HeapOOMTest {
        static class OOMObject {}
        public static void main(String[] args) {
            List<OOMObject> list = new ArrayList<>();
            while (true) {
                list.add(new OOMObject()); // 无限创建对象,无法回收
            }
        }
    }
  • 排查思路:通过 MAT 等工具分析内存快照,判断是 "内存泄漏"(对象无用但无法回收)还是 "内存溢出"(对象确实需要存活,但堆内存不足);前者修复代码释放引用,后者增大-Xmx参数。

(2)虚拟机栈 / 本地方法栈溢出

HotSpot 将虚拟机栈和本地方法栈合二为一,溢出分为两种情况:

  • StackOverflowError:线程请求的栈深度超过虚拟机允许的最大深度(单线程递归过深)。

    • 测试配置:-Xss128k(减小栈容量,易触发溢出)。

    • 测试代码:

      java 复制代码
      public class StackOverflowTest {
          private int stackLength = 1;
          public void stackLeak() {
              stackLength++;
              stackLeak(); // 无限递归
          }
          public static void main(String[] args) {
              StackOverflowTest test = new StackOverflowTest();
              try {
                  test.stackLeak();
              } catch (Throwable e) {
                  System.out.println("栈深度:" + test.stackLength);
                  throw e;
              }
          }
      }
  • OutOfMemoryError:虚拟机扩展栈时无法申请到足够内存(多线程创建过多,耗尽系统内存)。

    • 测试代码:

      java 复制代码
      public class StackOOMTest {
          public void dontStop() {
              while (true) {}
          }
          public void stackLeakByThread() {
              while (true) {
                  new Thread(() -> dontStop()).start(); // 无限创建线程
              }
          }
          public static void main(String[] args) {
              new StackOOMTest().stackLeakByThread();
          }
      }
    • 注意:该代码会耗尽系统内存,运行前需保存工作。

  • 排查思路:StackOverflowError 需减少递归深度或增大-Xss;OutOfMemoryError 需减少线程数,或通过减小堆内存、栈容量换取更多线程创建空间。

(3)元空间溢出(java.lang.OutOfMemoryError: Metaspace)
  • 触发条件:加载过多类(如动态生成类、依赖过多 jar 包),元空间(本地内存)不足。
  • 测试配置:-XX:MaxMetaspaceSize=10m(限制元空间最大 10MB)。
  • 排查思路:增大-XX:MaxMetaspaceSize参数,或减少不必要的类加载(如清理冗余依赖、避免动态类滥用)。

五、类加载机制

类加载是 JVM 将字节码文件转换为可执行代码的过程,核心包括 "类加载流程""双亲委派模型""破坏双亲委派模型的场景" 三部分。

1. 类的生命周期

一个类从加载到卸载的完整生命周期为:加载 → 连接(验证→准备→解析) → 初始化 → 使用 → 卸载。其中 "加载、验证、准备、解析、初始化" 是类加载的核心流程,顺序固定。

2. 类加载核心流程

(1)加载(Loading)
  • 核心任务:
    1. 通过类的全限定名(如java.lang.String)获取定义该类的二进制字节流(可从.class 文件、网络、内存等来源获取)。
    2. 将字节流的静态存储结构转换为方法区的运行时数据结构(类元信息)。
    3. 在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区中该类数据的访问入口(存储在堆中)。
  • 关键说明:"加载" 是 "类加载(Class Loading)" 的第一步,二者不可混淆 ------"类加载" 是完整流程(加载→连接→初始化),"加载" 仅指上述三步操作。
(2)验证(Verification)
  • 核心目的:确保字节码文件的信息符合《Java 虚拟机规范》,避免恶意或无效字节码危害 JVM 安全。
  • 验证内容:
    1. 文件格式验证:验证字节码文件的魔数、版本号、常量池格式等(确保是合法的.class 文件)。
    2. 字节码验证:验证字节码指令的语义合法性(如避免非法跳转、类型转换错误)。
    3. 符号引用验证:验证常量池中的符号引用(类、字段、方法引用)是否有效(如引用的类是否存在)。
(3)准备(Preparation)
  • 核心任务:为类中静态变量(被static修饰的变量)分配内存,并设置 "默认初始值"(而非代码中指定的初始值)。
  • 示例:若类中有public static int value = 123;,准备阶段会为value分配内存,设置默认值 0(int 类型默认值),123的赋值会在初始化阶段执行。
  • 特殊情况:若静态变量被final修饰(public static final int value = 123;),准备阶段会直接设置为 123------final静态变量是 "常量",编译时已确定值,存储在常量池。
(4)解析(Resolution)
  • 核心任务:将常量池中的 "符号引用" 替换为 "直接引用"。
  • 概念区分:
    • 符号引用:用字符串描述目标(如Ljava/lang/String;表示 String 类),与内存地址无关。
    • 直接引用:指向目标的内存地址(如对象指针、方法入口地址),可直接访问目标。
  • 解析对象:类、接口、字段、方法、方法类型等。
(5)初始化(Initialization)
  • 核心任务:执行类构造器<clinit>()方法,完成静态变量的赋值(代码中指定的初始值)和静态代码块的执行。
  • 关键细节:
    1. <clinit>()方法由编译器自动生成,整合了静态变量赋值语句和静态代码块(按代码顺序执行)。
    2. 若类中无静态变量和静态代码块,编译器不会生成<clinit>()方法。
    3. 父类的<clinit>()方法会先于子类执行(保证父类静态变量初始化完成)。
    4. 初始化阶段是类加载流程中唯一由应用程序主导的阶段,仅在类被首次主动使用时触发(如创建对象、调用静态方法、访问静态变量)。

3. 双亲委派模型

(1)类加载器层次结构

JVM 中存在多层类加载器,自 JDK1.2 以来形成固定架构:

  • 启动类加载器(Bootstrap ClassLoader):最顶层,由 C++ 实现,是 JVM 的一部分;加载 JDK 核心类库($JAVA_HOME/lib目录下的 jar 包,如 rt.jar)。
  • 扩展类加载器(Extension ClassLoader):由 Java 实现,加载$JAVA_HOME/lib/ext目录下的扩展类库。
  • 应用程序类加载器(Application ClassLoader):由 Java 实现,也称系统类加载器;加载 CLASSPATH 指定的类(应用程序代码、第三方 jar 包)。
  • 自定义类加载器:继承java.lang.ClassLoader,由开发者实现,用于加载特殊来源的类(如加密字节码、网络字节码)。
(2)双亲委派模型的工作流程

"双亲委派" 是类加载的核心规则:当一个类加载器收到类加载请求时,不会直接加载,而是先将请求委派给父类加载器;父类加载器若无法加载(搜索范围中无该类),子类加载器才会尝试自己加载。流程如下:

  1. 应用程序类加载器收到加载请求,先委派给扩展类加载器。
  2. 扩展类加载器收到请求,委派给启动类加载器。
  3. 启动类加载器在核心类库中搜索,若找到则加载;若未找到,返回给扩展类加载器。
  4. 扩展类加载器在扩展类库中搜索,若找到则加载;若未找到,返回给应用程序类加载器。
  5. 应用程序类加载器在 CLASSPATH 中搜索,若找到则加载;若未找到,抛出ClassNotFoundException
(3)双亲委派模型的核心优势
  1. 避免类重复加载:若父类加载器已加载某类,子类加载器无需重复加载,节省内存。
  2. 保证核心类库安全:核心类库(如java.lang.Object)由启动类加载器加载,开发者无法通过自定义类加载器替换核心类(如自定义java.lang.Object类),避免恶意代码篡改核心功能。

4. 破坏双亲委派模型的案例(SPI 机制:JDBC)

双亲委派模型虽有优势,但在某些场景下需 "破坏" 以满足功能需求,最典型的案例是 Java 的 SPI(Service Provider Interface)机制,以 JDBC 为例:

(1)问题背景
  • JDBC 的核心接口(java.sql.DriverDriverManager)定义在 JDK 的 rt.jar 包中,由启动类加载器加载。
  • 数据库驱动(如 MySQL 的com.mysql.cj.jdbc.Driver)是第三方实现,存在于 CLASSPATH 中的 mysql-connector-java.jar 包,需由应用程序类加载器加载。
  • 按双亲委派模型,启动类加载器加载的DriverManager无法访问应用程序类加载器加载的第三方驱动(父类加载器无法访问子类加载器加载的类),导致驱动无法被调用。
(2)解决方案:线程上下文类加载器

DriverManager通过 "线程上下文类加载器"(Thread.currentThread ().getContextClassLoader ())破坏双亲委派模型,核心流程如下:

  1. 线程启动时,线程上下文类加载器默认设置为应用程序类加载器。
  2. DriverManagergetConnection()方法中,通过线程上下文类加载器获取应用程序类加载器。
  3. 用该类加载器加载第三方数据库驱动(如 MySQL 的 Driver 类),实现启动类加载器加载的核心类访问子类加载器加载的第三方类。
(3)核心源码(DriverManager)
java 复制代码
private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException {
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized (DriverManager.class) {
        if (callerCL == null) {
            // 获取线程上下文类加载器(默认是应用程序类加载器)
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }
    // 用callerCL加载第三方驱动
    for (DriverInfo aDriver : registeredDrivers) {
        if (isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                return aDriver.driver.connect(url, info);
            } catch (SQLException ex) {
                // 异常处理
            }
        }
    }
    throw new SQLException("No suitable driver found");
}
(4)总结

线程上下文类加载器的本质是 "逆向委派"------ 允许父类加载器通过子类加载器加载类,打破了双亲委派模型 "父类加载器优先" 的规则,解决了 SPI 机制中核心类库与第三方实现的协作问题。

六、垃圾回收(GC)

垃圾回收是 JVM 自动回收堆和方法区中无用对象的过程,核心目标是释放内存,避免内存泄漏。GC 的核心流程是 "判断对象是否存活 → 回收无用对象",涉及 "对象存活判断算法""垃圾回收算法""垃圾收集器" 三部分。

1. 对象存活判断算法

GC 前需先判断对象是否 "死亡"(无用),主流 JVM 采用 "可达性分析算法",辅助以 "引用计数法"(已淘汰)。

(1)引用计数法(淘汰)
  • 核心逻辑:给每个对象添加引用计数器,有引用时计数器 + 1,引用失效时 - 1;计数器为 0 时,对象死亡。

  • 优点:实现简单,判定效率高。

  • 缺点:无法解决 "循环引用" 问题 ------ 两个对象互相引用,计数器均为 1,但实际已无用,无法被回收。

  • 示例(循环引用):

    java 复制代码
    public class ReferenceCountTest {
        public Object instance = null;
        private static final int _1MB = 1024 * 1024;
        private byte[] bigSize = new byte[2 * _1MB]; // 占用内存,便于观察GC
    
        public static void testGC() {
            ReferenceCountTest a = new ReferenceCountTest();
            ReferenceCountTest b = new ReferenceCountTest();
            a.instance = b; // a引用b
            b.instance = a; // b引用a
            a = null; // 取消引用
            b = null; // 取消引用
            System.gc(); // 触发GC
        }
    
        public static void main(String[] args) {
            testGC();
        }
    }
  • 运行结果:GC 日志显示6092K->856K,说明循环引用的对象被回收,证明 HotSpot 未使用引用计数法。

(2)可达性分析算法(主流)
  • 核心逻辑:以 "GC Roots" 为起始点,向下搜索引用链;若对象到 GC Roots 无任何引用链相连(不可达),则判定为死亡对象。
  • GC Roots 的可选对象(Java 语言中):
    1. 虚拟机栈(栈帧本地变量表)中引用的对象(如方法参数、局部变量)。
    2. 方法区中类静态属性引用的对象(如static Object obj = new Object())。
    3. 方法区中常量引用的对象(如final Object obj = new Object())。
    4. 本地方法栈中 JNI(Native 方法)引用的对象。
  • 示例:对象 Object5-Object7 虽互相引用,但到 GC Roots 不可达,被判定为可回收对象。
(3)引用的扩展分类(JDK1.2)

JDK1.2 后,Java 对 "引用" 的概念进行扩展,分为四级(强度递减),影响对象的存活判定:

  1. 强引用:程序中普遍存在的引用(如Object obj = new Object()),只要强引用存在,GC 永不回收对象。
  2. 软引用:描述 "有用但非必需" 的对象(SoftReference类),系统内存不足时(即将 OOM),会回收软引用关联的对象。
  3. 弱引用:描述 "非必需" 的对象(WeakReference类),GC 触发时,无论内存是否充足,都会回收弱引用关联的对象。
  4. 虚引用:最弱的引用(PhantomReference类),无法通过虚引用获取对象,仅用于在对象被 GC 时接收系统通知(跟踪对象回收状态)。

2. 垃圾回收算法

垃圾回收算法是 GC 的核心思想,不同算法适用于不同场景,主流算法包括以下四种:

(1)标记 - 清除算法(基础)
  • 核心流程:分为 "标记" 和 "清除" 两步。
    1. 标记:通过可达性分析,标记所有需要回收的死亡对象。
    2. 清除:遍历堆内存,回收所有被标记的对象,释放内存空间。
  • 优点:实现简单,无需移动对象。
  • 缺点:
    1. 效率低:标记和清除过程均需遍历所有对象,耗时较长。
    2. 内存碎片:回收后产生大量不连续的内存碎片,后续分配大对象时,可能因无法找到足够连续内存而提前触发 GC。
(2)复制算法(新生代首选)
  • 核心流程:将可用内存划分为大小相等的两块(A 区和 B 区),每次仅使用一块。
    1. 标记:标记 A 区中存活的对象。
    2. 复制:将 A 区存活对象复制到 B 区,按顺序排列(无内存碎片)。
    3. 清除:清空 A 区,下次使用 B 区,重复上述流程。
  • 优点:实现简单,运行高效,无内存碎片。
  • 缺点:内存利用率低(仅 50%),不适用于对象存活率高的场景(需频繁复制)。
  • 优化(HotSpot 新生代实现):新生代中 98% 的对象 "朝生夕死",无需 1:1 划分内存,而是分为 1 个 Eden 区(大)和 2 个 Survivor 区(小),默认比例 Eden:S0:S1=8:1:1。
    1. 初始分配:对象优先在 Eden 区创建。
    2. Minor GC 触发:Eden 区满时,标记 Eden 和 S0 区的存活对象,复制到 S1 区,清空 Eden 和 S0 区。
    3. Survivor 区切换:下次 Minor GC 时,标记 Eden 和 S1 区的存活对象,复制到 S0 区,清空 Eden 和 S1 区。
    4. 晋升老年代:对象在 S0 和 S1 区之间复制次数达到阈值(默认 15 次)后,晋升到老年代;若 Survivor 区空间不足,直接晋升老年代(分配担保)。
    5. 内存利用率:新生代可用内存为 90%(8+1),仅 10% 用于存储存活对象。
(3)标记 - 整理算法(老年代首选)
  • 核心流程:结合 "标记 - 清除" 和 "复制" 的优点,适用于对象存活率高的老年代。
    1. 标记:标记所有死亡对象。
    2. 整理:将所有存活对象向内存一端移动,紧凑排列。
    3. 清除:清空存活对象另一端的内存空间。
  • 优点:无内存碎片,内存利用率高。
  • 缺点:效率低于复制算法(需移动对象,额外消耗 CPU 资源)。
(4)分代收集算法(实际应用)
  • 核心思想:根据对象存活周期的不同,将堆分为新生代和老年代,采用不同的回收算法,兼顾效率和内存利用率。
    1. 新生代:对象存活时间短、存活率低,采用复制算法(高效、无碎片)。
    2. 老年代:对象存活时间长、存活率高,采用标记 - 整理算法(无碎片、高利用率)。
  • 补充:方法区的回收(元空间):回收废弃常量和无用类(类的所有实例已回收、类加载器已回收、无引用指向类对象),频率较低。
面试题:Minor GC 与 Full GC 的区别
维度 Minor GC(新生代 GC) Full GC(老年代 GC/Major GC)
触发区域 新生代(Eden/Survivor) 老年代(可伴随 Minor GC)
触发条件 Eden 区满 老年代满、元空间满、调用 System.gc () 等
回收算法 复制算法 标记 - 整理算法(或标记 - 清除)
执行效率 快(对象存活率低,复制量小) 慢(对象存活率高,需整理),约为 Minor GC 的 10 倍
影响范围 仅新生代线程,影响小 全堆回收,影响大(STW 时间长)

3. 垃圾收集器(HotSpot)

垃圾收集器是垃圾回收算法的具体实现,HotSpot 虚拟机提供了多款收集器,适用于不同场景(吞吐量、低停顿),核心收集器如下:

(1)核心概念
  • 并行(Parallel):多条 GC 线程同时工作,用户线程暂停(STW)。
  • 并发(Concurrent):GC 线程与用户线程同时执行(交替运行),用户线程无需长时间暂停。
  • 吞吐量:CPU 用于运行用户代码的时间 / CPU 总消耗时间(吞吐量 = 用户代码时间 / (用户代码时间 + GC 时间))。
(2)收集器分类与特性
收集器 适用区域 核心特性 回收算法 并发 / 并行 优点 缺点
Serial 新生代 单线程,STW 复制算法 串行 实现简单,单 CPU 效率高 多 CPU 场景效率低,STW 时间长
ParNew 新生代 多线程(Serial 多线程版),STW 复制算法 并行 支持与 CMS 配合,多 CPU 效率高 单 CPU 场景有线程交互开销
Parallel Scavenge 新生代 多线程,吞吐量优先 复制算法 并行 自适应调节策略,可控制吞吐量和停顿时间 不支持与 CMS 配合
Serial Old 老年代 单线程,STW 标记 - 整理 串行 实现简单,Client 模式默认 多 CPU 场景效率低
Parallel Old 老年代 多线程,吞吐量优先 标记 - 整理 并行 与 Parallel Scavenge 搭配,吞吐量最优 停顿时间较长
CMS(Concurrent Mark Sweep) 老年代 并发收集,低停顿 标记 - 清除 并发 响应速度快,STW 时间短 占用 CPU 资源,产生内存碎片,无法处理浮动垃圾
G1(Garbage First) 全区域 分区收集,兼顾吞吐量与低停顿 标记 - 整理 + 复制 并发 + 并行 无内存碎片,可预测停顿时间 实现复杂,小内存场景效率低
(3)重点收集器详解
① CMS 收集器(低停顿首选)
  • 核心目标:获取最短回收停顿时间,适用于 B/S 系统、互联网应用等对响应速度敏感的场景。
  • 运作流程(4 步):
    1. 初始标记(Initial Mark):标记 GC Roots 直接关联的对象,速度快,需 STW。
    2. 并发标记(Concurrent Mark):遍历引用链,标记所有死亡对象,与用户线程并发执行,无需 STW。
    3. 重新标记(Remark):修正并发标记期间因用户线程执行导致标记变动的对象,需 STW(停顿时间短于初始标记)。
    4. 并发清除(Concurrent Sweep):回收死亡对象,与用户线程并发执行,无需 STW。
  • 核心缺点:
    1. CPU 敏感:并发阶段占用 CPU 资源,导致应用程序吞吐量下降(默认 GC 线程数 = (CPU 数 + 3)/4)。
    2. 浮动垃圾:并发清除阶段用户线程仍产生新垃圾,无法在本次 GC 中回收,需预留内存空间(避免 OOM)。
    3. 内存碎片:基于标记 - 清除算法,产生大量碎片,可能导致提前触发 Full GC。
② G1 收集器(全区域首选)
  • 核心定位:面向服务端应用,旨在替代 CMS,兼顾吞吐量和低停顿,适用于大堆内存场景(如 10GB 以上)。
  • 核心特性:
    1. 分区收集:将堆划分为多个大小相等的 Region(区域),每个 Region 可动态标记为 Eden、Survivor、Old、Humongous(存储大对象,超过 Region 大小 50%)。
    2. 优先回收:基于 "Garbage First" 策略,优先回收垃圾最多的 Region(垃圾回收率最高)。
    3. 无内存碎片:整体采用标记 - 整理算法,局部(Region 之间)采用复制算法,回收后自动压缩内存。
    4. 可预测停顿时间:通过参数-XX:MaxGCPauseMillis设置最大停顿时间,G1 会动态调整回收 Region 数量,确保停顿时间不超标。
  • 运作流程(4 步):
    1. 初始标记:标记 GC Roots 直接关联的对象,与 Minor GC 同步执行,需 STW。
    2. 并发标记:遍历引用链,标记死亡对象,与用户线程并发执行;同时计算每个 Region 的垃圾回收率。
    3. 最终标记(Remark):修正并发标记期间的标记变动,采用 SATB(快照原子性)算法,STW 时间短。
    4. 筛选回收(Clean Up/Copy):筛选垃圾回收率高的 Region,复制存活对象到空 Region,清空原 Region;与 Minor GC 同步执行,需 STW。

4. 一个对象的一生(总结)

  1. 诞生:对象在新生代 Eden 区创建。
  2. 新生代流转:Eden 区满触发 Minor GC,存活对象复制到 Survivor 区(S0/S1),反复流转。
  3. 晋升老年代:对象在 Survivor 区复制 15 次后(默认阈值),或为大对象,晋升到老年代。
  4. 老年代存活:在老年代中持续存活,经历多次 Full GC。
  5. 死亡:当对象到 GC Roots 不可达,被标记为死亡对象,最终被 GC 回收(新生代复制算法 / 老年代标记 - 整理算法)。

七、Java 内存模型(JMM)

JMM(Java Memory Model)是 JVM 定义的内存访问规范,用于屏蔽不同硬件和操作系统的内存访问差异,确保 Java 程序在多线程环境下的并发安全性(原子性、可见性、有序性)。

1. 核心概念:主内存与工作内存

JMM 定义了线程与内存的交互规则,将内存分为两类:

  • 主内存:存储所有线程共享的变量(实例字段、静态字段、数组元素),是物理内存的抽象。
  • 工作内存:每个线程私有的内存区域,存储主内存变量的副本拷贝;线程对变量的所有操作(读取、赋值)均在工作内存中执行,无法直接访问主内存。

2. 内存间交互操作(8 种原子操作)

JMM 定义了 8 种原子操作,确保主内存与工作内存之间的数据同步,操作需满足原子性、不可分割:

  • lock(锁定):作用于主内存变量,标记为线程独占状态。
  • unlock(解锁):作用于主内存变量,释放锁定状态,允许其他线程锁定。
  • read(读取):作用于主内存变量,将变量值传输到线程工作内存。
  • load(载入):作用于工作内存变量,将 read 操作获取的变量值存入工作内存副本。
  • use(使用):作用于工作内存变量,将变量值传递给执行引擎(如运算)。
  • assign(赋值):作用于工作内存变量,将执行引擎的结果赋给变量副本。
  • store(存储):作用于工作内存变量,将变量值传输到主内存。
  • write(写入):作用于主内存变量,将 store 操作获取的变量值存入主内存。

3. JMM 的三大核心特性

并发程序正确执行需保证三大特性,JMM 通过上述操作和关键字(volatilesynchronized)实现:

(1)原子性
  • 定义:一个操作或多个操作要么全部执行且不被打断,要么全部不执行。
  • JMM 保证:read、load、assign、use、store、write 等基本操作原子性;更大范围的原子性(如i++)需通过 synchronizedjava.util.concurrent.locks锁实现。
  • 示例:i++并非原子操作(分为读取 i、i+1、赋值给 i 三步),多线程环境下可能出现线程安全问题。
(2)可见性
  • 定义:当一个线程修改共享变量的值,其他线程能立即感知到该修改。
  • 实现方式:
    1. volatile:修饰的变量修改后,会立即同步到主内存,其他线程读取时直接从主内存加载(禁用工作内存缓存)。
    2. synchronized:解锁前,线程需将工作内存中变量的修改同步到主内存;加锁时,线程清空工作内存副本,从主内存重新加载变量(happen-before 原则)。
    3. final:final 修饰的变量初始化完成后,其值对所有线程可见(不可修改)。
(3)有序性
  • 定义:线程内操作按代码顺序执行(线程内串行);线程间观察时,操作可能无序(指令重排序、工作内存与主内存同步延迟)。
  • 指令重排序:编译器或 CPU 为优化性能,在不影响单线程执行结果的前提下,调整指令执行顺序(如a=1; b=2可能被重排为b=2; a=1)。
  • 实现方式:
    1. volatile:禁止指令重排序(修饰的变量前后的指令不可重排)。
    2. synchronized:保证同一时刻只有一个线程执行同步块,间接保证有序性。
    3. happen-before 原则(先天有序性):无需任何关键字,JMM 默认保证的有序性,包括:
      • 程序次序规则:线程内代码按书写顺序执行。
      • 锁定规则:unlock 操作先行发生于后面对同一锁的 lock 操作。
      • volatile 变量规则:对 volatile 变量的写操作先行发生于读操作。
      • 传递规则:A 先行发生于 B,B 先行发生于 C,则 A 先行发生于 C。
      • 线程启动规则:Thread.start () 先行发生于线程内所有操作。
      • 线程中断规则:interrupt () 先行发生于线程检测到中断事件。
      • 线程终结规则:线程内所有操作先行发生于线程终止检测(如 join ())。
      • 对象终结规则:对象初始化完成先行发生于 finalize () 方法。

4. volatile 关键字详解

volatile是 JMM 提供的最轻量级同步机制,仅保证可见性和有序性,不保证原子性。

(1)核心特性
  1. 可见性:volatile 变量的修改会立即同步到主内存,其他线程读取时直接从主内存加载,避免缓存不一致。
  2. 禁止指令重排序:volatile 变量前后的指令不可重排,确保代码执行顺序与预期一致。
(2)不保证原子性的示例
java 复制代码
public class VolatileAtomicTest {
    public static volatile int num = 0;
    public static void increase() {
        num++; // 非原子操作,volatile无法保证
    }
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    increase();
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(num); // 结果小于1000,存在线程安全问题
    }
}
  • 原因:num++分为读取 num、num+1、赋值给 num 三步,volatile 仅保证读取时获取最新值,但运算过程中可能被其他线程修改,导致结果覆盖。
(3)volatile 的适用场景

需满足以下条件之一:

  1. 运算结果不依赖变量当前值(如布尔标志位)。
  2. 仅单一线程修改变量,其他线程读取。
  • 示例(正确场景):

    java 复制代码
    volatile boolean shutdownRequested;
    public void shutdown() {
        shutdownRequested = true; // 单一线程修改
    }
    public void work() {
        while (!shutdownRequested) { // 多线程读取
            // 执行任务
        }
    }
(4)禁止指令重排序的作用
  • 示例:

    java 复制代码
    // x、y为非volatile变量,flag为volatile变量
    x = 2; // 语句1
    y = 0; // 语句2
    flag = true; // 语句3
    x = 4; // 语句4
    y = -1; // 语句5
  • 规则:语句 3(volatile 写)不能重排到语句 1、2 之前,也不能重排到语句 4、5 之后;语句 1、2 之间,语句 4、5 之间可重排。

  • 意义:确保执行到语句 3 时,语句 1、2 的修改已完成且可见,避免多线程环境下因重排导致的逻辑错误。

5. 双重检查锁定单例模式(volatile 应用)

(1)问题代码(未用 volatile)
java 复制代码
public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) { // 加锁
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 非原子操作
                }
            }
        }
        return instance;
    }
}
  • 问题:instance = new Singleton()分为三步:1. 分配内存;2. 初始化成员变量;3. 赋值给 instance(instance 非 null)。JIT 编译可能重排为 1→3→2,导致线程 B 在第二次检查时看到 instance 非 null,但未初始化完成,使用时抛出异常。
(2)修复方案(volatile 修饰 instance)
java 复制代码
public class Singleton {
    private volatile static Singleton instance = null; // 禁止重排
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // 重排被禁止,确保1→2→3执行
                }
            }
        }
        return instance;
    }
}
  • 原理:volatile 禁止instance = new Singleton()的指令重排,确保对象初始化完成后,才将 instance 赋值为非 null,避免线程安全问题。

总结

  1. 内存布局:堆、栈、方法区各司其职

  2. 类加载:双亲委派确保安全,SPI 等场景需要打破规则

  3. 垃圾回收:分代收集是核心思想,不同场景选择不同收集器

  4. 内存模型:JMM 为并发编程提供保障

相关推荐
雨季6664 小时前
构建 OpenHarmony 简易文字行数统计器:用字符串分割实现纯文本结构感知
开发语言·前端·javascript·flutter·ui·dart
雨季6664 小时前
Flutter 三端应用实战:OpenHarmony 简易倒序文本查看器开发指南
开发语言·javascript·flutter·ui
小北方城市网4 小时前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
进击的小头4 小时前
行为型模式:策略模式的C语言实战指南
c语言·开发语言·策略模式
天马37984 小时前
Canvas 倾斜矩形绘制波浪效果
开发语言·前端·javascript
六义义4 小时前
java基础十二
java·数据结构·算法
Tansmjs4 小时前
C++与GPU计算(CUDA)
开发语言·c++·算法
qx094 小时前
esm模块与commonjs模块相互调用的方法
开发语言·前端·javascript
Suchadar5 小时前
if判断语句——Python
开发语言·python
ʚB҉L҉A҉C҉K҉.҉基҉德҉^҉大5 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python