尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

JVM 核心原理精讲:内存划分、双亲委派与垃圾回收一文搞懂

JVM 核心原理精讲:内存划分、双亲委派与垃圾回收一文搞懂 JVM 核心原理入门内存区域、类加载与垃圾回收引言JVMJava 虚拟机是 Java 能够一次编写、到处运行的基石也是 Java 面试中绕不开的高频考点。本文系统梳理 JVM 的内存区域划分、类加载的双亲委派机制以及垃圾回收的核心算法与主流回收器帮助你建立起完整且扎实的 JVM 知识框架。目录为什么需要引入 JVMJVM 内存区域划分程序计数器 / 栈 / 堆 / 元数据区JVM 类加载机制双亲委派JVM 的垃圾回收GC引用计数 / 可达性分析 / 分代 / 回收器Java 虚拟机JVM是 Java 平台的核心。它们三者之间的包含关系为JDK 包含 JREJRE 包含 JVM。如果你只运行 Java 程序安装 JRE 即可如果还要开发才需要完整的 JDK。JDKJava开发工具包JREJava运行时环境JVMJava虚拟机开发工具如: javac编译器核心类库如: rt.jar本文内容偏八股型主要考察概念理解与记忆。0. 为什么需要引入 JVM引入 JVM 的核心目的是实现跨平台Java 程序编译后生成字节码由不同操作系统上的 JVM 各自解释执行从而屏蔽底层操作系统的差异达到一次编写、到处运行。1. JVM 内存区域划分Java 程序运行过程中使用的内存本质上都是 JVM 管理的内存。JVM 启动时向操作系统申请一大块内存随后由 JVM 自行对这些内存进行区域划分与分类管理。线程私有区域 (Thread Private)本地方法栈(Native Method Stacks)执行 Native 方法程序计数器(Program Counter Register)记录当前线程执行的字节码行号虚拟机栈(JVM Stacks)─────────────────────栈帧-1 (方法A): 局部变量表 | 操作数栈 | 动态链接 | 返回地址栈帧-2 (方法B): 局部变量表 | 操作数栈 | 动态链接 | 返回地址栈帧-3 (方法C): 局部变量表 | 操作数栈 | 动态链接 | 返回地址线程共享区域 (Thread Shared)堆区 (Heap)─────────────────────Young区: [Eden] [S0] [S1]Old区: [Old 区]元数据区 (Metaspace)─────────────────────1. [Klass] 类元信息类的结构信息类名、父类、接口、字段等2. [方法元信息]方法的字节码、异常表、访问标志等3. [运行时常量池]编译期生成的字面量、符号引用类/字段/方法4. [注解/其他]类、方法、字段上的注解元数据程序计数器程序计数器仅保存一个数字指向下一条将要执行的 Java 字节码指令的地址这块内存由 JVM 通过软件方式维护。栈、堆与元数据区内存区域可进一步细分为虚拟机栈供 Java 程序使用的栈维护方法之间的调用关系。方法调用时会创建一个栈帧其中保存了调用该方法时传入的实参、方法内部的局部变量、方法结束后返回上层方法的地址以及返回值。本地方法栈供 C/C 代码使用。JVM 底层多由 C 实现Java 代码向下调用的底层逻辑最终会进入本地方法栈。堆内存占用最大存放我们new出来的对象及其普通成员变量。元数据区旧版本称为方法区存放类对象以及被static修饰的属性类变量。这些区域都可能发生内存溢出栈溢出通常是栈帧过多如递归层级太深或创建了过大的局部变量。堆溢出通常是new得太多如向集合类无限添加元素需要定位是哪个对象被过度创建。需要特别注意的是堆和元数据区在整个 JVM 中是唯一的线程共享而程序计数器、虚拟机栈、本地方法栈都是一个线程一份线程私有。2. JVM 类加载机制类加载是指把.class文件读取到内存中并构建出对应类对象的过程。2.1 类加载的流程加载Loading根据代码中书写的全限定类名在文件系统中找到对应的.class文件。验证Verification检查读到的二进制内容是否为合法的 Class 格式确认它确实是一个.class文件。二进制文件通常会把开头四个字节固定为特定的魔数由规范制定者约定。注意版本兼容用 Java 17 编译的.class文件无法在 Java 8 上运行但 Java 8 编译出来的可以在 Java 17 上运行向上兼容。准备Preparation为将要创建的类对象分配内存空间在元数据区中申请并将未初始化的字段统一置为 0。字符串常量处理把当前类用到的字符串常量放入运行时常量池此时常量在池中就有了真实的存放地址。解析与初始化完成符号引用到直接引用的转换并执行类的初始化逻辑。类加载整体是**“懒加载”Lazy Loading**逻辑用到该类时才加载调用该类的静态方法或访问静态成员时触发加载加载子类时会优先触发其父类的加载。2.2 双亲委派模型严格来说称为双亲委派并不十分准确更准确的描述应是单条父加载器链上的委派有时也被戏称为单亲委派。双亲委派出现在类加载的第一步加载阶段其目的是找到对应的.class文件。这涉及到一个关键模块——类加载器ClassLoader。BootstrapClassLoader启动类加载器加载 Java 标准库中的类如java.lang.*。ExtensionClassLoader扩展类加载器父为 1加载 Java 扩展库中的类如javax.*扩展包。ApplicationClassLoader应用程序类加载器父为 2加载第三方 Jar 包以及当前项目的类。类加载器层级结构1) BootstrapClassLoader (启动类加载器) (负责加载 Java 标准库的类如 java.lang.*) ↓ 父加载器 2) ExtensionClassLoader (扩展类加载器) (负责加载 Java 拓展库的类如 javax.* 扩展包) ↓ 父加载器 3) ApplicationClassLoader (应用程序类加载器) (负责加载 第三方 Jar 包 和 你当前项目的 Class 文件)双亲委派加载流程[3) ApplicationClassLoader] 收到全限定类名的加载请求 ⬇ 向上委托 [2) ExtensionClassLoader] 收到请求 ⬇ 向上委托 [1) BootstrapClassLoader] 收到请求 [1] 扫描自己负责的 Java 标准库目录寻找该类 ═══ 找到了? [是] 结束加载返回 Class 对象。 ═══ 找不到? 将控制权交还给 [2]。 [2] 扫描自己负责的 Java 扩展库目录寻找该类 ═══ 找到了? [是] 结束加载返回 Class 对象。 ═══ 找不到? 将控制权交还给 [3]。 [3] 扫描自己负责的项目 Classpath 路径第三方库及当前项目代码寻找该类 ═══ 找到了? [是] 结束加载返回 Class 对象。 ═══ 找不到? 抛出异常java.lang.ClassNotFoundException之所以这样设计是因为 Java 的类加载器之间没有子类标识——父加载器并不认识自己的子加载器因此委派只能自下而上而非自上而下。其本质是一段递归调用的逻辑。下面是 JDK 源码中简化后的核心逻辑protectedClass?loadClass(Stringname,booleanresolve){// 1. 检查缓存已加载则直接返回// 2. 向上委托先交由父加载器尝试加载Class?cparent.loadClass(name,resolve);// 3. 父加载器加载失败时自己再去查找并加载if(cnull){cfindClass(name);// 这才是最真正的加载动作}returnc;}整个调用过程类似a → b → c → b → a的回溯本质上是一段DFS深度优先搜索逻辑。3. JVM 的垃圾回收GC3.1 为什么要 GCGC 主要解决的是内存泄漏问题。在 C 语言中局部变量随栈帧自动释放全局变量不主动释放进程结束时由系统回收通过malloc申请的内存属于堆区需要程序员手动free释放。如果只申请不释放最终会出现没有空闲内存可分配的情况导致内存申请失败引发 Bug。C 引入了智能指针机制在一定程度上缓解了内存泄漏问题。而 JVM 的做法是专门指派一些线程周期性地扫描new出来的对象自动判断哪些对象已经不再使用并自行将其释放。这种方式自然会带来更大的运行时开销。3.2 回收哪些区域程序计数器不需要回收它有自己的固定位置。栈不需要回收随方法调用/返回自动管理。堆是 GC 的主要战场。元数据区类对象一般只加载、很少卸载。堆上存放的主要是对象。需要强调GC 回收以对象为最小单位具有原子性只会整体回收一个对象而不会只回收对象的一部分。3.3 如何判断对象是否为垃圾GC 从引用入手来判断对象是否可回收。例如一个对象被引用变量 s 指向而 s 又关联着 a、b、c 三个引用。3.3.1 引用计数法Java 未采用Python、PHP 使用每个对象内部持有一个隐式的计数器成员每当发生一次引用赋值该计数就加 1引用失效时减 1。当计数降为 0即可释放。该方法有两个明显缺点额外消耗内存空间每个对象都要额外保存一个计数。若对象本身很小如仅一个int字段引用计数的开销可能占到对象总大小的很大比例。无法解决循环引用问题。classTest{Testt;}TestanewTest();// a 的引用计数 t1 1TestbnewTest();// b 的引用计数 t2 1a.tb;// t2b.ta;// t1anull;// t1--bnull;// t2--此时 a、b 本应被回收但由于彼此通过t字段互相引用t1、t2的计数都仍为 1导致无法被回收。3.3.2 可达性分析Java 采用在一段 Java 代码中一系列对象之间通过引用形成关联整体构成类似树形的结构。classTest{AanewA();BbnewB();CcnewC();}TesttnewTest();classA{DdnewD();EenewE();}classB{FfnewF();GgnewG();}tabcdefg可达性分析从**根节点GC Roots**出发尝试遍历整棵对象引用树遍历过程中经过的对象都被标记为可达其余未被标记的对象即为不可达可以作为垃圾回收。可作为GC Roots的对象包括栈上的局部变量常量池引用所指向的对象所有引用类型的静态成员。一轮 GC 就是把所有 GC Roots 尽可能遍历完从而识别出哪些对象是垃圾。3.3.3 识别垃圾后如何释放标记-清除Mark-Sweep直接把标记出的垃圾释放掉但会产生内存碎片。其弊端在于总内存看似充足却可能因碎片过多而申请不到连续的大块内存。复制算法Copying用以解决内存碎片问题。把内存一分为二每次只使用其中一半如 A、B 两部分A 中有对象 1~5其中 1、3 是垃圾就把存活的 2、4、5 复制到 B然后清空 A。缺点是空间利用率低且存活对象多时复制开销大。标记-整理Mark-Compact类似顺序表删除中间元素将存活对象向一端靠拢、压缩从而减少碎片。JVM 最终将以上三种思路合三为一构成了一套更复杂的综合方案。3.3.4 分代回收Generational Collection分代回收根据对象的生存特点采取不同的回收策略。其核心经验规律是对象的年龄越大继续存活下去的概率也越大。JVM 把整个堆分为两大部分——新生代Young Generation和老年代Old Generation针对老年代的 GC 称为Major GC / Old GC开销大、频率低针对新生代的 GC 称为Minor GC开销小、频率高因为 Eden 区很快被填满就会触发两者合在一起称为Full GC。新生代进一步细分为伊甸区Eden占 8、幸存区Survivor占 1、幸存区Survivor占 1。分代回收的流程如下新new出来的对象先放入伊甸区Eden第一轮 GC 会淘汰掉绝大部分对象存活的进入幸存区熬过一轮 GC 的对象还会继续接受筛选未淘汰的通过复制算法转移到另一个幸存区因该区域较小复制带来的内存消耗也少每熬过一轮 GC对象年龄1年龄达到一定阈值后被拷贝到老年代进入老年代后GC 频率显著降低主要通过标记-整理算法来回收。此外如果一个对象特别大会直接进入老年代。以上只是一个简化版模型实际实现更为复杂。JVM 提供了多种垃圾回收器CMS尽可能多线程并发标记尽量减少对业务线程的影响G1能处理内存空间特别大的场景把堆划分为多个 Region每次只回收其中一部分ZGC尽可能让 GC 对业务逻辑的停顿时间极短。3.4 各垃圾回收器对比面试常考回收器核心算法 / 特点适用场景STW停顿特点Serial单线程回收新生代复制 老年代标记-整理客户端程序、单核、小堆全程 STW停顿长但实现简单ParallelParNew / Parallel Old多线程并行回收追求吞吐多核、后台计算吞吐优先仍 STW关注吞吐量而非延迟CMSConcurrent Mark Sweep与业务线程并发标记清除响应时间敏感的老年代仅初始标记 / 重新标记短暂停顿易产生内存碎片G1Garbage First把堆切成一个个 Region可预测停顿大堆数 GB ~ 数十 GB可设停顿目标MaxGCPauseMillis做 Mixed GCZGC着色指针 读屏障几乎全阶段并发超大堆、超低延迟停顿 10ms且几乎不随堆增大而增长记忆顺序Serial单线程→ Parallel多线程吞吐→ CMS并发低延迟但碎片→ G1可预测停顿主流→ ZGC极致低延迟。新生代用 Minor GC高频、复制算法老年代用 Major/Full GC低频、标记-整理这是分代回收的默认节奏。小结本文围绕 JVM 的三大核心主题展开内存区域划分JVM 从操作系统申请大块内存并自行管理分为线程共享的堆、元数据区以及线程私有的程序计数器、虚拟机栈、本地方法栈其中堆最容易发生溢出也是 GC 的主战场。类加载机制类加载包含加载、验证、准备、解析、初始化等阶段并遵循懒加载原则双亲委派模型通过逐级向上委派、再向下回退的递归DFS逻辑查找并加载.class文件保障了类加载的安全与有序。垃圾回收GCJava 采用可达性分析而非引用计数判断垃圾结合标记-清除、复制、标记-整理三种思路并进一步演化为分代回收新生代 Minor GC 老年代 Major/Full GCSerial、Parallel、CMS、G1、ZGC 等回收器则在吞吐量与停顿时间之间不断权衡演进。掌握这些基础概念既是理解 Java 程序运行机制的关键也是应对技术面试的重要基石。
返回列表