JVM知识
目录
运行时数据区域
程序计数器
Java虚拟机栈
局部变量表
操作数栈
动态链接
本地方法栈
Java堆
方法区
运行时常量池
字符串常量池
直接内存
Java对象的创建过程
对象的内存布局
对象的访问
常见的 GC 类型
Minor GC(Young GC)
Major GC(Old GC)
Full GC
Mixed GC
触发Minor GC
触发Full GC
Minor GC 与 Full GC 对别
JVM判断对象死亡流程
1.核心算法:可达性分析(Reachability Analysis)
引用计数法
可达性分析
2.引用类型对存活判定影响
3.对象“真正死亡”的两次标记流程
如何判断一个常量是废弃常量
如何判断一个类是无用的类
垃圾回收器的算法
标记-清除算法(Mark-Sweep)
复制算法(Copying)
标记-整理算法(Mark-Compact)
分代收集算法(Generational Collection)
垃圾收集器
串行收集器(单线程)
Serial收集器
Serial Old收集器
并行收集器(多线程)
ParNew收集器
Parallel Scavenge收集器
Parallel Old收集器
并发收集器(低延迟)
CMS(Concurrent Mark Sweep)收集器
G1(Garbage-First)收集器
1.Young GC(年轻代回收)
2. Mixed GC(混合回收)
3. Full GC(完全回收)
类加载过程
类加载器
双亲委派机制
优点
Tomcat打破双亲委派机制
为什么
怎么实现
1. 类加载器架构
2. 打破委派的关键逻辑
3. 安全边界控制
核心JVM参数
1.内存管理参数
2.垃圾回收器参数
3.监控与诊断参数
4.容器化部署(如Docker/K8s)专用配置
JDK监控和故障处理
常用命令行
MAT工具
1.初步筛查:Leak Suspects报告
2. 直方图(Histogram)分析
3. 支配树(Dominator Tree)定位
4. 对比分析(关键场景)
Arthas 核心排查场景
1.性能瓶颈定位
2.动态代码诊断
3.线程问题排查
运行时数据区域
区域 | 线程私有/共享 | 内存管理特点 | 异常类型 |
---|---|---|---|
程序计数器 | 私有 | 无OOM | 不会内存溢出 |
虚拟机栈 | 私有 | 栈帧结构,固定或动态扩展 | StackOverflowError/OOM |
本地方法栈 | 私有 | 服务于Native方法 | StackOverflowError/OOM |
堆 | 共享 | 分代垃圾回收,动态扩展 | OOM: Java heap space |
方法区(元空间) | 共享 | 本地内存实现,类元数据存储 | OOM: Metaspace |
直接内存 | 共享 | 堆外内存,NIO优化 | OOM(受系统内存限制) |
程序计数器
记录当前线程执行的字节码指令地址(行号),用于控制执行流程(如分支、循环等)和多线程切换后的恢复。
Java虚拟机栈
存储方法调用的栈帧(Stack Frame),包含局部变量表、操作数栈、动态链接、方法出口等信息。
局部变量表
主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)
操作数栈
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接
主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。
本地方法栈
虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
Java堆
存储对象实例和数组,是垃圾回收(GC)的主要区域。
方法区
存储类元数据(类名、字段、方法等)、常量、静态变量、即时编译后的代码缓存等。JDK 8之前由永久代(PermGen)实现,JDK 8及之后由元空间(Metaspace)实现,使用本地内存而非JVM内存
运行时常量池
方法区的一部分,存储类文件中的常量池信息(如字面量、符号引用),支持动态性(如String.intern()
方法)
字符串常量池
字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。
JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动到了 Java 堆中。
直接内存
直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。
Java对象的创建过程
1.类加载检查:当使用new
关键字或反射创建对象时,JVM首先检查类是否已被加载
2.分配内存:对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配的两种方式 :
- 指针碰撞:
- 适用场合:堆内存规整(即没有内存碎片)的情况下。
- 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
- 使用该分配方式的 GC 收集器:Serial, ParNew
- 空闲列表:
- 适用场合:堆内存不规整的情况下。
- 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
- 使用该分配方式的 GC 收集器:CMS
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
3.初始化零值:分配内存后,JVM将实例数据初始化为默认值(如int
为0、引用类型为null
),确保未显式赋值的字段可直接使用
4.设置对象头:初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式
5.执行init方法:在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init>
方法还没有执行,所有的字段都还为零。所以需要按照程序员的意愿进行初始化,例如:按代码顺序初始化成员变量和实例代码块、执行用户定义的构造函数逻辑、将堆内存地址赋值给栈中的引用变量,对象进入可用状态。
对象的内存布局
对象在堆中的结构分为三部分:
- 对象头(Header):标记字段和类型指针。
- 实例数据(Instance Data):包含父类及子类定义的成员变量值。
- 对齐填充(Padding):确保对象大小为8字节的整数倍(HotSpot虚拟机要求)
对象头包括两部分信息:
- 标记字段(Mark Word):用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
- 类型指针(Klass pointer):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。
对象的访问
Java对象通过引用变量访问,引用变量存储在栈内存中,实际对象数据存储在堆内存。访问过程的关键在于如何通过引用找到堆中的对象实例数据,主要分为两种方式:
- 句柄访问:如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
- 直接指针访问:如果使用直接指针访问,reference 中存储的直接就是对象的地址。
这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
常见的 GC 类型
Minor GC(Young GC)
针对新生代(Eden + Survivor 区)的垃圾回收,触发频率高,速度快。
Major GC(Old GC)
针对老年代的垃圾回收,通常伴随至少一次 Minor GC,速度较慢。
Full GC
全局回收,清理整个堆(新生代 + 老年代)和元空间(或永久代),耗时长且会暂停所有线程。
Mixed GC
G1 收集器特有,回收整个新生代和部分老年代,触发条件为老年代内存占比超过 45%。
触发Minor GC
由于对象一般情况下是直接在新生代中的 Eden 区进行分配,如果Eden区域没有足够的空间,那么就会触发young GC(Minor GC)。因为 Java 对象大多数具备朝生夕死的特性,所以 young GC(Minor GC)会非常频繁,回收速度也非常快。
触发Full GC
- 老年代空间不足:晋升对象超过老年代容量,或大对象直接分配失败。
- 元空间/永久代不足(JDK8 前为永久代,JDK8 后为元空间)。
- 显式调用
System.gc()
:建议 JVM 执行 Full GC(实际是否执行由 JVM 决定)。 - 空间分配担保失败:在 YGC 之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果小于,说明 YGC 是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发 Full GC;如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC
- 垃圾收集器特定行为:如 CMS 的并发模式失败(Concurrent Mode Failure)或 G1 的混合 GC 失败。
Minor GC 与 Full GC 对别
GC 类型 | 触发场景 | 回收范围 | STW 影响 |
---|---|---|---|
Minor GC | Eden 区满 | 新生代(Eden+Survivor) | 短时暂停(毫秒级) |
Full GC | 老年代/元空间不足、显式调用 | 全堆(含元空间) | 长时暂停(秒级) |
JVM判断对象死亡流程
JVM 判断对象死亡的步骤
- 可达性分析:从 GC Roots 遍历引用链,标记不可达对象。
- 引用类型过滤:根据软/弱引用决定是否提前回收。
- 两次标记:通过
finalize()
给予对象最后一次自救机会。 - 最终回收:自救失败的对象被垃圾回收器清除。
1.核心算法:可达性分析(Reachability Analysis)
引用计数法
给对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。
可达性分析
JVM 从一组称为 GC Roots 的根对象出发,通过引用链向下搜索。若某个对象无法通过任何引用链与 GC Roots 相连,则判定为不可达对象,即“死亡”。
哪些对象可以作为 GC Roots 呢?
类型 | 示例 |
---|---|
虚拟机栈引用的对象 | 局部变量、方法参数、临时变量 |
方法区静态属性引用的对象 | 类的静态变量(如 public static Object obj; ) |
方法区常量引用的对象 | 字符串常量池中的引用 |
本地方法栈(JNI)引用的对象 | Native 方法引用的对象 |
同步锁持有的对象 | synchronized 锁定的对象 |
JVM 内部引用 | 系统类加载器、异常对象、Class 对象 |
2.引用类型对存活判定影响
JVM 根据引用强度决定回收优先级:
-
强引用(Strong Reference)
- 默认引用类型(如
Object obj = new Object();
)。 - 只要强引用存在,对象绝不会被回收。
- 默认引用类型(如
-
软引用(Soft Reference)
- 内存不足时才会回收(适合缓存场景)。
- 示例:
SoftReference<Object> softRef = new SoftReference<>(obj);
-
弱引用(Weak Reference)
- 无论内存是否充足,下次 GC 必然回收。
- 示例:
WeakReference<Object> weakRef = new WeakReference<>(obj);
-
虚引用(Phantom Reference)
- 不影响对象生命周期,仅用于回收跟踪(需配合
ReferenceQueue
)
- 不影响对象生命周期,仅用于回收跟踪(需配合
3.对象“真正死亡”的两次标记流程
即使对象不可达,也需经过以下步骤才会被回收:
-
第一次标记
- 对象被判定为不可达后,JVM 标记其为“待回收”。
-
筛选是否需要执行
finalize()
- 若对象未覆盖
finalize()
或已执行过该方法,则直接判定死亡。 - 若需执行,对象被加入 F-Queue 队列,由低优先级
Finalizer
线程触发其finalize()
方法。
- 若对象未覆盖
-
第二次标记
- 若对象在
finalize()
中成功自救(如将this
赋值给某静态变量),则移出回收集合。 - 若自救失败,则被标记为“真正死亡”。
- 若对象在
如何判断一个常量是废弃常量
假如在字符串常量池中存在字符串 "abc",如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 "abc" 就是废弃常量,如果这时发生内存回收的话而且有必要的话,"abc" 就会被系统清理出常量池了。
如何判断一个类是无用的类
类卸载的过程
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收器的算法
标记-清除算法(Mark-Sweep)
- 原理:从GC Roots(如静态变量、活动线程栈引用等)遍历对象图,标记所有可达对象。回收未被标记的对象内存
- 优点:实现简单,直接回收垃圾对象。
- 缺点:
- 产生内存碎片,影响大对象分配;
- 需扫描整个堆两次(标记+清除),效率较低。
- 适用场景:老年代回收(如CMS收集器的部分阶段)
复制算法(Copying)
- 原理
- 标记阶段:从GC Roots(如静态变量、活动线程栈引用等)遍历对象图,标记所有可达对象。
- 清除阶段:回收未被标记的对象内存
- 优点:
- 无内存碎片;
- 回收效率高(仅扫描存活对象)。
- 缺点:
- 浪费50%内存空间;
- 对象复制成本高,不适合存活率高的场景。
- 适用场景:新生代回收(如Serial、ParNew收集器),因98%的新生代对象“朝生夕死”,存活对象少
标记-整理算法(Mark-Compact)
- 原理:
- 标记阶段:同标记-清除算法;
- 整理阶段:将存活对象向内存一端移动,消除碎片。
- 优点:避免内存碎片,提高内存利用率。
- 缺点:对象移动成本高,暂停时间长(Stop-The-World)。
- 适用场景:老年代回收(如Serial Old、Parallel Old收集器)
分代收集算法(Generational Collection)
- 原理:
根据对象生命周期将堆划分为 新生代(Young Generation)和 老年代(Old Generation):- 新生代:采用复制算法(Eden区 + 两个Survivor区),触发Minor GC。
- 老年代:采用标记-清除或标记-整理算法,触发Major GC/Full GC。
- 设计依据:
- 新生代对象存活率低,适合高效复制的算法;
- 老年代对象存活率高,需避免碎片和移动开销。
- 优点:结合场景优化效率,是现代JVM主流策略(如HotSpot虚拟机)
垃圾收集器
串行收集器(单线程)
-
Serial收集器
- 作用区域:新生代
- 算法:复制算法
- 特点:单线程工作,GC时需暂停所有用户线程(Stop-The-World, STW)。
- 适用场景:单核CPU或客户端模式(如桌面应用),内存开销小。
-
Serial Old收集器
- 作用区域:老年代
- 算法:标记-整理算法
- 特点:Serial的老年代版本,作为CMS失败时的后备方案
并行收集器(多线程)
-
ParNew收集器
- 作用区域:新生代
- 算法:复制算法
- 特点:Serial的多线程版本,需与CMS配合使用,多核环境下性能优于Serial。
-
Parallel Scavenge收集器
- 作用区域:新生代
- 算法:复制算法
- 特点:这是 JDK1.8 默认收集器,吞吐量优先,可自适应调节参数(如
-XX:MaxGCPauseMillis
控制最大停顿时间)。
-
Parallel Old收集器
- 作用区域:老年代
- 算法:标记-整理算法
- 特点:Parallel Scavenge的老年代搭档,JDK6后支持,注重吞吐量
并发收集器(低延迟)
-
CMS(Concurrent Mark Sweep)收集器
- 作用区域:老年代
- 算法:标记-清除算法
- 特点:
- 并发标记与清理,减少STW时间,适合响应敏感场景(如Web服务),CMS 垃圾回收器在 Java 9 中已经被标记为过时(deprecated),并在 Java 14 中被移除。
- 缺点:
- 对 CPU 资源敏感:在并发阶段,CMS 虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说是处理器的计算能力)而导致应用程序变慢,降低吞吐量
- 无法处理浮动垃圾:在
CMS
的并发标记和并发清理阶段,由于用户线程继续运行,因此有可能会有新的垃圾对象产生。但这一部分垃圾对象是出现在标记结束之后,CMS
无法在当次收集中处理掉它们,只能留待下一次垃圾收集在清理,这一部分垃圾就称为浮动垃圾 - 大量空间碎片的产生:它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
- 并发失败:若用户线程分配过快,预留内存不足时触发 Full GC
- 流程:
- 初始标记: 短暂停顿,仅标记 GC Roots 直接关联的对象(如栈帧、静态变量),耗时极短,需要STW;
- 并发标记: 用户线程与 GC 线程并发执行,从初始标记对象出发,遍历整个引用链,标记所有存活对象。
- 重新标记: 重新标记阶段就是为了修正并发标记期间,因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段需要
STW
,而且停顿时间通常比初始阶段稍长一些,但也远比并发标记阶段的时间短。 - 并发清除: 用户线程与 GC 线程并发执行,清理未被标记的死亡对象(不整理内存,产生碎片),清理期间用户线程可能产生新垃圾(浮动垃圾需下次回收)。
-
G1(Garbage-First)收集器
Garbage First(G1)
收集器是一款主要面向服务端应用的垃圾收集器,开创了收集器面向局部收集的设计思路和基于 Region
的内存布局形式。
- 作用区域:新生代 + 老年代(全堆)
- 算法:分区标记-整理 + 局部复制算法
- 特点:
- 分区回收:堆划分为 2048 个等大 Region(1MB~32MB),逻辑分代但物理混合
- Humongous 区:专存大于 Region 50% 的大对象
- 可预测停顿:通过
-XX:MaxGCPauseMillis
设定目标停顿时间。 - JDK9后默认收集器,替代CMS。
1.Young GC(年轻代回收)
触发时机
- Eden区满了,无法再分配新对象时触发。
流程:
- 标记存活对象:从GC Roots出发,标记Eden和Survivor区的存活对象。
- 复制存活对象:将Eden和Survivor区的存活对象复制到新的Survivor区(To Survivor)或直接晋升到Old区(如果年龄足够或Survivor区放不下)。
- 清理Eden区:Eden区所有对象都会被清理(因为要么被复制走了,要么不可达)。
- 更新年龄:Survivor区对象年龄+1,达到阈值的晋升到Old区。
特点:
- 只回收年轻代(Eden+Survivor)。
- 停顿时间短,频率高。
- 并行执行,效率高。
2. Mixed GC(混合回收)
触发时机
- Old区占用率较高,达到阈值后,默认值是45%,通过参数 -XX:InitiatingHeapOccupancyPercent(简称IHOP)控制,G1会在Young GC后自动触发Mixed GC。
流程:
- 初始标记(Initial Mark):与Young GC一起完成,标记GC Roots直接可达的对象。
- 并发标记(Concurrent Mark):并发地遍历整个堆,标记所有存活对象。
- 最终标记(Remark):暂停应用,完成最后的标记工作,处理并发标记期间发生的对象变动。
- 筛选回收(Cleanup):统计各Region的存活率,选出回收收益高的Old区Region。
- 复制和清理:
- 回收Eden、Survivor区。
- 同时回收部分Old区(选中的Region),将存活对象复制到其他Region或晋升。
特点:
- 回收年轻代+部分老年代(不是全部老年代)。
- 通过回收收益预测,优先回收垃圾最多的Region。
- 停顿时间可控,效率高于Full GC。
3. Full GC(完全回收)
触发时机:
- 老年代空间不足,无法晋升对象。
- Mixed GC无法回收足够空间。
- 显式调用
System.gc()
。 - 元空间(Metaspace)溢出等特殊情况。
流程:
- 暂停所有应用线程(Stop The World)。
- 标记所有存活对象:遍历整个堆(包括年轻代和老年代),标记所有可达对象。
- 整理和回收:
- 回收所有不可达对象。
- 整理堆空间,可能会进行对象移动和压缩(防止碎片化)。
- 恢复应用线程。
特点:
- 回收整个堆(年轻代+老年代+元空间)。
- 停顿时间长,影响应用性能。
- 频率低,尽量避免。
类加载过程
类加载就是把类(通常是.class
文件的形式)通过类加载器加载到 JVM 中,经过一系列的解析成可用的 class 类。
- 加载 : 类加载器将二进制流读到内存,并生成Class对象,作为方法区这个类的各种数据的访问入口
- 链接 - 细分为三个子阶段:
- 验证 : 主要验证加载进来的二进制流是否符合一定格式,类文件的结构是否符合Java规范等。
- 准备 : 为类的静态变量在方法区分配内存并赋初值(如 int 的初值为 0,引用类型为null)这时还未执行任何Java代码,比如static int a = 123,在准备阶段a的值是0,而不是123
- 解析
-
将常量池中的符号引用(抽象名称)替换为直接引用(内存地址或偏移量),例如:类/接口、字段、方法的符号引用转为实际内存指针
-
- 初始化 :
- 执行类构造器<clinit>()方法
- 为类变量赋予正确的初始值,比如static int a = 123,在准备阶段a的值是0,现在会被赋值为123
- 执行静态代码块
- 使用(Using)
- 类加载完成后,就可以使用这个类了
- 卸载(Unloading)
- 当类不再被使用时,可以被卸载,需要满足三个条件:(也是如何判断类是否无用可以被回收的原则)
- 该类的所有实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类的Class对象没有被任何地方引用
- 当类不再被使用时,可以被卸载,需要满足三个条件:(也是如何判断类是否无用可以被回收的原则)
类加载器
- 启动类加载器(Bootstrap ClassLoader)
- 负责加载JAVA_HOME/lib目录下的核心类库
- 由C++实现,不是Java类
- 扩展类加载器(Extension ClassLoader)
- 负责加载JAVA_HOME/lib/ext目录下的扩展类库
- 由Java实现
- 应用程序类加载器(Application ClassLoader)
- 负责加载用户类路径(ClassPath)上的类库
- 是默认的类加载器
- 自定义类加载器
- 用户可以通过继承ClassLoader类来实现自己的类加载器
类加载器过程
Bootstrap ClassLoader(启动类加载器)↑
Extension ClassLoader(扩展类加载器)↑
Application ClassLoader(应用程序类加载器)↑
Custom ClassLoader(自定义类加载器)
双亲委派机制
当需要加载一个类时,类加载器会按照以下步骤工作:
- 委托父类加载器
- 首先检查父类加载器是否已经加载过这个类
- 如果父类加载器已经加载过,直接返回该类的Class对象
- 如果父类加载器没有加载过,则委托父类加载器去加载
- 父类加载器加载过程
- 如果父类加载器还有父类,则继续向上委托
- 直到到达最顶层的Bootstrap ClassLoader
- 自顶向下尝试加载
- 如果Bootstrap ClassLoader无法加载,则向下传递
- 由Extension ClassLoader尝试加载
- 如果Extension ClassLoader也无法加载,则继续向下传递
- 最后由Application ClassLoader尝试加载
优点
安全性保障:核心类库(如 java.lang.*
)由启动类加载器(Bootstrap ClassLoader)优先加载,防止恶意代码篡改核心类
避免类重复加载:通过层级委派(父加载器 → 子加载器),确保同一个类在 JVM 中仅加载一次,节省内存并避免冲突。
保证类一致性:同一类在不同加载器下具有相同定义,避免同名类因加载来源不同导致的行为异常
Tomcat打破双亲委派机制
为什么
隔离性:不同 Web 应用可能依赖同名但版本冲突的库(如 log4j 1.x
和 2.x
)。传统双亲委派会导致所有应用共享同一类,无法隔离
热部署:应用更新时需动态重载类,而双亲委派机制中父加载器已加载的类无法被替换
灵活性限制:应用可能需要优先加载自身目录的类(如 WEB-INF/lib
中的库),而非父加载器提供的版本
怎么实现
Tomcat 通过自定义类加载器层级和调整加载顺序实现:
1. 类加载器架构
-
WebappClassLoader
:每个 Web 应用独立拥有,负责加载WEB-INF/classes
和WEB-INF/lib
中的类。 -
SharedClassLoader
:加载多个应用共享的库(如通用工具包)。 -
CommonClassLoader
:加载 Tomcat 自身及全局共享库(如 Servlet API)。
2. 打破委派的关键逻辑
WebappClassLoader
重写 loadClass
方法,调整加载顺序:
- 步骤1:先检查自身是否已加载该类。
- 步骤2:优先从应用本地路径(
WEB-INF
)加载,而非委派父加载器。 - 步骤3:若本地未找到,再委派给
SharedClassLoader
或CommonClassLoader
// 简化版 WebappClassLoader 逻辑
public Class<?> loadClass(String name) throws ClassNotFoundException {// 1. 检查自身是否已加载Class<?> clazz = findLoadedClass(name);if (clazz == null) {try {// 2. 优先从应用本地加载(打破委派的关键)clazz = findClass(name); } catch (ClassNotFoundException e) {// 3. 本地未找到时委派给父加载器clazz = super.loadClass(name); }}return clazz;
}
3. 安全边界控制
- 对 Java 核心类(如
java.*
)仍强制委派给父加载器,防止应用覆盖核心类。 - 对 JavaEE 规范类(如
javax.servlet.*
),优先由CommonClassLoader
加载,确保使用 Tomcat 提供的实现
核心JVM参数
1.内存管理参数
-Xms
:初始堆大小(如-Xms1g
),避免运行时动态扩容的开销。-Xmx
:最大堆大小(如-Xmx4g
),需与-Xms
一致以减少内存波动。-Xmn
:新生代大小(如-Xmn512m
),建议占堆的30%~40%。-XX:MaxMetaspaceSize
:元空间上限(如-XX:MaxMetaspaceSize=256m
),替代JDK8前的永久代。-XX:SurvivorRatio
:Eden区与Survivor区比例(如-XX:SurvivorRatio=8
,即Eden:Survivor=8:1)
2.垃圾回收器参数
- G1GC(推荐)
-XX:+UseG1GC 启用G1垃圾收集器
- -XX:MaxGCPauseMillis=200 目标最大停顿时间(毫秒)
- -XX:InitiatingHeapOccupancyPercent=45 堆占用45%时启动并发标记
- -XX:G1HeapRegionSize=16m Region大小(建议堆≥4G时设16m)
- -XX:ConcGCThreads=4 并发标记线程数(建议=ParallelGCThreads/4)
- -XX:ParallelGCThreads=8 并行回收线程数(建议=CPU核心数)
- -XX:G1NewSizePercent=30 新生代最小占比
- -XX:G1MaxNewSizePercent=60 新生代最大占比
- -XX:G1MixedGCLiveThresholdPercent=85 混合GC存活对象阈值
- -XX:+ParallelRefProcEnabled 并行处理引用对象
- CMS
- -XX:+UseConcMarkSweepGC 启用CMS
- -XX:+UseParNewGC 新生代使用ParNew
- -XX:CMSInitiatingOccupancyFraction=75 老年代占用75%时触发CMS回收
- -XX:+UseCMSInitiatingOccupancyOnly 强制按阈值触发,避免自动调整
- -XX:ParallelGCThreads=8 并行GC线程数(建议=CPU核心数)
- -XX:ConcGCThreads=4 并发标记线程数(建议=ParallelGCThreads/4)
- -XX:+CMSParallelRemarkEnabled 降低重新标记阶段的停顿
- -XX:+UseCMSCompactAtFullCollection Full GC时压缩内存碎片
- -XX:CMSFullGCsBeforeCompaction=0 # 每次Full GC均压缩碎片
- -XX:MetaspaceSize=256m # 元空间初始大小(避免扩容GC)
- ZGC:
-XX:+UseZGC
,适用于超大堆(>32GB)和极致低延迟场景。 - 并行GC:
-XX:+UseParallelGC
,侧重高吞吐量。
3.监控与诊断参数
-XX:+PrintGCDetails -Xloggc:/path/to/gc.log
:记录详细GC日志。-
-XX:+PrintGCDateStamps:
在 GC 日志中输出具体日期时间戳(例如2023-06-03T14:40:31.391+0800
) -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof
:OOM时自动生成堆转储。-XX:NativeMemoryTracking=detail
:跟踪非堆内存使用
4.容器化部署(如Docker/K8s)专用配置
- -XX:+UseContainerSupport :启用容器感知,自动读取容器内存/CPU限制
- -XX:InitialRAMPercentage=50.0 # 初始堆占容器内存的50%
- -XX:MaxRAMPercentage=75.0 # 最大堆占容器内存的75%
替代传统 -Xms/-Xmx
:避免硬编码内存值导致容器资源不足或浪费
JDK监控和故障处理
常用命令行
-
jps(JVM进程状态工具)
- 功能:列出所有正在运行的Java进程及其LVMID(本地虚拟机唯一ID)。
- 常用命令:
jps -l
:输出主类全名jps -v
:显示JVM启动参数
-
jstat(JVM统计监控工具)
- 功能:实时监控类加载、内存、GC、JIT编译等数据。
- 关键参数:
jstat -gcutil <pid>
:显示堆内存各区域使用比例(Eden/Survivor/Old区)jstat -gc <pid> 1000 5
:每秒输出1次GC数据,共5次jstat -class vmid
:显示 ClassLoader 的相关信息;jstat -compiler vmid
:显示 JIT 编译的相关信息;jstat -gccapacity vmid
:显示各个代的容量及使用情况;jstat -gcnew vmid
:显示新生代信息;jstat -gcnewcapcacity vmid
:显示新生代大小与使用情况;jstat -gcold vmid
:显示老年代和永久代的行为统计,从 jdk1.8 开始,该选项仅表示老年代,因为永久代被移除了;jstat -gcoldcapacity vmid
:显示老年代的大小;jstat -gcpermcapacity vmid
:显示永久代大小,从 jdk1.8 开始,该选项不存在了,因为永久代被移除了;jstat -gcutil vmid
:显示垃圾收集信息
-
jinfo(配置信息工具)
- 功能:实时地查看和调整虚拟机各项参数(仅限可动态调整的参数)。
- 示例:
jinfo -flag CMSInitiatingOccupancyFraction <pid>
-
jmap(内存映像工具)
- 功能:生成堆转储快照(Heap Dump),分析内存对象分布。
- 常用命令:
jmap -dump:format=b,file=heap.hprof <pid>
:生成堆转储文件jmap -histo <pid>
:统计堆中对象数量及大小
-
jstack(堆栈跟踪工具)
- 功能:生成虚拟机当前时刻的线程快照,定位死锁、线程阻塞等问题。
- 进阶参数:
jstack -l <pid>
:附加锁信息jstack -F
:强制输出(针对无响应进程)
MAT工具
MAT分析步骤:
1.初步筛查:Leak Suspects报告
- 打开堆转储后,MAT自动生成Leak Suspects Report(饼图形式展示内存占用Top对象)。
- 关注Problem Suspect部分,查看疑似泄漏对象的引用链。
2. 直方图(Histogram)分析
- 通过 Histogram 视图查看对象数量和内存占用:
- 按包名过滤(如
com.example.*
)聚焦目标对象。 - 按
Retained Heap
排序,定位占用最高的对象。
- 按包名过滤(如
- 右键对象 → Merge Shortest Paths to GC Roots → 排除弱/软引用(仅保留强引用),找出阻止GC的引用链。
3. 支配树(Dominator Tree)定位
- 在 Dominator Tree 视图中,查看支配内存的对象树状结构。
- 重点关注Activity、Fragment、单例对象等可能泄漏的组件,检查其子树是否包含异常大量实例。
4. 对比分析(关键场景)
- 获取两个堆转储(如泄漏前/后),通过 Compare Basket 对比:
- 将Histogram或OQL结果分别加入Compare Basket → 点击 ! 图标对比差异。
- 观察对象数量增长情况,锁定泄漏对象
Arthas 核心排查场景
1.性能瓶颈定位
- CPU 飙高
thread -n 3 -i 1000 # 统计1秒内最忙的3个线程
profiler start # 生成火焰图定位热点方法
- 方法耗时分析:
trace com.example.Service * '#cost>100' # 追踪耗时>100ms的方法调用链
2.动态代码诊断
- 反编译线上类:
jad com.example.LeakClass # 反编译源码,验证代码版本
- 监控方法参数/返回值:
watch com.example.Service getUser "{params,returnObj}" -x 2 # 打印入参和返回值
3.线程问题排查
- 死锁/阻塞:
thread -b # 检测阻塞线程
thread --state WAITING # 查看等待状态的线程
- 线程池积压
ognl '@java.util.concurrent.ThreadPoolExecutor@getInstance().getQueue().size()' # 获取任务队列长度