JVM-java 虚拟机

学习难度:❤️❤️❤️

线程私有:程序计数器+虚拟机栈+本地方法栈

线程共享:堆+方法区

为什么要有JVM?解决什么问题?

JVM 是java 程序的"操作系统",它让Java 实现"一次编写,到处运行"

复制代码
.java 源文件 → javac 编译 → .class 字节码 → JVM 解释/执行 → 运行

JVM内存结构(五大区域)

复制代码
         ┌─────────────────────────────────┐
         │           JVM 内存              │
         ├─────────────────────────────────┤
         │ 1. 方法区(Method Area)        │ ← 存类信息、静态变量
         │ 2. 堆(Heap)                   │ ← 存对象(最重要!)
         │ 3. 虚拟机栈(VM Stack)         │ ← 存方法调用(局部变量)
         │ 4. 本地方法栈(Native Method)  │ ← 调用 C/C++ 代码
         │ 5. 程序计数器(Program Counter)│ ← 记录当前执行到哪一行
         └─────────────────────────────────┘

1.堆(Heap)-对象的"家"

所有new 出来的对象都存在这里!!!最大的GC 主战场OOM

复制代码
User user = new User();        // user 对象 → 存在堆里
String name = new String("zhang"); // 字符串对象 → 也在堆里
int[] arr = new int[100];      // 数组 → 也在堆里

堆又分为两部分(新生代/老年代)

复制代码
         ┌───────────────────────────────────────────────┐
         │                   堆(Heap)                  │
         ├───────────────────────────────────────────────┤
         │               新生代(Young Generation)     │ ← 约 80% 内存
         │ ┌───────────┐ ┌───────────┐ ┌───────────┐     │
         │ │   Eden    │ │  Survivor │ │  Survivor │     │
         │ │   区      │ │     S0    │ │     S1    │     │
         │ │  (80%)    │ │   (10%)   │ │   (10%)   │     │
         │ └───────────┘ └───────────┘ └───────────┘     │
         │         ↓ Minor GC(频繁)                   │
         │                                               │
         │               老年代(Old Generation)       │ ← 约 20% 内存
         │ ┌─────────────────────────────────────────┐ │
         │ │                                         │ │
         │ │              存活时间长的对象           │ │
         │ │              (经过多次GC还活着)       │ │
         │ │                                         │ │
         │ └─────────────────────────────────────────┘ │
         │         ↓ Major GC / Full GC(少)          │
         └───────────────────────────────────────────────┘

对象在堆中的"一生"(从出生到死亡)

  1. 出生:在Eden 区 (新生区)分配内存
  2. 第一次GC :Minor GC ,当Eden 区 满了 触发最小回收,可达性分析区分(活着/死掉)的对象,活着的复制到SO 区,死的直接清理==复制算法,高效,无碎片。
  3. 第二次GC :Minor GC ,将SO活着的---->复制到 S1 区 ,SO 清空
  4. 没GC 一次对象的 age +1 ,默认 15次 ----->老年代
  5. 老年代15次GC 存活:缓存,单例,长连接
  6. 老年代满了------>Major GC(只清理老年代) /Full GC (常用,堆+方法区全部都清理,System.gc() 手动调用,这是建议不一定执行) (stop the world),标记-清除或者 标记-整理算法
  7. 还是满了 OutOfMemoryError
类型 回收区域 触发条件 停顿时间 是否常见
Minor GC 新生代 Eden 满 短(几ms) ✅ 频繁
Major GC 老年代 老年代满 ❌ 很少单独发生
Full GC 整个堆 + 方法区 多种原因 很长(100ms~几s) ✅ 但要避免
参数 说明
-Xms20m 初始堆大小 20MB
-Xmx20m 最大堆大小 20MB(固定)
-Xmn10m 新生代 10MB
-XX:+PrintGCDetails 打印 GC 详细日志
-XX:+PrintGCTimeStamps 打印时间戳

常见问题

