不做大哥好多年 不做大哥好多年
首页
  • MySQL
  • Redis
  • Elasticsearch
  • Kafka
  • Etcd
  • MongoDB
  • TiDB
  • RabbitMQ
  • 01.GO基础
  • 02.面向对象
  • 03.并发编程
  • 04.常用库
  • 05.数据库操作
  • 06.Beego框架
  • 07.Beego商城
  • 08.GIN框架
  • 09.GIN论坛
  • 10.微服务
  • 01.Python基础
  • 02.Python模块
  • 03.Django
  • 04.Flask
  • 05.SYL
  • 06.Celery
  • 10.微服务
  • 01.Java基础
  • 02.面向对象
  • 03.Java进阶
  • 04.Web基础
  • 05.Spring框架
  • 100.微服务
  • Docker
  • K8S
  • 容器原理
  • Istio
  • 数据结构
  • 算法基础
  • 算法题分类
  • 前置知识
  • PyTorch
  • 01.Python
  • 02.GO
  • 03.Java
  • 04.业务问题
  • 05.关键技术
  • 06.项目常识
  • 10.计算机基础
  • Linux基础
  • Linux高级
  • Nginx
  • KeepAlive
  • ansible
  • zabbix
  • Shell
  • Linux内核

逍遥子

不做大哥好多年
首页
  • MySQL
  • Redis
  • Elasticsearch
  • Kafka
  • Etcd
  • MongoDB
  • TiDB
  • RabbitMQ
  • 01.GO基础
  • 02.面向对象
  • 03.并发编程
  • 04.常用库
  • 05.数据库操作
  • 06.Beego框架
  • 07.Beego商城
  • 08.GIN框架
  • 09.GIN论坛
  • 10.微服务
  • 01.Python基础
  • 02.Python模块
  • 03.Django
  • 04.Flask
  • 05.SYL
  • 06.Celery
  • 10.微服务
  • 01.Java基础
  • 02.面向对象
  • 03.Java进阶
  • 04.Web基础
  • 05.Spring框架
  • 100.微服务
  • Docker
  • K8S
  • 容器原理
  • Istio
  • 数据结构
  • 算法基础
  • 算法题分类
  • 前置知识
  • PyTorch
  • 01.Python
  • 02.GO
  • 03.Java
  • 04.业务问题
  • 05.关键技术
  • 06.项目常识
  • 10.计算机基础
  • Linux基础
  • Linux高级
  • Nginx
  • KeepAlive
  • ansible
  • zabbix
  • Shell
  • Linux内核
  • Python

  • GO

  • Java

    • 01.Java基础
    • 02.Java虚拟机
      • 01.JVM 体系结构
        • 0、Java虚拟机概述
        • 1、类加载子系统
        • 1) 类加载过程 七阶段
        • 2) 类加载器
        • 3) 运行时数据区
        • 2、执行引擎
      • 02.JVM 内存管理
        • 1、对象创建
        • 2、对象 内存分配
        • 3、对象的访问方式
        • 4、垃圾回收(GC)
        • 1)可达性分析
        • 2)常见垃圾回收算法
        • 5、逃逸分析
        • 6、内存分配策略
      • 03.JVM 线程管理与同步
        • 1、线程模型
        • 2、Java 内存模型(JMM)
        • 3、synchronized、volatile 关键字
        • 4、对象的内存可见性
        • 5、锁优化
    • 100.JVM vs Golang
  • 业务问题

  • 关键技术

  • 项目常识

  • 计算机基础

  • 常识
  • Java
xiaonaiqiang
2025-02-19
目录

02.Java虚拟机

# 01.JVM 体系结构

