一、JVM 内存模型1.1 JVM 整体架构JVM 主要由以下几个核心部分组成类加载子系统负责将.class文件加载到内存中执行校验、准备、解析、初始化等步骤运行时数据区堆存储对象实例线程共享方法区存储类信息、常量、静态变量栈每个线程私有存储局部变量、方法栈帧程序计数器、本地方法栈等执行引擎解释执行字节码通过 JIT 将热点代码编译为机器码本地方法接口调用底层 C/C 本地方法1.2 运行时数据区详解线程私有的区域程序计数器当前线程所执行的字节码的行号指示器唯一一个不会抛出OutOfMemoryError的区域Java 虚拟机栈描述 Java 方法执行的内存模型每个方法执行都会创建一个栈帧包含局部变量表操作数栈动态链接方法出口等可能抛出的异常StackOverflowError线程请求的栈深度大于虚拟机允许的深度OutOfMemoryError栈可以动态扩展但无法申请到足够内存本地方法栈为 Native 方法服务与虚拟机栈类似线程共享的区域堆几乎所有的对象实例和数组都在这里分配内存垃圾收集器管理的主要区域也称为 GC 堆可以细分为新生代Eden、S0、S1老年代会抛出OutOfMemoryError方法区存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据在 HotSpot 虚拟机中JDK 8 之前称为“永久代”JDK 8 之后改为元空间使用本地内存实现会抛出OutOfMemoryError运行时常量池方法区的一部分用于存放编译期生成的各种字面量和符号引用二、JVM 调优2.1 调优目标JVM 调优主要是根据业务需求和压测结果调整内存参数和垃圾回收策略以达到系统更稳定吞吐量更高延迟更低2.2 调优方向调整堆内存大小合理设置-Xms初始堆大小和-Xmx最大堆大小避免频繁 Full GC选择合适的垃圾收集器低延迟场景G1、ZGC高吞吐场景Parallel GC优化 GC 相关参数调整新生代、老年代比例调整 GC 触发阈值减少 Full GC 次数监控与定位问题通过日志、JProfiler、VisualVM 等工具分析关注 GC 情况、内存泄漏、线程阻塞等问题2.3 一句话总结JVM 调优就是通过调整内存和 GC 参数结合监控分析减少 GC 压力、提升系统稳定性和性能。三、垃圾回收GC3.1 垃圾回收分区内存结构JVM 把堆内存主要分为两大区新生代Young Generation存放新创建的对象分为Eden 区 两个 Survivor 区S0、S1特点对象生命周期短、回收频繁Minor GC老年代Old Generation存放经过多次回收仍存活的对象特点体积大、回收少但耗时Full GC/Major GC元空间Metaspace存放类信息、常量、静态变量等不属于堆由本地内存实现3.2 常见垃圾回收器按版本和用途分类回收器特点适用场景Serial GC单线程回收简单但停顿时间长单核、小内存、客户端程序Parallel GC多线程回收追求高吞吐量后台计算、批处理任务ParNew GCParallel Scavenge 的多线程版本配合 CMS 使用CMS老年代回收器追求低延迟对响应时间敏感的应用G1目前企业最常用的通用回收器兼顾吞吐量和低延迟ZGC / Shenandoah超低延迟回收器毫秒级停顿大内存、低延迟要求高的服务CMS 收集器详解重点核心思想最短回收停顿时间。大部分工作与用户线程并发执行。回收过程初始标记STW并发标记重新标记STW并发清除优点并发收集低停顿缺点并发阶段会占用 CPU 资源可能影响应用性能无法处理“浮动垃圾”可能导致“Concurrent Mode Failure”使用“标记-清除”算法会产生内存碎片可能触发频繁的 Full GCG1 收集器详解重点核心思想将整个堆划分为多个大小相等的独立区域优先回收价值最大的区域垃圾最多的区域故名 Garbage-First。回收过程初始标记并发标记最终标记筛选回收优势并行与并发分代收集空间整合整体看是标记-整理局部看是复制可预测的停顿时间模型3.3 如何判断对象是否可回收1. 引用计数法Java 未采用给对象添加一个引用计数器被引用时 1引用失效时 -1缺点无法解决循环引用问题2. 可达性分析算法Java 采用通过一系列称为“GC Roots”的对象作为起点从这些节点开始向下搜索所走过的路径称为“引用链”当一个对象到 GC Roots 没有任何引用链相连时则证明此对象不可用GC Roots 包括虚拟机栈栈帧中的局部变量表中引用的对象本地方法栈中 JNI即 Native 方法引用的对象方法区中静态属性引用的对象方法区中常量引用的对象所有被同步锁synchronized 关键字持有的对象3.4 垃圾回收算法1. 标记-清除算法Mark-Sweep过程先标记出存活的对象再清除未被标记的垃圾对象优点实现简单缺点回收后会产生内存碎片可能导致后续无法分配大对象效率不高2. 标记-复制算法Mark-Copy过程将内存分为两块只用其中一块存活对象复制到另一块再清空使用过的内存优点无内存碎片效率高缺点内存利用率减半适合存活对象少的场景如新生代3. 标记-整理算法Mark-Compact过程先标记存活对象再将存活对象向内存一端移动最后清理边界外的垃圾优点无内存碎片解决了标记-清除的碎片问题缺点需要移动对象有一定性能开销适合老年代4. 分代收集算法Generational Collection核心思想根据对象存活周期的不同将内存分为新生代和老年代采用不同的回收算法新生代对象存活时间短用标记-复制算法老年代对象存活时间长用标记-清除或标记-整理算法3.5 垃圾回收调优核心目标减少 GC 次数、降低 GC 停顿、避免 Full GC调优方向合理设置堆内存大小调整-Xms、-Xmx避免内存过小导致频繁 GC尽量让-Xms -Xmx减少扩容/缩容开销优化新生代比例调整新生代大小如-XX:NewRatio让对象尽早回收减少进入老年代的概率选择合适的垃圾收集器低延迟G1、ZGC高吞吐Parallel GC老系统兼容CMS减少大对象和内存泄漏避免大对象直接进入老年代通过监控定位内存泄漏修复代码监控 GC 状态查看 GC 日志使用 JProfiler 等工具分析停顿时间、频率根据结果再调整参数一句话总结GC 调优就是通过合理设置内存、选择合适 GC、减少大对象和内存泄漏降低 Full GC 和停顿时间提升系统稳定性和性能。四、类加载机制4.1 Java 的引用类型引用类型特点回收时机强引用最常见的引用如Object obj new Object()只要强引用存在垃圾收集器就永远不会回收软引用有用但非必需的对象系统将要发生内存溢出之前会进行第二次回收弱引用非必需对象强度比软引用更弱只能生存到下一次垃圾收集发生之前虚引用最弱的引用完全不影响对象的生命周期对象被收集器回收时收到系统通知4.2 类的生命周期加载过程加载通过类的全限定名获取定义此类的二进制字节流将字节流所代表的静态存储结构转化为方法区的运行时数据结构在内存中生成一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口验证确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求不会危害虚拟机自身安全准备为类变量分配内存并设置初始值零值例如public static int value 123;在准备阶段后value是 0而不是 123解析将常量池内的符号引用替换为直接引用的过程初始化执行类构造器clinit()方法的过程真正开始执行类中定义的 Java 程序代码字节码例如上面的value在此阶段被赋值为 123使用卸载4.3 双亲委派模型核心逻辑类加载器加载类时先把请求向上委派给父加载器直到启动类加载器加载顺序父加载器先尝试加载若加载不到再由子加载器自己加载目的保证类的唯一性避免重复加载同时保护核心类不被自定义类替换工作流程当一个类加载器收到类加载请求它首先不会自己去尝试加载而是把这个请求委派给父类加载器去完成每一层的类加载器都是如此因此所有的加载请求最终都应该传送到顶层的启动类加载器只有当父加载器反馈自己无法完成这个加载请求它的搜索范围中没有找到所需的类时子加载器才会尝试自己去加载类加载器层次自上而下启动类加载器加载JAVA_HOME/lib下的核心库扩展类加载器加载JAVA_HOME/lib/ext下的扩展库应用程序类加载器加载用户类路径上的类自定义类加载器好处避免类的重复加载确保一个类在全局唯一性保护程序安全防止核心 API 被随意篡改例如你自己定义了一个java.lang.String类双亲委派机制会保证最终加载的是核心库里的 String而不是你自定义的五、经典问题排查5.1 如何排查 OOM 问题增加参数dump 出堆转储文件-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/path/to/dump.hprof使用分析工具MAT (Memory Analyzer Tool)JProfilerVisualVM分析步骤首先看泄漏嫌疑报告找到占用内存最大的对象查看 GC Roots 的引用链找到是哪个对象持有了这些本该被回收的对象的引用结合代码定位问题根源5.2 线上服务 CPU 飙升如何排查找到 CPU 占用最高的 Java 进程top找到该进程内 CPU 占用最高的线程top-Hp[PID]将线程 PID 转换为 16 进制printf%x\n[线程PID]导出线程快照jstack[Java进程PID]jstack.log分析线程堆栈在jstack.log文件中搜索刚才转换得到的 16 进制 nid找到对应的线程堆栈信息定位问题分析该线程正在执行的代码常见原因死循环、频繁 GC 等5.3 如何判定内存泄漏1. 看内存趋势是否“只增不减”长期观察 used_memory业务写入量没明显增加但内存持续上涨重启后内存明显下降过一段时间又慢慢涨回来基本可以判定为内存泄漏或内存碎片严重2. 检查内存碎片率是否异常查看 mem_fragmentation_ratio长期 1.5 甚至 2说明碎片在不断累积碎片率高 ≠ 内存泄漏但会表现为“内存看起来很大但实际有效数据不多”若碎片率高且内存持续涨就是泄漏 碎片双重问题3. 排查是否有“持续增长的 key”检查是否有未设置过期时间的大 key、大 hash、大 list持续追加、不清理的日志/统计类 key业务代码中忘记删除的临时 key使用工具排查redis-cli--bigkeysredis-cli--scan总结Redis 内存泄漏主要看内存持续上涨重启后明显回落碎片率异常存在长期不释放的大 key文档整理完成结构清晰便于复习和查阅。