问题 原因 解决方案
OutOfMemoryError: Java heap space 堆内存不足,对象太多 1. 增加堆大小 -Xmx<br>2. 查找内存泄漏(用 MAT 工具)
频繁 Minor GC Eden 区太小,对象太多 增大新生代 -Xmn
频繁 Full GC 老年代碎片化或内存不足 1. 调整老年代大小<br>2. 换 G1/ZGC 回收器
GC 停顿时间长 Full GC 太慢 用 G1、ZGC 等低延迟回收器

2.虚拟机栈(VM Stack)-方法的调用记录

每当调用一个方法,JVM 就给他分配一个"栈帧"(Strack Frame)

  1. 一个线程一个栈

  2. 方法执行完成,栈帧弹出

  3. 栈内存小,速度快

  4. 递归太深,StackOverflowError

    public void methodA() {
    int a = 10; // a 存在栈里
    methodB(); // 调用 methodB → 压入新栈帧
    }

    public void methodB() {
    String name = "zhang"; // name 存在栈里
    }

3.方法区(Method Area)--类的"信息"

  1. 类的信息(类名,方法,字段)

  2. 静态变量(static)

  3. 常量(final )

  4. JDK8之后,方法区==原空间,存在"本地内存"

    public class User {
    public static int count = 0; // count 存在方法区
    public final String type = "user"; // 常量也在这里
    }

4.本地方法栈--执行本地(C/C++)方法

  1. 非java语言实现的方法==本地方法(Native Method)

  2. System.out.println() → 最终会调用 JVM_InvokeConsolePrint() 这种 C++ 写的方法。

    public class Test {
    public static void main(String[] args) {
    System.out.println("Hello"); // println 底层就调用了 native 方法
    }
    }
    Java 方法 → 调用 native 方法 → JVM 进入本地方法栈 → 执行 C/C++ 代码 → 返回结果

    说说本地方法栈的作用
    答:本地方法栈是 JVM 为执行本地方法(Native Method)服务的内存区域。
    当 Java 程序调用用 C/C++ 等语言实现的方法时(比如 Thread.start()、Object.hashCode()),
    JVM 会使用本地方法栈来保存调用信息。
    它和虚拟机栈非常相似,都是线程私有的,也会抛出 StackOverflowError 和 OutOfMemoryError。
    只不过虚拟机栈服务 Java 方法,本地方法栈服务 native 方法。

  • 每当调用一个native 方法,JVM 在本地方法栈中分配一个"栈帧"
  • 方法执行完成,栈帧弹出
  • 与虚拟机栈VS本地方法栈 (一个服务Java 方法,native (非java )方法)

5.程序计数器--记录当前线程执行到哪一行字节码指令了

程序计数器==线程的"书签"

工作原理

复制代码
public void method() {
    int a = 10;        // 行号 1
    int b = 20;        // 行号 2
    int c = a + b;     // 行号 3
    System.out.println(c); // 行号 4
}
为什么程序计数器是线程私有的
答:因为程序计数器用来记录线程执行的位置。
每个线程有自己的执行流程,比如线程A执行到第10行,线程B执行到第5行。
如果共用一个计数器,就会混乱。
所以必须是线程私有的,保证每个线程都能正确恢复执行位置。
而且它是唯一不会发生 OutOfMemoryError 的区域,因为它的内存大小是固定的。

JVM 执行时候:

  1. 线程执行到第3行-》程序计数器=3
  2. 时间片用完,CPU 切换其他的线程
  3. 一会又轮到这个线程-》从程序计数器读取"3",继续执行

对象的创建

对象的一生

复制代码
1. new User() → 在 Eden 区分配内存
2. 方法结束 → 局部变量失效,对象可能成垃圾
3. Minor GC → 扫描,活的对象复制到 S0
4. 再次 GC → S0 活的对象复制到 S1
5. 活了15次 → 升级到老年代
6. 老年代也满了 → Major GC(Full GC)
7. 还不够 → OutOfMemoryError!
  1. 创建
  2. 类加载检查new 指令-》类加载检查(是否被重复加载,解析,初始化过)
  3. 内存分配方式(1.指针碰撞,2.空闲列表)
  4. 初始化零值
  5. 设置对象头
  6. 执行init 方法
  7. 完成

