【Java SE】JVM

JVM

JVM内存区域划分

JVM 从操作系统申请一大块内存后,会根据功能划分为几个不同的运行时数据区。这些区域各司其职,有的线程私有,有的线程共享。


线程共享区域
元数据区 (Metaspace) -XX:PermSize -XX:MaxPermSize
常量池
类元信息 (Class)
Cache
JIT编译产物
堆区 (Heap) -Xms -Xmx
新生代 (Young Generation) -Xmn
Eden
S0 (Survivor)
S1 (Survivor)
老年代 (Old Generation)
线程私有区域
虚拟机栈 (JVM Stacks) -Xss
栈帧-3 (方法C)

局部变量表

操作栈

动态连接

方法返回地址
栈帧-2 (方法B)

局部变量表

操作栈

动态连接

方法返回地址
栈帧-1 (方法A)

局部变量表

操作栈

动态连接

方法返回地址
程序计数器 (Program Counter Register)
本地方法栈 (Native Method Stacks)

这张图采用自顶向下的布局,分别展示了线程私有的程序计数器、本地方法栈和虚拟机栈(含 -Xss 参数及三个栈帧内部结构),线程共享的堆区(包含新生代 Eden/S0/S1、老年代及 -Xms/-Xmx/-Xmn 参数)以及元数据区(包含常量池、类元信息、Cache、JIT 编译产物及 PermSize 相关参数)

程序计数器(Program Counter Register)

  • 作用:记录当前线程正在执行的字节码指令地址。当线程切换后,能恢复到正确的执行位置。
  • 特点
    • 内存占用非常小。
    • 线程私有(每个线程都有自己的 PC)。
    • 不会发生 OutOfMemoryError

Java 虚拟机栈(JVM Stacks)

  • 作用 :描述 Java 方法执行的线程内存模型。每个方法执行时都会创建一个栈帧
  • 栈帧包含
    • 局部变量表:存储方法参数和方法内的局部变量。基本类型直接存储值,引用类型存储对象的地址。
    • 操作数栈:用于存放计算过程中的中间结果,字节码指令会频繁压栈和出栈。
    • 动态链接:指向运行时常量池中该方法的符号引用,用于支持多态。
    • 方法返回地址:方法正常或异常退出后,返回到调用方的位置。
  • 特点
    • 线程私有。
    • 栈深度超过限制 → StackOverflowError(如无限递归)。
    • 动态扩展失败 → OutOfMemoryError

🔍 注意:局部变量是基本类型或对象引用时,存储在栈上;但对象本身始终在堆上。

本地方法栈(Native Method Stacks)

  • 作用 :为虚拟机调用的 native 方法(如用 C/C++ 实现的底层方法)服务。
  • 特点:与虚拟机栈类似,只是服务的对象不同。HotSpot 虚拟机中,本地方法栈和虚拟机栈合二为一。

Java 堆(Heap)

  • 作用所有线程共享 的内存区域,用于存储 new 出来的对象数组
  • 分代设计 (为了优化垃圾回收):
    • 新生代(Young Generation):又分为 Eden 区、S0(Survivor From)、S1(Survivor To)。大多数对象在这里诞生并很快消亡。
    • 老年代(Old Generation / Tenured):存放生命周期较长的对象,从新生代多次回收后晋升而来。
  • 特点
    • 垃圾回收的主要区域,也称为 "GC 堆"。
    • 可通过 -Xms-Xmx 控制堆大小。
    • 内存不足时抛出 OutOfMemoryError

📌 new出来的对象的存储位置小总结:⭐

  • 局部变量(包括对象引用) → 栈上的局部变量表
  • 成员变量(实例变量) → 堆上的对象内部
  • 静态成员变量 → 元数据区(见下文)

