学习难度:❤️❤️❤️

线程私有:程序计数器+虚拟机栈+本地方法栈
线程共享:堆+方法区
为什么要有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(少) │
└───────────────────────────────────────────────┘
对象在堆中的"一生"(从出生到死亡)
- 出生:在Eden 区 (新生区)分配内存
- 第一次GC :Minor GC ,当Eden 区 满了 触发最小回收,可达性分析区分(活着/死掉)的对象,活着的复制到SO 区,死的直接清理==复制算法,高效,无碎片。
- 第二次GC :Minor GC ,将SO活着的---->复制到 S1 区 ,SO 清空
- 没GC 一次对象的 age +1 ,默认 15次 ----->老年代
- 老年代15次GC 存活:缓存,单例,长连接
- 老年代满了------>Major GC(只清理老年代) /Full GC (常用,堆+方法区全部都清理,System.gc() 手动调用,这是建议不一定执行) (stop the world),标记-清除或者 标记-整理算法
- 还是满了 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)
-
一个线程一个栈
-
方法执行完成,栈帧弹出
-
栈内存小,速度快
-
递归太深,StackOverflowError
public void methodA() {
int a = 10; // a 存在栈里
methodB(); // 调用 methodB → 压入新栈帧
}public void methodB() {
String name = "zhang"; // name 存在栈里
}
3.方法区(Method Area)--类的"信息"
-
类的信息(类名,方法,字段)
-
静态变量(static)
-
常量(final )
-
JDK8之后,方法区==原空间,存在"本地内存"
public class User {
public static int count = 0; // count 存在方法区
public final String type = "user"; // 常量也在这里
}
4.本地方法栈--执行本地(C/C++)方法
-
非java语言实现的方法==本地方法(Native Method)
-
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 执行时候:
- 线程执行到第3行-》程序计数器=3
- 时间片用完,CPU 切换其他的线程
- 一会又轮到这个线程-》从程序计数器读取"3",继续执行

对象的创建
对象的一生
1. new User() → 在 Eden 区分配内存
2. 方法结束 → 局部变量失效,对象可能成垃圾
3. Minor GC → 扫描,活的对象复制到 S0
4. 再次 GC → S0 活的对象复制到 S1
5. 活了15次 → 升级到老年代
6. 老年代也满了 → Major GC(Full GC)
7. 还不够 → OutOfMemoryError!
- 创建
- 类加载检查new 指令-》类加载检查(是否被重复加载,解析,初始化过)
- 内存分配方式(1.指针碰撞,2.空闲列表)
- 初始化零值
- 设置对象头
- 执行init 方法
- 完成
垃圾回收GC
1.垃圾回收是什么
JVM 自动把不再使用的对象找出来,清理掉,腾出空间,"360空间清理大师"
- 让开发者专注业务逻辑,不用管内存释放
- 防止内存泄漏:程序在运行过程,代码错误(忘记释放资源,引用未清理),导致内存空间一直被占用,无法收回,可用内存在持续减少,看起来像天然气泄露了一样。
- 防止内存溢出:OutOfMemory(OOM),程序在申请大块内存/内存耗尽时,系统没有内存了,导致程序异常终止,抛出异常(申请内存>可用内存)
2.怎么判断是垃圾-可达性分析算法
从一些"根对象"出发,访问不到就是"垃圾" -----GC Roots
3.垃圾回收算法
- 标记-清除(Mark -Sweep):标记出活的,清理掉死的-->产生内存碎片(小数据占用了大内存),适合老年代----CMS
- 复制算法(Copying):From--To 标记活的,复制到To 区 ,清除 From区(新生代,浪费一部分空间)
- 标记-整理(Mark-Compent):标记活的,移动整理一起,清理死的(G1,老年代)
- 分代收集(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)、极致低延迟 | ZGC 或 Shenandoah |
| 云原生、容器化部署 | ZGC(停顿短,资源利用率高) |
✅ 现在新项目,直接上 G1 或 ZGC!
5.监控和优化
类加载机制
1.什么是类加载?
JVM把.class文件从磁盘加载到内存,并且生成一个java.lang.Class 对象的过程,称为"类加载"
User user = new User(); // 触发类加载
当你第一次使用这个类的时候:
- JVM 找User.class 文件
- 读取字节码
- 在方法区创建Class 对象,类"信息"
- 执行静态代码块
- 类加载==懒加载,用到才加载
2.类加载的三阶段(加载,链接,初始化)
┌─────────────────┐
│ 加载 │ ← 找到 .class 文件,加载到内存
└────────┬────────┘
↓
┌────────┴────────┐
│ 链接 │ ← 验证、准备、解析
└────────┬────────┘
↓
┌────────┴────────┐
│ 初始化 │ ← 执行 <clinit> 方法(静态变量赋值、静态代码块)
└─────────────────┘
- 加载完成,JVM 中才有Class 对象,但还没开始用
- 链接:验证字节码合法,安全,分配内存,解析
- 类构造器,静态代码
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.什么是双亲委派?(委托父/爷类,最后自己)为什么要有?
当一个子类加载器收到加载请求时,先委托父类加载器去加载,只有父类加载不了,自己才尝试加载。
- 防止核心类被篡改(发现父类里面有,不会覆盖)
- 避免同一个类被"加载器"重复加载
5.打破双亲委派的场景(SPI)
让父类能反过来委托子类加载器去加载类
SPI(Service Provider Interface) 服务接口提供-----JDBC就是典型!
Connection conn = DriverManager.getConnection(url, user, pwd);
-
DriverManager ---->rt.jar 由 根节点 Bootstrap 加载(父类)
-
com.mysql.cj.jdbc.Driver 由 Application 加载(子类)
-
问题产生:导致 根加载器无法加载这个类 (爷爷看不懂孙子的书写什么??)
-
解决方案:线程上下文类加载器(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();
✅ 用于热部署、加密类加载、模块化系统
如果对您有帮助,给个免费点赞❤️❤️❤️❤️!!!!!!!