垃圾回收GC

1.垃圾回收是什么

JVM 自动把不再使用的对象找出来,清理掉,腾出空间,"360空间清理大师"

  1. 让开发者专注业务逻辑,不用管内存释放
  2. 防止内存泄漏:程序在运行过程,代码错误(忘记释放资源,引用未清理),导致内存空间一直被占用,无法收回,可用内存在持续减少,看起来像天然气泄露了一样。
  3. 防止内存溢出:OutOfMemory(OOM),程序在申请大块内存/内存耗尽时,系统没有内存了,导致程序异常终止,抛出异常(申请内存>可用内存)

2.怎么判断是垃圾-可达性分析算法

从一些"根对象"出发,访问不到就是"垃圾" -----GC Roots

3.垃圾回收算法

  1. 标记-清除(Mark -Sweep):标记出活的,清理掉死的-->产生内存碎片(小数据占用了大内存),适合老年代----CMS
  2. 复制算法(Copying):From--To 标记活的,复制到To 区 ,清除 From区(新生代,浪费一部分空间)
  3. 标记-整理(Mark-Compent):标记活的,移动整理一起,清理死的(G1,老年代)
  4. 分代收集(Generational Collection):新生代(复制算法 Minor GC ),老年代(标记-清除/整理--->Full GC)

4.垃圾回收器

回收器 作用区域 算法 特点 适用场景
Serial 新生代 复制 单线程,简单 单核 CPU、Client 模式
ParNew 新生代 复制 多线程,配合 CMS 老版本 Web 应用
Parallel Scavenge 新生代 复制 吞吐量优先 批处理、后台计算
CMS 老年代 标记-清除 并发回收,低延迟 JDK 8 Web 项目(已废弃)
G1(Garbage First) 整个堆 分区域复制 可预测停顿,大堆 推荐!互联网系统
ZGC 整个堆 读屏障 + 并发 <1ms 停顿,TB 级内存 极致低延迟
Shenandoah 整个堆 同 ZGC Red Hat 开发,低延迟 大内存应用

1.G1(Garbage First)---现代主流

把堆分为多个Region (小区域),优先回收(garbage first)垃圾最多的 "Region",最大停顿时间

-XX:MaxGCPauseMillis=200 ,,,<500ms ,互联网高并发

2.ZGC ----低延迟

并发<1ms 金融交易+游戏服务器

场景 推荐 GC
普通 Web 应用(Spring Boot) G1(JDK 8+)
大数据、批处理、吞吐优先 Parallel + Parallel Old
老系统、小内存(<4GB) CMS (JDK 8)或 G1
超大堆(>32GB)、极致低延迟 ZGCShenandoah
云原生、容器化部署 ZGC(停顿短,资源利用率高)

现在新项目,直接上 G1 或 ZGC!

5.监控和优化

类加载机制

1.什么是类加载?

JVM把.class文件从磁盘加载到内存,并且生成一个java.lang.Class 对象的过程,称为"类加载"

复制代码
User user = new User(); // 触发类加载

当你第一次使用这个类的时候:

  1. JVM 找User.class 文件
  2. 读取字节码
  3. 在方法区创建Class 对象,类"信息"
  4. 执行静态代码块
  5. 类加载==懒加载,用到才加载

2.类加载的三阶段(加载,链接,初始化)

复制代码
         ┌─────────────────┐
         │    加载         │ ← 找到 .class 文件,加载到内存
         └────────┬────────┘
                  ↓
         ┌────────┴────────┐
         │    链接         │ ← 验证、准备、解析
         └────────┬────────┘
                  ↓
         ┌────────┴────────┐
         │    初始化       │ ← 执行 <clinit> 方法(静态变量赋值、静态代码块)
         └─────────────────┘
  1. 加载完成,JVM 中才有Class 对象,但还没开始用
  2. 链接:验证字节码合法,安全,分配内存,解析
  3. 类构造器,静态代码

3.类加载器

