资讯动态

JVM整体架构详解:类加载、运行时数据区、执行引擎与GC全链路

发布时间:2026/10/24 7:04:08 来源:尧图企业网站定制
很多写了几年 Java 的开发者如果被问一句“JVM 的整体架构是什么”大概率能说出堆、栈、方法区几个名词但真要对着内存结构把类加载、运行时数据区、执行引擎、垃圾回收这四件事串成一条完整链路很多人会卡壳。我见过不少工作四五年的同行调过 OOM、配过 GC 参数但对 JVM 的认知还是碎片化的——知道某个点说不清整体。JVM 本质上就是一台“虚拟机电脑”类加载子系统相当于操作系统里的程序加载器负责把 .class 文件读进来运行时数据区对应电脑的内存条和硬盘划分出堆、栈、方法区等不同区域执行引擎则是 CPU负责真正跑字节码指令再加上本地方法接口这个“外设接口”四部分协作完成一个 Java 程序的完整生命周期。这篇文章我会按这条主线把 JVM 整体架构掰开揉碎讲清楚每个环节都会配上职责说明和大小参数建议也会穿插一些线上排查时才会遇到的细节——适合刚入门想建立整体认知的同学也适合有经验但对某些环节一直模棱两可的开发者查漏补缺。1. 整体架构与核心设计思路1.1 从“虚拟机”到“软件计算机”的映射关系先理解一个底层逻辑JVM 的设计目标就是屏蔽操作系统差异让 Java 字节码可以在任何平台上跑出相同结果。为了做到这一点它在内部模拟了一台完整的计算机。我列一张对应关系表你看完就明白整个架构的骨架了JVM 组成模块对应计算机部件核心职责类加载子系统操作系统的程序加载器加载 .class 字节码完成校验、准备、解析、初始化运行时数据区内存条 硬盘 CPU 缓存存储类元信息、对象实例、方法栈帧、执行所需数据执行引擎CPU 指令集解释或编译字节码指令执行字节码级别的指令集本地方法接口外设接口/驱动程序调用 C/C 编写的底层方法打通操作系统能力这个映射关系不是凭空设计的而是 JVM 规范硬性要求的。规范定义了“虚拟机应该长什么样”具体实现比如 HotSpot、OpenJ9只要在规范框架内做优化就行。所以学习整体架构时优先理解规范层面的分工再看 HotSpot 的具体实现差异思路会清晰很多。1.2 JVM 生命周期一个 Java 程序的完整旅程理解了四大部分的映射关系之后我用一个最简单的HelloWorld来串联整个 JVM 的工作流程这样你会对整体架构有一个完整的画面感。开发者编写.java源文件通过 javac 编译成.class字节码文件。这一步由 JDK 的编译器完成不涉及 JVM 运行。用户执行java HelloWorld操作系统启动 JVM 进程。JVM 先完成自身初始化创建好运行时数据区的基本结构。类加载子系统上场通过引导类加载器Bootstrap ClassLoader、扩展类加载器Extension ClassLoader和应用类加载器App ClassLoader将HelloWorld.class加载到内存经过加载、验证、准备、解析、初始化五个阶段最终在方法区HotSpot 中是元空间形成Class对象。JVM 找到main方法入口创建第一个栈帧压入虚拟机栈开始解释执行字节码指令。执行引擎每执行一条字节码指令就可能涉及堆中对象的创建、栈帧中的变量操作、方法区的常量读取。程序运行过程中对象的创建和废弃不断发生垃圾收集器在后台自动回收不再被引用的对象避免内存耗尽。所有线程执行完毕JVM 进程退出清理所有内存资源。这个流程里类加载子系统管“进来”运行时数据区管“存放”执行引擎管“执行”GC 管“清理”四个环节咬合在一起构成了 JVM 的完整生命周期。1.3 模块化设计的本质好处JVM 采用模块化架构不只是为了好看三个直接好处值得一提第一职责分离让优化可以局部进行。HotSpot 团队可以在执行引擎里狂堆 JIT即时编译优化技术而不用动类加载子系统GC 从 Serial 演进到 G1、ZGC也不需要重写执行引擎。每块独立演进降低了整个虚拟机的维护复杂度。第二规范抽象让多种语言可以共用运行时。JVM 不关心字节码来自 Java 还是 Kotlin、Groovy、Scala只要符合 JVM 规范的字节码格式就能跑在 JVM 上。这也是为什么 JVM 生态能容纳这么多语言的根本原因。第三平台无关性落到了实处。字节码是统一的中间表示JVM 负责解释执行或者编译执行操作系统差异被虚拟机吸收掉了。Windows 上编译的.class文件拿到 Linux 上能直接运行这就是模块化设计的功劳。2. 类加载子系统JVM 的“入站口”2.1 类加载的五个阶段每一步在干什么类加载子系统负责把编译好的.class文件变成 JVM 可以使用的运行时结构。完整流程分五个阶段其中加载是“读取”验证、准备、解析是“整理”初始化是“真正启用”。加载Loading通过类的全限定名获取二进制字节流可以是本地文件、jar 包、网络来源甚至是动态生成的字节码比如动态代理。加载完成后JVM 把字节流中的静态存储结构转换为方法区中的运行时数据结构并在堆中生成一个java.lang.Class对象作为访问入口。验证Verification这是安全防线。JVM 要确认字节码文件没有恶意或非法的指令。比如检查魔数cafebabe、版本号是否兼容、元数据是否符合规范、字节码指令是否安全。验证失败会抛出VerifyError。生产环境如果想跳过验证提速可以在启动参数里加-Xverify:none但不建议对未知来源的类这么做等于拆掉防火墙。准备Preparation为类的静态变量分配内存并设置默认零值。注意这里设置的默认值是零值不是代码里写的初始值。比如private static int count 100;在准备阶段count的值是 0真正变成 100 要等到初始化阶段。但final static常量例外因为常量在编译期就确定了值准备阶段直接赋值。解析Resolution把常量池中的符号引用替换为直接引用。符号引用就是“用名字代替地址”比如类名com/example/User、方法名sayHello直接引用就是真实的内存地址或偏移量。这个阶段可以发生在初始化之后——JVM 为了性能做了延迟解析优化。初始化Initialization真正执行类构造器clinit方法按代码里写的值给静态变量赋值执行静态代码块。只有当类被主动使用时才会触发初始化比如 new 对象、调用静态方法、反射操作、初始化子类父类会先初始化等。2.2 双亲委派机制为什么它能防止类被重复加载类加载器之间有一个层次关系引导类加载器Bootstrap ClassLoader最顶层用 C 实现加载JAVA_HOME/lib下的核心类rt.jar中的类不继承任何 Java 类。扩展类加载器Extension ClassLoader加载JAVA_HOME/lib/ext目录下的类Java 9 之后被平台类加载器取代。应用类加载器App ClassLoader加载classpath指定路径下的类也就是我们自己写的代码。自定义类加载器继承ClassLoader可以突破默认加载路径限制。双亲委派的核心逻辑一个类加载器收到加载请求时先把这个请求委派给父类加载器去处理父类处理不了才轮到子类自己加载。举个例子假设应用类加载器接到加载java.lang.String的请求它不会自己加载而是往上抛给扩展类加载器扩展类加载器又抛给引导类加载器。引导类加载器发现这是核心类直接加载。这样确保了一个核心类在 JVM 里永远只有一份不会被应用层的同名类篡改。为什么要设计成这样我给出三个实际原因避免重复加载同一个类不能在不同加载器下产生多个版本否则会出现类型混乱instanceof判断都可能失效。安全性防止用户自定义的java.lang.String混进核心类库破坏 JDK 基础行为。一致性保证带有全限定名的类在 JVM 内只有唯一一份。双亲委派模型也有被打破的场景比如 JDBC 的 SPI 机制、热部署场景中的 Web 容器都会通过线程上下文类加载器反向加载。“打破”不是否定这个机制而是在特定场景下允许子加载器先加载再把没有加载过的类委派给父加载器。2.3 面试和线上排查中必须知道的加载器细节类加载这块有两点实操时非常有价值分享两个我踩过的坑。第一NoClassDefFoundError 和 ClassNotFoundException 的区别。前者是类在编译期存在、运行期丢失常见于 jar 包冲突或部署时漏包后者是类压根找不到最常见于 classpath 配置错误。排查时先执行java -verbose:class看类实际从哪个 jar 加载再用jar tf检查目标 jar 里有没有对应类基本能定位 90% 的问题。第二ClassLoader.getResource和getResourceAsStream返回 null 时看看是不是 jar 内路径写错了。类的包路径对应的资源路径com.example对应的目录是com/example很多人在这里漏写斜杠或写错大小写。资源文件的读取建议尽量用Thread.currentThread().getContextClassLoader()而不是this.getClass().getClassLoader()在 Web 应用这种多类加载器环境下前者更不容易踩到加载器视野问题。3. 运行时数据区JVM 的内存地图3.1 线程私有的三个区域程序计数器、虚拟机栈、本地方法栈运行时数据区是 JVM 架构中信息密度最高的部分按照是否线程共享分成两大类。先看线程私有的三个区域。程序计数器Program Counter Register这是占用空间最小、却最容易被忽视的内存区域。每个线程都有自己的程序计数器记录当前线程正在执行的字节码指令地址。字节码解释器就是靠改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、异常处理、线程恢复等基础功能都依赖它。为什么需要它因为线程切换后需要恢复到正确的执行位置没有程序计数器线程的上下文恢复就无法实现。它是唯一一个没有OutOfMemoryError的内存区域。虚拟机栈Java Virtual Machine Stack描述 Java 方法执行的内存模型。每个方法被调用时会创建一个栈帧栈帧里存储了局部变量表、操作数栈、动态链接、方法出口等信息。方法的调用到结束对应一个栈帧的入栈到出栈。局部变量表存放方法参数和方法内部定义的局部变量包括基本数据类型、对象引用和returnAddress。它的容量以变量槽Slot为单位long和double占两个槽其余占一个。操作数栈也叫表达式栈用于存放计算过程的中间结果是执行引擎的工作台。比如执行a b会把 a 和 b 压入操作数栈执行加法指令后把结果弹出。动态链接指向运行时常量池中该方法的引用支持方法调用时的动态解析。这个区域会抛出两种异常线程请求深度超过虚拟机栈允许深度时抛StackOverflowError虚拟机栈支持动态扩展且扩展时无法申请到足够内存时抛OutOfMemoryError。本地方法栈Native Method Stack为虚拟机执行本地方法服务。HotSpot 将它和虚拟机栈合并在一起所以用-Xss参数可以同时调整两者大小。本地方法栈主要服务于native方法比如Object.hashCode()底层通过 C 实现的部分。3.2 线程共享的两个区域堆和方法区堆HeapJVM 内存中最大的一块被所有线程共享。几乎所有的对象实例和数组都在这里分配JIT 优化后可能发生栈上分配这个后面讲执行引擎时会提。它是垃圾收集器管理的主要区域现代 JVM 主流的分代收集理论将堆划分为年轻代Eden 区、两个 Survivor 区和老年代。默认大小建议物理内存 8G 及以上的机器-Xms和-Xmx建议设为物理内存的一半左右两者设置相同值避免堆扩容抖动。比如 8G 内存的服务器设置-Xms4g -Xmx4g是常见配置。堆溢出时抛出OutOfMemoryError: Java heap space。补充一个高频记忆点Eden 区和 Survivor 区的比例默认是 8:1:1可以通过-XX:SurvivorRatio8调整。绝大部分新建对象在 Eden 区分配经过 Minor GC 后存活的对象移到 Survivor 区达到年龄阈值默认 15 岁后晋升到老年代。方法区Method Area存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。它和堆一样是线程共享的对垃圾收集比较敏感。Java 8 及之后HotSpot 用**元空间Metaspace**取代了之前的永久代两者的核心区别是永久代在 JVM 内存中受-XX:MaxPermSize限制元空间使用的是本地内存受操作系统可用内存限制。元空间默认是无限使用的设置-XX:MetaspaceSize可以指定初始值-XX:MaxMetaspaceSize限制上限。如果元空间加载的类过多且没设置上限OOM 时错误信息会是OutOfMemoryError: Metaspace。还有一块容易忽略的是运行时常量池它是方法区的一部分存放类和接口的常量池表包括字面量字符串、final 常量值和符号引用。Java 7 之后字符串常量池从永久代挪到了堆中这也直接影响了对字符串intern()方法的理解。3.3 常用内存参数配置速查表直接给一份实战参数对照表方便你配置 JVM 时查阅参数作用建议值参考备注-Xms初始堆大小物理内存的 1/4 ~ 1/2建议与-Xmx相同减少扩容开销-Xmx最大堆大小物理内存的 1/4 ~ 1/2线上必须显式设置禁止裸奔-Xss线程栈大小512k ~ 1m过大会浪费内存过小容易栈溢出-XX:MetaspaceSize元空间初始阈值256m达到阈值会触发 Full GC 来卸载类-XX:MaxMetaspaceSize元空间上限512m 或按需防止类加载器泄漏拖垮系统-XX:SurvivorRatioEden/Survivor 比例8默认 8:1:1-XX:NewRatio年轻代/老年代比例2默认 1:2即年轻代占堆 1/3-XX:MaxDirectMemorySizeNIO 直接内存上限默认等于堆上限容易被忽略NIO 泄漏时重点排查我特别提醒一个点服务器物理内存充足时堆大小不要贪大。JVM 堆很大意味着 GC 停顿时间可能更长而且操作系统需要留内存给文件缓存、线程栈、直接内存。堆占物理内存 50%~60% 是一个常见的安全区间具体还要结合应用类型调整——定时批处理任务适当调大堆高并发在线服务更注重 GC 停顿时间。3.4 线上 OOM 的典型现场与定位思路内存区域讲完了结合线上实战说说各种 OOM 长什么样。堆内存溢出错误信息包含Java heap space。最常见的诱因是对象泄漏或者创建了超大集合。排查思路先用jmap -dump:formatb,fileheap.hprof pid导出堆快照再用 MATMemory Analyzer分析 Dominator Tree看大对象被谁引用。我处理过一个典型案例一个缓存 Map 只往里面放不清理几十个线程反复写入堆被撑爆。定位之后改成带过期机制的本地缓存问题消失。元空间溢出错误信息包含Metaspace。常见于反复部署的应用类加载器泄漏导致旧的类无法卸载。用jstat -gc pid观察M列Metaspace 使用量是不是持续上升如果一直在涨重点排查是不是自定义类加载器没有释放、框架频繁创建代理类。线程栈溢出错误信息包含StackOverflowError通常是递归没有终止条件或者递归层数过深。用jstack pid看线程栈找到反复出现的相同调用帧基本就是问题所在。-Xss需要加的是递归深度而不是无脑调大。直接内存溢出错误信息通常是OutOfMemoryError但没有明确区域或者出现Direct buffer memory。这个和 NIO 使用不当有关allocateDirect分配的直接内存不受堆大小控制受-XX:MaxDirectMemorySize控制。排查时重点查看是否有没有释放的DirectByteBuffer。4. 执行引擎从字节码到 CPU 指令的翻译官4.1 解释器和 JIT 编译器如何分工执行引擎是 JVM 真正“干活”的地方负责执行字节码指令。它由三部分组成解释器、即时编译器JIT Compiler、垃圾收集器。先说前两者。解释器Interpreter逐条解释字节码指令并执行。好处是启动快不需要等待编译坏处是执行速度慢同一段代码每次执行都要重新解释。JIT 编译器Just-In-Time Compiler把热点代码被频繁执行的代码编译成机器码后续执行直接调用编译后的机器码大幅提升执行效率。HotSpot 这个名字的由来就是因为它会“找出热点代码进行编译优化”。两者协作模式JVM 启动时先用解释器快速跑起来同时监控代码执行频率。当某段代码达到热点阈值默认方法调用计数器 10000 次可通过-XX:CompileThreshold调整JIT 介入将其编译成机器码。这个策略兼顾了启动速度和长期运行后的性能称为混合模式Mixed Mode。如果想看当前运行在哪种模式用java -version输出里会显示mixed mode。4.2 分层编译和逃逸分析为什么要搞这么复杂现代 JVM 的 JIT 编译器有 C1Client Compiler和 C2Server Compiler两个级别。C1 编译速度快、优化力度轻C2 编译速度慢、优化力度强。为了平衡启动速度和峰值性能HotSpot 引入了分层编译Tiered Compilation编译层级说明使用场景0解释执行程序启动阶段1C1 编译无 profiling简单方法直接编译2C1 编译有限 profiling收集方法调用和分支信息3C1 编译完整 profiling为 C2 编译做准备4C2 编译极其热点的代码最终优化默认开启分层编译用-XX:TieredCompilation显式开启-XX:-TieredCompilation关闭。**逃逸分析Escape Analysis**是 JIT 优化中非常关键的一项技术它分析对象的作用域如果对象只在方法内部创建和使用不会逃逸到方法外部JVM 会把这个对象的所有字段拆散直接在栈上分配不会有 GC 压力。如果对象没有发生任何逃逸可能连栈上分配都省了直接做标量替换用局部变量代替对象字段。这也是为什么你写的一段高性能代码在 JVM 层可能完全不是你以为的执行方式。长时间运行的服务性能好不只是 JIT 编译了机器码而是它不断收集运行时信息后做了深度的优化决策。这些优化是解释器模式下完全无法实现的。4.3 热点检测与常见 JIT 优化一览热点检测有基于采样的和基于计数器的两种方式。HotSpot 用的是计数器方式为每个方法维护调用计数器。方法执行一次计数器加 1回收到一定阈值就交给 JIT 处理。除了方法调用计数器还有回边计数器Back Edge Counter统计方法内循环的执行次数循环达到阈值也会触发编译。常见的 JIT 优化手段包括方法内联Method Inlining把被调用方法的代码直接粘贴到调用方方法中省去方法调用的开销。这是最重要的一项优化几乎所有热点方法都会内联。逃逸分析Escape Analysis如上文所述决定对象是否可能在栈上分配或标量替换。锁消除Lock Elimination如果 JVM 判定对象不会逃逸就会去掉synchronized锁省去不必要的同步开销。循环展开Loop Unrolling把循环体复制多份减少循环判断和跳转的次数。公共子表达式消除Common Subexpression Elimination重复计算的表达式只算一次结果复用。为了看到 JIT 到底做了哪些优化可以加参数打印编译日志-XX:PrintCompilation -XX:UnlockDiagnosticVMOptions -XX:PrintInlining。输出会显示哪些方法被编译了、哪些方法内联失败了及原因。4.4 GC 与执行引擎的协同关系执行引擎中的第三个成员——垃圾收集器负责管理堆内存。GC 关注的核心问题是哪些对象需要回收、什么时候回收、如何回收。对象是否需要回收主流判定方式是可达性分析Reachability Analysis。从 GC Roots比如虚拟机栈中引用的对象、静态变量引用的对象、常量引用的对象、JNI 引用的对象出发遍历对象引用图没有到达路径的对象被认为是可回收的。引用计数法因为循环引用问题很难解决主流 JVM 已经不采用。收集算法上标记-复制用于年轻代把存活对象复制到 Survivor 区标记-整理用于老年代把存活对象向一端移动解决内存碎片问题。不同场景选用不同垃圾收集器我的参考表格垃圾收集器适用场景特点JDK 默认情况Serial单核环境、客户端简单可靠停顿时间长JDK 8 客户端默认Parallel Scavenge Parallel Old多核、吞吐量优先吞吐量优先停顿可控JDK 8 服务端默认CMS低延迟优先并发标记清除碎片化问题JDK 9 起废弃G1大堆、兼顾延迟和吞吐分 region 管理可预测停顿JDK 9 默认ZGC超大堆、极低延迟着色指针停顿时间与堆大小无关JDK 15 支持关于 GC 参数和调优我在第 5 部分会展开讲这里先把“GC 是执行引擎的一部分”这个坐标立住。5. 常用 JVM 监控工具与调优实战思路5.1 先定位问题再动参数调优 JVM 最容易犯的错误是一上来就改参数。正确顺序一定是先监控、再看日志、然后分析快照、最后才调参数。否则你改了参数也不知道改对了没有甚至可能把原本正常的问题掩盖成新问题。我的标准排查流程jps确认目标 Java 进程 ID。jstat -gc pid 1000观察 GC 频率和耗时连续采样 1-2 分钟。如果 GC 频繁且 YGC 后存活对象异常多考虑对象晋升问题如果 Full GC 频繁优先确认是不是老年代空间不足。jmap -heap pid看堆各区域使用情况必要时jmap -dump导出堆快照分析大对象。jstack pid看线程状态排查死锁、线程阻塞、长时间运行的任务。确认根因后再有针对性地调整堆大小、代际比例、GC 收集器。5.2 一个典型的 Full GC 调优案例某模拟项目 X 的线上服务高峰期出现频繁的 Full GC接口响应时间飙升到秒级。当时的排查过程第一步jstat -gc pid 1000观察输出发现 Full GC 每 2 分钟一次每次耗时约 2 秒老年代使用率在 Full GC 后仍然高达 85% 以上。第二步导出堆快照jmap -dump:formatb,fileheap.hprof pid用 MAT 分析。Dominator Tree 里看到一个静态 Map 占用了老年代接近 3GB 的空间Map 里的 key 是时间戳value 是订单详情——典型的缓存没有过期机制的泄漏。第三步代码层面优化引入带过期时间的本地缓存控制最大条目数同时把启动参数从-Xms4g -Xmx4g调整为-Xms6g -Xmx6g。第四步上线后观察Full GC 频率从每 2 分钟一次降低到高峰期间基本不触发接口响应时间回到正常水平。这个案例我想表达的点是参数调整只是兜底手段根因往往在代码层面。JVM 调优不是调参数大赛而是“用工具定位问题、用代码修复问题、用参数做兜底优化”的组合拳。5.3 常用参数分类记忆为了便于记忆我把 JVM 参数按用途分类堆内存类-Xms、-Xmx、-Xmn年轻代大小、-XX:NewRatio、-XX:SurvivorRatio。元空间类-XX:MetaspaceSize、-XX:MaxMetaspaceSize。栈内存类-Xss。GC 类-XX:UseG1GC、-XX:UseParallelGC、-XX:MaxGCPauseMillis目标停顿时间、-XX:G1HeapRegionSizeG1 区域大小。日志类-Xlog:gc*JDK 9 统一日志、-XX:PrintGCDetailsJDK 8 及以前、-XX:PrintGCDateStamps、-XX:PrintHeapAtGC。OOM 快照类-XX:HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath/path/to/heap.hprof。这两个参数强烈建议生产环境都加上OOM 时自动留下现场。JIT 类-XX:CompileThreshold、-XX:TieredCompilation、-XX:PrintCompilation。6. 常见问题排查速查与实践心得6.1 问题与排查对照表我从从业经验里整理了几个高频问题做成速查表方便你遇到类似情况时快速定位现象可能原因排查工具处理建议OutOfMemoryError: Java heap space堆内存不足、对象泄漏jmap MAT导出快照找大对象修复泄漏或调大堆OutOfMemoryError: Metaspace元空间不足、类加载器泄漏jstat 观察 M 列设置 MaxMetaspaceSize排查类加载器释放逻辑StackOverflowError递归无出口、栈深度过深jstack修复递归逻辑必要时微调 -Xss频繁 Full GC老年代空间不足、大对象过多jstat -gcutil分析晋升情况排查大对象缓存CPU 飙高死循环、GC 线程占满、JIT 编译频繁top jstack抓线程栈找到热点线程或检查 GC 日志接口响应变慢GC 停顿变长、锁竞争、线程阻塞jstat jstack分场景定位 GC 耗时或线程争用6.2 实操心得少走弯路的个人经验最后分享几个我实际踩过坑才明白的道理。第一个是重启治标不治本JVM 问题要抓到第一现场。OOM 不是重启就完事的重启后堆快照丢了下次可能换个时间点继续爆。生产环境务必开启-XX:HeapDumpOnOutOfMemoryError把堆快照留下来等你有空慢慢分析。这是所有线上 Java 服务的最低配置没有例外。第二个是元空间泄漏比堆泄漏更难发现因为它的增长是缓慢的。堆泄漏通常几天甚至几小时就能撑爆元空间泄漏可能要跑几周才出现MetaspaceOOM。建议监控系统里专门加一项 Metaspace 使用量的趋势告警超过基线持续上升就要警惕。第三个是不要过早对 JIT 做微观优化。我看到有些同学为了“优化”在代码里写一堆奇怪的手动展开循环、避免多态调用实际上 C2 编译器比你更懂这些。先把代码写得清晰易懂跑出性能数据再针对 JIT 日志发现的真实热点优化。这个顺序反了一定会浪费大量时间。第四个是多学看 GC 日志这是最直接的运行时体检报告。GC 日志格式虽然一开始看着门槛高但花两个小时把字段都搞懂后面排查问题的效率提升不止一个量级。JDK 9 之后用-Xlog:gc*统一管理输出可读性比之前的老格式好很多。JVM 整体架构这张图我希望你能在心里默画出来类加载子系统在左侧负责装入类文件运行时数据区在中间按线程私有和线程共享铺开内存地图执行引擎在右侧解释和编译字节码GC 像隐形管家一样吞吐着堆内存里的对象。这个架构图一旦建立起来再看 JVM 相关的问题、参数、工具就不再是零散的知识点而是一个可以定位坐标的完整地图。我个人现在看任何 JVM 相关的源码、文章和问题都会先问自己一句这个内容落在架构图的哪个模块想清楚坐标再往深处走效率会高很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