元数据区(Metaspace)/ 方法区(Method Area)

  • 作用 :存储已被虚拟机加载的类元信息静态变量常量池 (如 String 常量池)、即时编译器编译后的代码等。
  • 元信息包括
    • 类的全限定名、访问修饰符(public/private 等)。
    • 父类、实现的接口列表。
    • 字段信息(字段名、类型、修饰符)。
    • 方法信息(方法名、参数类型、返回值、修饰符等)。
  • Java 8 前后的变化
    • Java 7 及以前:方法区位于永久代(PermGen),有默认大小上限,容易 OOM。
    • Java 8 开始 :使用元数据区(Metaspace) ,不再使用虚拟机内存,而是使用本地内存(Native Memory),默认大小仅受物理内存限制。
  • 特点
    • 线程共享。
    • 回收目标主要是常量池和类的卸载(条件比较苛刻)。
    • 内存不足 → OutOfMemoryError

反射的核心依据:反射之所以能动态获取类的字段、方法等信息,正是因为元数据区保存了完整的类结构元数据。

类加载机制

【Java SE】类加载机制

垃圾回收机制(GC)

手动释放内存太麻烦、太容易出错(如忘记释放、重复释放、野指针等)。Java 引入自动垃圾回收,由 JVM 自动识别不再使用的内存并释放。

GC 工作两大步骤

  1. 找到垃圾

不再使用的对象
2. 释放垃圾

回收内存

如何找到垃圾 ------ 对象存活判断

引用计数法(Reference Counting)

  • 原理:每个对象附带一个整数计数器,每多一个引用就 +1,引用失效就 -1。计数器为 0 时即为垃圾。
  • 采用者:Python、PHP 等。
  • 缺点(Java 未采用)

缺点2_循环引用
相互引用
引用断开
引用断开
对象 A
对象 B
外部引用 a = null
外部引用 b = null
计数仍为 1
计数仍为 1
无法回收的垃圾
缺点1_额外内存
每个对象额外 4 字节存储计数器
内存开销大

循环引用示例(两个对象互相引用,外部引用断开后依然无法回收):

java 复制代码
class Test {
    Test t = null;
}
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
a = null;
b = null;  // 此时 a、b 对象引用计数均为 1,但已无法使用

可达性分析(Reachability Analysis)⭐

  • 原理 :从一组 GC Roots 出发,沿着引用链遍历所有对象。能被遍历到的对象标记为 可达 ,未被标记的即为 垃圾

堆中对象
GC Roots 起点
栈中局部变量引用
静态变量引用
常量池引用
JNI 引用
对象 A

可达
对象 B

可达
对象 C

可达
垃圾对象

不可达

  • 过程

    1. 确定当前的 GC Roots(随时可获取)。
    2. 从 GC Roots 开始遍历所有引用链。
    3. 每访问到一个对象,标记为 可达
    4. 遍历结束后,未被标记的对象就是 不可达,即垃圾。
  • 触发频率 :JVM 会周期性地执行可达性分析(通常在内存分配达到一定阈值时触发,或者在空闲时执行)。

如何释放垃圾

标记-清除(Mark-Sweep)

  • 步骤:标记所有垃圾对象 → 直接清除,回收内存。
  • 优点:简单、无需移动对象。
  • 缺点 :产生大量内存碎片,导致后续分配大对象时失败。

清除垃圾
回收后
对象1 存活
空闲碎片
对象3 存活
空闲碎片
对象5 存活
回收前
对象1 存活
对象2 垃圾
对象3 存活
对象4 垃圾
对象5 存活

标记-复制(Mark-Copy)

  • 步骤 :将内存分为两块,每次只使用其中一块。GC 时将存活对象复制到另一块,然后整体清空原块。
  • 优点:无碎片,分配速度快。
  • 缺点
    • 内存利用率低(最多只用一半)。
    • 存活对象多时,复制成本高。
  1. 复制存活对象到 To 区
  2. 清空 From 区,交换角色
    回收后状态
    From 区

已清空,变为空闲
To 区

存放复制过来的存活对象
回收前状态
From 区

使用中,含存活+垃圾
To 区

空闲

标记-整理(Mark-Compact)

  • 步骤 :标记存活对象 → 将所有存活对象向一端移动(类似顺序表搬运) → 清理边界以外的内存。
  • 优点:解决内存碎片,且内存利用率高。
  • 缺点:移动对象有开销(尤其在对象数量多时)。