类加载器 加载路径 说明
Bootstrap ClassLoader JAVA_HOME/jre/lib(如 rt.jar C++ 实现,最顶层
Extension ClassLoader jre/lib/ext 加载扩展库
Application ClassLoader classpath(项目类路径) 加载我们写的类
复制代码
          ┌───────────────────────┐
          │ Bootstrap ClassLoader │ ← 加载 rt.jar
          └──────────▲────────────┘
                     │ 委托
          ┌──────────┴────────────┐
          │ Extension ClassLoader │ ← 加载 ext 目录
          └──────────▲────────────┘
                     │ 委托
          ┌──────────┴────────────┐
          │ Application ClassLoader│ ← 加载项目代码
          └───────────────────────┘

4.什么是双亲委派?(委托父/爷类,最后自己)为什么要有?

当一个子类加载器收到加载请求时,先委托父类加载器去加载,只有父类加载不了,自己才尝试加载。

  1. 防止核心类被篡改(发现父类里面有,不会覆盖)
  2. 避免同一个类被"加载器"重复加载

5.打破双亲委派的场景(SPI)

让父类能反过来委托子类加载器去加载类

SPI(Service Provider Interface) 服务接口提供-----JDBC就是典型!

复制代码
Connection conn = DriverManager.getConnection(url, user, pwd);
  1. DriverManager ---->rt.jar 由 根节点 Bootstrap 加载(父类)

  2. com.mysql.cj.jdbc.Driver 由 Application 加载(子类)

  3. 问题产生:导致 根加载器无法加载这个类 (爷爷看不懂孙子的书写什么??)

  4. 解决方案:线程上下文类加载器(ThreadContextClassLoader),说白了就是父类,调用了一个你自己做的类加载器(默认是AppClassLoader() )

    // DriverManager 中的代码
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    ServiceLoader.load(Driver.class, cl); // 用 Application ClassLoader 去加载

ClassNotFoundException (类的路径找不到),

场景 说明
JDBC DriverManager 加载第三方驱动
JNDI 查找资源时加载用户实现
Spring 加载用户定义的 BeanFactoryPostProcessor
Tomcat 每个 Web 应用有自己的类加载器,通过上下文加载
SPI(服务发现) ServiceLoader 默认使用上下文类加载器

6.实现一个类加载器

继承ClassLoader +类路径

复制代码
public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = loadByte(name);
        return defineClass(name, data, 0, data.length);
    }

    private byte[] loadByte(String name) {
        String fileName = classPath + File.separator + name.replace(".", "/") + ".class";
        try (FileInputStream fis = new FileInputStream(fileName);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int ch;
            while ((ch = fis.read()) != -1) {
                baos.write(ch);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

使用

复制代码
MyClassLoader myLoader = new MyClassLoader("C:/myclasses");
Class<?> clazz = myLoader.loadClass("com.example.User");
Object obj = clazz.newInstance();

✅ 用于热部署、加密类加载、模块化系统

如果对您有帮助,给个免费点赞❤️❤️❤️❤️!!!!!!!

相关推荐
小北方城市网5 小时前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
六义义6 小时前
java基础十二
java·数据结构·算法
ʚB҉L҉A҉C҉K҉.҉基҉德҉^҉大6 小时前
自动化机器学习(AutoML)库TPOT使用指南
jvm·数据库·python
毕设源码-钟学长6 小时前
【开题答辩全过程】以 基于SpringBoot的智能书城推荐系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
笨手笨脚の7 小时前
深入理解 Java 虚拟机-03 垃圾收集
java·jvm·垃圾回收·标记清除·标记复制·标记整理
莫问前路漫漫7 小时前
WinMerge v2.16.41 中文绿色版深度解析:文件对比与合并的全能工具
java·开发语言·python·jdk·ai编程
九皇叔叔7 小时前
【03】SpringBoot3 MybatisPlus BaseMapper 源码分析
java·开发语言·mybatis·mybatis plus
挖矿大亨7 小时前
c++中的函数模版
java·c++·算法
dyyx1117 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
weixin_499771558 小时前
使用Seaborn绘制统计图形:更美更简单
jvm·数据库·python