# 0、Java虚拟机概述

  • Java虚拟机(JVM)是一个虚拟计算机,用于执行Java字节码,屏蔽了底层操作系统的差异,实现了跨平台运行

  • JVM 负责字节码加载、验证、执行和优化,包括 JIT 编译和垃圾回收等机制

  • JVM 通过类加载器、堆栈管理、GC 机制等提供自动化的内存管理,减少手动管理的负担

  • 举例说明:假设有一个简单的Java程序

    • 1. 编译:javac HelloWorld.java 生成 HelloWorld.class 字节码文件

    • 2. 运行:java HelloWorld 启动JVM,加载并执行字节码

    • 3. 执行过程:

      • 类加载:JVM加载 HelloWorld.class
      • 字节码验证:确保字节码合法
      • 解释/编译:JVM解释或编译字节码为机器码
      • 执行:调用 System.out.println 输出 "Hello, World!"
      • 垃圾回收:程序结束后,JVM回收内存
    • public class HelloWorld {
          public static void main(String[] args) {
              System.out.println("Hello, World!");
          }
      }
      
      1
      2
      3
      4
      5

# 1、类加载子系统

# 1) 类加载过程 七阶段

  • JVM 的类加载子系统负责从 .class 文件中加载字节码,并将其转换为 JVM 运行时可以理解的 Class 对象
  • 类的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载,并由不同的 类加载器 负责加载
  • 类加载过程(加载、验证、准备、解析、初始化、使用、卸载)

  • ① 加载:读取 class 字节码,生成 Class 对象

    • Class<?> clazz = Class.forName("com.example.MyClass");
      
      1
  • ② 验证:检查字节码格式、元数据、字节码合法性

  • ③ 准备:为类变量分配内存,初始化默认值(不执行代码)

    • class Example {
          // JVM 只会给 a 赋默认值 0,真正赋值 10 会在初始化阶段进行
          static int a = 10;
      }
      
      1
      2
      3
      4
  • ④ 解析:符号引用转换为直接引用

    • 符号引用(如 com/example/MyClass)存储的,而不是直接的内存地址
    • 直接引用 解析后,JVM 将符号引用替换为实际内存地址,便于后续执行
  • ⑤ 初始化:执行 <clinit> 方法,初始化静态变量和代码块

    • 初始化顺序:父类先于子类,静态变量和静态代码块按顺序执行

    • <clinit> 方法只执行一次,且在类的首次主动使用时触发

    • // 准备阶段:a = 0(默认值)
      // 初始化阶段:执行 static 代码块,a = 20,输出 "Static Block Executed"
      class Example {
          static int a = 10;
          static { 
              a = 20; 
              System.out.println("Static Block Executed");
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
  • ⑥ 使用:类进入运行状态,可创建对象或调用静态方法

  • ⑦ 卸载:无引用的 Class 对象被回收

    • 当一个 Class 没有任何实例且没有被其他类引用,JVM 的 垃圾回收器(GC) 可能会回收该 Class 对象,释放方法区的内存
    • 但在 JVM 运行期间,Bootstrap ClassLoader 加载的类不会被卸载

# 2) 类加载器

  • JVM 采用“父委托机制”(Parent Delegation Model)来加载类,避免重复加载,提升安全性
  • JVM 内置类加载器
类加载器 作用 代码获取方式
Bootstrap ClassLoader 加载 java.lang 包及核心类(rt.jar / java.base) 由 C/C++ 实现,非 Java 代码
Extension ClassLoader 加载 lib/ext/ 目录下的类(扩展类) sun.misc.Launcher$ExtClassLoader
Application ClassLoader 加载应用程序的 classpath 下的类 sun.misc.Launcher$AppClassLoader
  • 自定义类加载器

    • 自定义 ClassLoader 适用于 动态加载类(如热部署、插件机制)

    • class MyClassLoader extends ClassLoader {
          @Override
          protected Class<?> findClass(String name) throws ClassNotFoundException {
              byte[] bytes = loadClassData(name);  // 读取字节码
              return defineClass(name, bytes, 0, bytes.length);
          }
      }
      
      1
      2
      3
      4
      5
      6
      7

# 3) 运行时数据区

  • JVM 运行时将内存划分为多个区域

    • 区域 作用
      方法区(Metaspace) 存储类的元信息、静态变量、常量池
      堆(Heap) 存储对象实例,GC 主要管理的区域
      栈(Stack) 线程私有,存储局部变量和方法调用栈帧
      程序计数器(PC Register) 记录当前线程执行的指令地址
      本地方法栈(Native Stack) 供 Native 方法(JNI)使
  • 示例(栈与堆)

    • class Example {
          int x = 10;   // 堆内存(Heap)
          void method() {
              int y = 5; // 栈内存(Stack)
          }
      }
      
      1
      2
      3
      4
      5
      6

# 2、执行引擎

  • JVM 执行引擎(Execution Engine)负责 将 Java 字节码转换为机器码,并交由 CPU 执行

  • 主要有 三种执行方式

    • 编译方式 运行方式 优势 劣势 适用场景
      解释器 逐行解释执行 启动快 运行慢 短时间运行的程序、JVM 启动时
      JIT 编译器 热点代码编译为机器码 运行快、动态优化 启动慢 服务器端、高性能应用
      AOT 编译器 预编译为本地代码 启动快 缺乏 JIT 优化 云计算、微服务、嵌入式
  • 1)解释器

    • 逐行解释执行 Java 字节码,每次运行都需要重新解析相同的代码

    • 启动速度快,无需额外的编译步骤

    • 适用于一次性执行的代码,如脚本、短时间运行的任务

    • // 逐行读取字节码,每次执行 System.out.println(i) 时都重新解释
      // 无法缓存执行结果,导致重复解析相同代码,影响性能
      for (int i = 0; i < 1000; i++) {
          System.out.println(i);
      }
      
      1
      2
      3
      4
      5
  • 2)JIT 编译器(即时编译)

    • 动态分析代码,识别热点代码(高频调用的代码),将其编译成本地机器码

    • 缓存已编译的机器码,下次调用时可直接执行,避免重复解释

    • 适用于长时间运行的应用,如 Web 服务器、大数据计算、游戏引擎等

    • 缺点

      • 初次执行较慢,因为 JIT 需要运行一段时间收集信息,识别热点代码后才会优化
      • 占用额外 CPU 资源,运行时进行编译需要 CPU 计算,可能影响其他任务的执行
    • // JIT 会发现 compute() 方法被频繁调用,将其编译成本地机器码,下次直接执行
      for (int i = 0; i < 1000; i++) {
         compute();
      }
      
      1
      2
      3
      4
  • 3)AOT 编译器(Ahead-of-Time)

    • 在程序运行前,将 Java 字节码直接编译为本地机器码,省去 JIT 运行时编译的开销
    • 优点:
      • 启动速度快,不依赖 JIT,即可获得接近本地代码的执行性能
      • 适用于资源受限的环境(如云计算、容器、微服务)
    • 缺点:
      • 缺乏 JIT 的动态优化能力,可能在长时间运行的应用中不如 JIT 高效
      • 需要额外的编译时间和存储空间
    • 适用于云计算、微服务、嵌入式系统等需要快速启动的应用