存活对象向一端移动
整理后
存活
存活
存活
连续可用空间
整理前
存活
垃圾
存活
垃圾
存活

分代回收(Generational Collection) ⭐

Java 将标记-复制标记-整理 相结合,根据对象的年龄(熬过 GC 的次数)采用不同策略,扬长避短。

对象的年龄与区域划分

  • 年龄:每经历一轮 GC 仍存活,年龄 +1。
  • 区域划分
    • 新生代(Young Generation) :年龄小的对象。
      • 伊甸区(Eden):新对象诞生地(占新生代 80%)。
      • 幸存区(Survivor):S0 和 S1,各占 10%,用于复制回收。
    • 老年代(Old Generation):年龄大的对象,预期长期存活。

Minor GC

存活对象复制
角色互换

下次复制到对端
年龄达到阈值

晋升老年代
Major/Full GC
堆内存
新生代
老年代
伊甸区 80%
幸存区 S0 10%
幸存区 S1 10%
标记-整理

或标记-清除

对象晋升流程

Minor GC 存活

年龄=1
多次 GC 仍存活

年龄递增
达到阈值

年龄 ≥ 15
新对象
伊甸区
幸存区 From
幸存区 To
老年代

为什么这样设计?

  • 绝大多数新对象 朝生夕死 ,伊甸区回收时只需复制极少量存活对象到幸存区,标记-复制开销极低。
  • 幸存区 S0/S1 反复复制,让真正长期存活的对象 熬成婆,最终晋升老年代。
  • 老年代对象生命周期长,使用标记-整理(或标记-清除)避免浪费一半空间,且回收频率低,整理成本可接受。

分代回收的优点

  • 扬长避短
    • 新生代:复制算法 → 无碎片 + 复制量小(大部分对象已死)。
    • 老年代:整理/清除算法 → 高空间利用率 + 移动成本相对低频。
  • 整体效率高,是 Java 主流 GC 策略(G1、ZGC 等也基于分代或分区思想)。

三者之间的联系与总结

核心内容 主要关注点 常见问题
内存区域划分 数据存放在哪里(栈、堆、元数据区......) 栈溢出、堆溢出、元数据区溢出
类加载机制 字节码如何被加载进元数据区,并生成 Class 对象 NoClassDefFoundError、ClassCastException
垃圾回收 如何自动清理不再使用的堆内存 内存泄漏、频繁 Full GC、停顿时间过长

三者协同工作

  • 类加载器将字节码装载到元数据区,同时生成 Class 对象放在堆中。
  • 程序运行时,方法调用在栈帧中推进,局部变量指向堆中的对象。
  • 当对象不再被任何栈帧引用(不可达 GC Roots)时,垃圾回收器在合适的时机回收其堆内存。
相关推荐
小短腿的代码世界2 小时前
Qt进程间通信全体系深度解析:从QSharedMemory到本地Socket的七层武器
开发语言·qt
小陶来咯2 小时前
小智接入懒人说书MCP
java·开发语言
m0_470857642 小时前
PHP怎么实现工厂模式_Factory模式编写指南【指南】
jvm·数据库·python
Dicky-_-zhang3 小时前
日志管理实战:ELK与Loki对比选型与落地实践
java·jvm
nJI74egg13 小时前
JavaEE初阶---《JUC 并发编程完全指南:组件用法、原理剖析与面试应答》
java·面试·java-ee
刮风那天3 小时前
Android AMS创建进程不用Binder而用Socket?
android·java·binder
小王C语言3 小时前
【线程概念与控制】:线程封装
jvm·c++·算法
程序员老邢3 小时前
【技术底稿 37】Spring Boot 3.x 自动装配 “死锁” 排查:3 个注解实现条件化装配与 Mock 兜底
java·spring boot·后端·自动装配·rag·技术底稿
学习,学习,在学习3 小时前
Qt工控仪器程序框架设计详解(工控多仪器控制版本)
开发语言·c++·qt