# 02.JVM 内存管理

  • 1️⃣ 对象创建(类加载、内存分配)
  • 2️⃣ 对象分配(新生代 / 老年代 / 大对象)
  • 3️⃣ 对象访问(句柄 / 直接指针)
  • 4️⃣ 垃圾回收(可达性分析 & GC 选择)
  • 5️⃣ 逃逸分析优化(减少 GC 负担)
  • 6️⃣ 根据对象生命周期决定内存分配策略

# 1、对象创建

1️⃣ 对象创建:JVM 如何创建一个对象?

  • 1)类加载检查

    • JVM 先检查类是否已加载 到 方法区(Method Area),如果未加载,则触发 类加载(Class Loading)
  • 2)分配内存(在堆中申请对象所需空间)

    • 指针碰撞(Bump-the-Pointer):适用于 堆内存连续 的场景,只需移动指针即可分配内存

    • 空闲列表(Free List):适用于 堆内存碎片化 的情况,需要维护一个空闲块列表来寻找合适的内存区域

  • 3)初始化对象

    • 默认值初始化(0/false/null)

    • 调用构造方法(Constructor) 进行自定义初始化

  • 示例: 对象 p 的创建流程

    • JVM 检查 Person 类是否已加载到 方法区

    • 在 堆内存 分配 Person 对象(使用指针碰撞或空闲列表)

    • 对象头 存储 GC 年龄、类型指针、锁状态等信息

    • name = "Alice", age = 25 赋值,调用构造方法完成初始化

    • public class Person {
          String name;
          int age;
      
          public Person(String name, int age) {
              this.name = name;
              this.age = age;
          }
      
          public static void main(String[] args) {
              Person p = new Person("Alice", 25);
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

# 2、对象 内存分配

  • Java 使用 分代回收 机制,内存被划分为

    • 新生代:Eden + Survivor(S0/S1)

    • 老年代:存放长期存活的对象

    • 永久代:存储类元数据、方法等

  • 新生代 & 老年代 的对象分配规则

    • 新对象优先在 Eden 区分配(默认情况)

    • Eden 区满了触发 Minor GC,存活的对象移动到 Survivor(S0 / S1)

    • Survivor 区对象经过 N 次 GC 后(默认 15 次,-XX:MaxTenuringThreshold 可调节),晋升到 老年代

    • 大对象(如 1MB+ 的数组)直接进入老年代,避免在 Survivor 复制增加开销(-XX:PretenureSizeThreshold 控制)

# 3、对象的访问方式

JVM 主要有 两种对象访问方式:句柄 vs 直接指针

  • 句柄访问(Handle) p → 句柄地址 → 对象地址

    • JVM 维护一个 句柄池,对象引用指向 句柄地址,再由句柄找到 实际对象地址

    • 适用于对象频繁移动的 GC 策略(如 G1 GC)

  • 直接指针访问(Direct Pointer) p → 对象地址

    • 引用直接指向对象,提高访问速度

    • 适用于普通 GC(如 Parallel GC、CMS),因为对象位置不经常变化

# 4、垃圾回收(GC)

# 1)可达性分析

  • JVM 采用 可达性分析算法 判断对象是否存活

    • 即 从 GC Roots 开始遍历可达对象,不可达的对象会被标记为垃圾,等待回收

    • GC Roots 是指 JVM 认为始终存活的对象

  • 以下几类对象被认为是 GC Roots: 栈中局部变量、静态变量、JNI 引用

    • public class GCRootDemo {
          private static GCRootDemo staticRef;
          private GCRootDemo instanceRef;
      
          public static void main(String[] args) {
              // ① 方法栈中的局部变量
              GCRootDemo localVar = new GCRootDemo();
      
              // ② 静态变量
              staticRef = new GCRootDemo();
      
              // 被可达对象引用,也存活
              localVar.instanceRef = new GCRootDemo();
      
              // 触发 GC
              System.gc();
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18

# 2)常见垃圾回收算法

  • JVM 的 GC 采用 多种垃圾回收算法,适用于不同场景
GC 算法 优点 缺点 适用场景
标记-清除 直接清理垃圾对象,不需要额外空间 产生大量内存碎片,影响后续内存分配性能 老年代 GC
复制 无碎片化,分配速度快,适用于新生代对象 需要额外 50% 内存,浪费空间 新生代(Eden → Survivor)
标记-整理 无碎片化,整理后分配效率高 效率比复制算法稍低,但比标记-清除高 老年代 GC
分代回收 结合不同算法,针对不同对象优化回收 需要额外的代管理策略 Java 堆整体
  • ① 标记-清除(Mark-Sweep)

    • 标记:从 GC Roots 开始标记所有可达对象
    • 清除:删除不可达对象,释放空间(但会产生内存碎片)
    • 缺点:
      • 碎片化问题,回收后内存不连续,可能导致大对象分配失败
  • ② 复制(Copying)

    • 将存活对象 复制 到另一块新区域
    • 清空旧区域,减少碎片化
    • 缺点:
      • 内存浪费:需要 额外 50% 空间 存放对象
  • ③ 标记-整理(Mark-Compact)

    • 标记存活对象(和标记-清除一样)。

    • 将所有存活对象移动到内存的一端,使得内存连续,避免碎片化

    • 移动后,清理未使用的空间,使得新的对象可以顺利分配

    • 适用场景:

      • 适用于 老年代 GC,因为老年代对象生命周期较长,存活率较高,使用 复制算法成本太高(占用过多内存)
  • ④ 分代回收(Generational GC)

    • 分代回收 是 JVM 主要的 GC 策略
    • 大部分对象生命周期很短,应尽快回收(新生代)
    • 少部分对象生命周期较长,应该使用更高效的回收方式(老年代)

# 5、逃逸分析

  • 在 Java 中,"逃逸"指的是 对象的作用范围超出了当前方法或线程的控制
  • 导致 JVM 无法在栈上分配对象,而必须将其分配到堆,从而增加 GC 负担
  • Java 中的逃逸(Escape)可以分为两种

  • ① 方法逃逸(Method Escape)

    • 对象被 方法返回 或 赋值给方法外部的变量,JVM 无法确定其生命周期,只能分配到堆中

    • public class EscapeExample {
          private static Object globalObj;
      
          public void setGlobalObj() {
              globalObj = new Object(); // 对象逃逸到全局作用域
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
  • ② 线程逃逸(Thread Escape)

    • 对象被多个线程共享,JVM 必须确保数据安全性,通常使用 堆分配 + 线程同步

    • public void escapeToThread() {
          sharedObject = new Object(); // 逃逸到多线程
          new Thread(() -> {
              System.out.println(sharedObject); // 另一个线程访问
          }).start();
      }
      
      1
      2
      3
      4
      5
      6

# 6、内存分配策略

JVM 采用 分代回收,不同生命周期的对象采用不同策略

  • 短生命周期对象 → 新生代(Eden 分配)
  • 长生命周期对象 → 老年代(多次 GC 后晋升)
  • 大对象 → 直接进入老年代(避免频繁复制)

📌 JVM 调优:

  • -XX:NewRatio=2(年轻代和老年代比例)
  • -XX:SurvivorRatio=8(Eden 和 Survivor 比例)

# 03.JVM 线程管理与同步

# 1、线程模型

JVM 的线程模型基于 操作系统原生线程,即 Java 的 Thread 实际上是 映射到操作系统的线程,由 CPU 直接调度,这意味着

  • 每个 Java 线程几乎都是一个独立的 OS 线程,需要 内核调度,上下文切换成本较高。
  • 适用于计算密集型任务,但 当线程数过多时,性能会下降(如频繁切换导致开销大)。

用户线程 vs. 守护线程

  • 用户线程:应用程序主动创建的线程,如 Thread、ExecutorService 线程池。
  • 守护线程:
    • JVM 后台运行的线程,比如 GC 线程、JIT 编译器线程
    • 当所有用户线程结束,JVM 也会退出,守护线程不会阻止 JVM 关闭

Golang 线程模型(对比 JVM)

  • Goroutine 轻量级:Goroutine 不是 OS 线程,而是 由 Go 运行时管理,调度更加灵活
  • M:N 线程调度:多个 Goroutine 可复用少量 OS 线程,避免线程过多导致的上下文

# 2、Java 内存模型(JMM)

Java 内存模型(JMM)主要用于 解决 CPU 缓存一致性问题,确保多线程间的 可见性、有序性、原子性

JMM 的基本结构

  • 主内存:所有共享变量存储的位置,所有线程都能访问。
  • 工作内存:
    • 每个线程 有自己的工作内存,它是寄存器、CPU 缓存等的抽象
    • 线程只能直接操作自己的工作内存中的变量

happens-before 规则(核心内存可见性规则)

  • synchronized 释放锁前,变量必须刷新到主存(保证可见性)
  • volatile 变量的写入对所有线程立即可见(保证可见性)。
  • 线程 start() 之前的操作对新线程可见(保证启动时能看到正确数据)。

# 3、synchronized、volatile 关键字

  • synchronized 关键字可以修饰 方法 和 代码块,用于保证 线程安全:

    • 可见性:线程释放锁前,变量修改必须刷新到主内存

    • 原子性:同一时间只有一个线程能持有锁

    • 有序性:锁保证代码块内的指令不会被重排序

    • public synchronized void method() {
          // 线程安全方法
      }
      
      public void method2() {
          synchronized (this) {
              // 线程安全代码块
          }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
  • volatile 用于保证 变量的可见性 和 禁止指令重排序,但 不保证原子性

    • 可见性:写入 volatile 变量后,JMM 立即刷新到主存,使其他线程可见

    • 禁止指令重排序:防止编译器优化导致执行顺序不符合预期

    • volatile boolean flag = true;
      
      public void stop() {
          flag = false; // 线程修改,立即对其他线程可见
      }
      
      1
      2
      3
      4
      5

# 4、对象的内存可见性

内存可见性问题:

  • 多线程可能会读取 旧值,因为变量更新可能还在 CPU 缓存,尚未刷新到 主存

  • volatile 可以保证变量每次读取都直接从主存获取,避免缓存一致性问题

  • synchronized 通过 锁释放时刷写主存,确保线程间变量可见性

  • 关联点:

    • JMM 定义的 happens-before 规则 影响线程同步方式

    • 锁机制和 volatile 结合 可提升可见性和性能

# 5、锁优化

  • ① 偏向锁、轻量级锁、重量级锁

    • JVM 在 synchronized 执行时,锁可以 动态升级 以提高性能

    • 偏向锁适用于单线程访问,若线程竞争升级为轻量级锁

    • 轻量级锁使用 CAS,适用于短暂竞争,而竞争激烈时会升级为 重量级锁

    • 锁类型 适用场景 优点 缺点
      偏向锁 线程独占锁 减少 CAS 操作 竞争时需要升级
      轻量级锁 线程短暂竞争 采用 CAS 自旋 竞争激烈时性能下降
      重量级锁 多线程竞争激烈 线程安全 线程阻塞,切换开销大
  • ② 自旋锁、锁消除、锁粗化

    • 自旋锁:

      • 线程等待时,不进入阻塞状态,而是不断尝试获取锁,减少上下文切换开销
      • JVM 通过 -XX:+UseSpinning 启用自旋锁
    • 锁消除:

      • JIT 编译时发现 不可能竞争的锁,会自动去掉,减少开销
      • 例如局部变量的 synchronized 可能会被 JVM 消除
    • 锁粗化:

      • 如果同一线程 频繁加锁/解锁,JVM 可能会 扩大锁范围,减少加锁次数,提高效率

      • 例如:

        synchronized(obj) { a++; }
        synchronized(obj) { b++; }
        
        // 会被优化为:
        synchronized(obj) {
            a++;
            b++;
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
  • ③ CAS(Compare And Swap) CAS 是无锁并发的核心技术

    • 通过 cmpxchg 指令 在多核 CPU 直接操作内存,避免线程阻塞

    • 适用于 原子变量(AtomicInteger、AtomicReference)

    • AtomicInteger count = new AtomicInteger(0);
      count.incrementAndGet();  // CAS 方式保证原子性
      
      1
      2
    • 关联点:

      • CAS 与 volatile 结合可实现无锁同步,适用于高并发场景

      • CAS 可能导致 ABA 问题(可用 AtomicStampedReference 解决)

上次更新: 2025/2/19 16:42:39
01.Java基础
100.JVM vs Golang

← 01.Java基础 100.JVM vs Golang→

最近更新
01
04.数组双指针排序_子数组
03-25
02
08.动态规划
03-25
03
06.回溯算法
03-25
更多文章>
Theme by Vdoing | Copyright © 2019-2025 逍遥子 技术博客 京ICP备2021005373号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式