jvm内存模型
- 方法区(
Method Area)、堆(Heap)、栈(Stack)、本地方法栈、程序计数器 - 堆内存结构:新生代(
Eden,From Survivor,To Survivor)、老年代 - 元空间(
Metaspace)与永久代(PermGen)的区别
类加载机制
- 类加载过程:加载 -> 验证 -> 准备 -> 解析 -> 初始化
- 类加载器种类:启动类加载器(
Bootstrap)、扩展类加载器(Extension)、应用程序类加载器(AppClassLoader) - 双亲委派模型(
Parent Delegation Model)
垃圾回收机制(GC)
GC Roots的类型(虚拟机栈中的引用对象、类静态属性引用对象等)- 常见垃圾回收算法:标记-清除、复制、标记-整理、分代收集
- 常见垃圾收集器:
Serial、Parallel Scavenge、CMS、G1、ZGC GC日志分析(如Xlog:gc*)
jvm性能调优
JVM启动参数配置(堆大小、元空间大小、GC策略)- 常见
JVM参数实例:
-Xms512m Xmx1024m -XX:MetaspaceSize=128m -XX:+UseG1GC
3. 性能监控工具:JConsole、VisualVM、MAT、Arthas、SkyWalking
jvm异常场景排查
- 常见异常类型:
OOM(Out of Memory)、StackOverflowError、GC overhead limit exceeed - 如何生成和分析
head dump文件
jvm字节码与执行引擎
Java字节码结构(Class文件格式)Class文件解析工具:javap -c- 执行引擎:解释执行 vs 即时编译(
JIT)
常见面试问题汇总
jvm内存结构
JVM内存分为哪些区域?各有什么作用?
程序计数器(Program Counter Register)
- 作用:记录当前线程所执行的字节码指令地址;对于
java方法,记录的是虚拟机字节码指令的地址,对于Native方法,则是空(undefined) - 特点:线程私有;唯一一个在
JVM规范中没有规定OutOfMemoryError的区域
java虚拟机栈(Java Virtual Machine Stack)
- 作用:存储方法调用时的局部变量、操作数栈、动态链接、方法出口等信息;每个方法被执行时会创建一个栈帧(
Stack Frame),并压入栈中 - 特点:线程私有;生命周期与线程相同;可能出现
StackOverflowError(栈溢出) 或OutOfMemoryError(内内存不足)
本地方法栈(Native Method Stack)
- 作用:为
JVM使用到的Native方法服务(如通过JNI调用的C/C++方法) - 特点:与
Java虚拟机栈类似,但服务于Native方法;具体实现由JVM提供商决定(如HotSpot中两者合二为一)
java堆(Java Heap)
- 作用:存放对象实例(几乎所有对象实例都在这里分配内存);是垃圾回收器(
GC)管理的主要区域 - 特点:线程共享;启动时创建;可通过
-Xms和-Xmx设置初始堆大小和最大堆大小;分为新生代(Young Generation)和老年代(Old Generation),进一步细分为Eden区,Survivor(From和To)
方法区(Method Area)
- 作用:存储类信息(类的元数据)、常量池、静态变量、编译器编译过后的代码等
- 特点:线程共享;在
JDK8及之前,使用永久代(PermGen)实现;在JDK8及之后,被元空间(Metaspace)取代
运行时常量池(Runtime Constant Pool)
- 作用:是方法区的一部分;存储编译期生成的各种字面量和符号引用(如字符串常量、类与方法的符号引用)
- 特点:类加载后常量池中的内容会被放入运行时常量;支持动态添加常量(如
String.intern())
元空间(Metaspace JDK8+)
- 作用:替代永久代(
PermGen),用于存储类的元数据(如类名、方法定义、字段定义等) - 特点:使用本地内存(
Native Memory),不再受限于PermGen固定大小;默认情况下自动调整大小,可通过参数控制(如-XX:MaxMetaspaceSize);更安全,避免了PermGen OOM的问题
| 区域名称 | 是否线程私有 | 是否可OOM | 主要作用 |
|---|---|---|---|
| 程序计数器 | 是 | 否 | 指向当前线程执行的字节码指令 |
| JAVA虚拟机栈 | 是 | 是 | 存储方法调用的局部变量、操作数栈 |
| 本地方法栈 | 是 | 是 | 为 Native 方法服务 |
| Java堆 | 否 | 是 | 存放对象实例,GC主要区域 |
| 方法区(JDK8及以前) | 否 | 是 | 存储类信息、常量池、静态变量等 |
| 运行时常量池 | 否 | 是 | 方法区的一部分,存方法编译器常量 |
| 元空间(JDK8+) | 否 | 是 | 替代方法区,存储类元数据 |
Java堆和栈的区别是什么?
| 特性 | Java堆 | Java栈 |
|---|---|---|
| 所属线程 | 所有线程共享 | 每个线程独立拥有 |
| 存储内容 | 对象实例、数组等 | 方法中的局部变量、基本数据类型、对象引用(Reference)、操作数栈等 |
| 生命周期 | 与JVM生命周期一致,程序启动时创建,关闭时销毁 | 与线程生命周期一致,方法调用时压栈,调用结束出栈 |
| 访问方式 | 可以被多个线程访问(需注意线程安全) | 线程私有,不存在并发问题 |
| 内存管理 | 由垃圾回收器(GC)自动管理 | 自动分配和释放,无需手动干预 |
| 性能开销 | 分配和回收成本较高 | 分配和释放速度快,效率高 |
| 异常控制 | OutOfMemoryError(当对空间不足时) | StackOverflowError(递归过深或栈空间不足)OutOfMemoryError(线程过多导致栈无法分配) |
| 大小控制 | 可通过 -Xms 和 -Xmx 设置初始和最大堆大小 | 可通过-Xss设置每个线程栈大小 |
Metaspace是什么?和PermGen有什么区别?
Metaspace 是 JVM 用来存储类的元信息的区域(类名、方法信息、字段信息、编译后的代码、常量池等)。
| 特性 | PermGen(JDK 7 及以前) | Metaspace(JDK 8+) |
|---|---|---|
| 内存类型 | 堆内存的一部分 | 本地内存(Native Memory) |
| 默认最大大小 | 固定大小(受限于 -XX:MaxPermSize) | 默认不限制(可自动扩展) |
OOM风险 | 容易因为加载大量类导致OOM | 更安全,但仍可能OOM(需设置上限) |
| 垃圾回收机制 | 由 Full GC 回收无用类元数据 | 同样由 GC 回收,但更灵活 |
| 配置参数 | -XX:PermSize,-XX:MaxPermSize | -XX:MetaspaceSize,-XX:MaxMatespaceSize |
| 是否需要手动调优 | 是,常需调整大小 | 否,通常自动管理,但生产环境建议设置上限 |
Metaspace 优势有哪些?以及潜在的问题?
优势:
- 避免
PermGen OOM问题:使用本地内存,理论上不受堆大小的限制 - 更灵活的内存管理:根据实际需求自动扩展或收缩
- 与
Native库更好的兼容性:更贴近底层操作系统资源管理
潜在的问题
- 仍可能发生
OOM:如果不设置MaxMetaspaceize,可能导致占用过多的本地内存,最终抛出java.lang.OutOfMemoryError: Metaspace - 类加载泄露风险:如
ClassLoader没有被正确回收,会导致元数据持续增长
类加载机制
- 类加载的过程是怎样的?
加载(Loading)
- 作用:查找并加载类的二进制字节流(
.class文件),并将其读入内存 - 触发方式:
- 显示加载:如
Class.forName("com.example.MyClass") - 隐式加载:如遇到
new关键字、静态字段访问、调用静态方法等
- 显示加载:如
- 结果:在方法区或元空间中创建一个
java.lang.Class对象
注意:加载阶段可以使用自定义的
ClassLoader来实现类的动态加载
验证(Verification)
- 作用:确保加载的
.class文件格式正确,符合当前JVM的要求,防止恶意代码破坏虚拟机 - 验证内容:
- 文件格式验证(是否符合
Class文件规范) - 元数据验证(语义分析,如继承关系是否合法)
- 字节码验证(确保操作不会危害虚拟机安全)
- 符号引用验证(确保常量池中的符号引用能解析)
- 文件格式验证(是否符合
注意:如果验证失败,会抛出
java.lang.VerifyError异常
准备(Preparation)
- 作用:为类的静态变量(
static fields)分配内存,并设置默认初始值(不是程序员赋的值) - 实例:
// 准备阶段只是将 value 初始化为0,真正的赋值在初始化阶段
public static int value = 123;
注意:常量(
final static)在准备阶段就会被显式赋值
解析(Resolution)
- 作用:将类、接口、字段和方法的符号引用(
Symbolic Reference)替换成直接引用(Direct Reference) - 符号引用:用一组符号来描述所引用的目标,与内存无关
- 直接引用:指向目标在内存中的实际地址
- 解析时机:
- 可在类加载时解析(静态解析)
- 也可以在第一次使用该符号引用时解析(运行时常量池加载)
初始化(Initialization)
- 作用:真正执行类中定义的
java程序代码 (即<clinit>方法),包括- 执行类构造器
<clinit>(由编译器自动收集所有静态变量的赋值动作和静态代码块合并生成) - 执行接口的默认初始化逻辑
- 执行类构造器
- 触发条件(主动引用):
- 创建类的实例(如
new MyClass()) - 调用类的静态方法(如
MyClass.staticMethod()) - 使用或赋值类的静态非
final字段 - 使用反射调用类(如
Class.forName()) - 子类初始化前会先初始化父类
- 包含
main方法的类会被首先初始化
- 创建类的实例(如
注意:只有主动引用才会触发类的初始化,被动引用(如通过子类引用父类静态字段)不会导致子类初始化
- 什么是双亲委派模型?为什么需要它?
在java中,类加载之间存在一种层级结构,当一个类加载器收到类加载请求时,它不会立即自己去加载这个类,而是将请求委托给其父类加载器去处理,只有当父类加载器无法完成加载任务时,才由当前类加载器尝试加载。
- 避免类的重复加载
- 同一个类只被加载一次,防止多个类加载器分别加载相同的类,造成内存浪费和冲突
- 保障核心类的安全性
- 核心类(如
java.lang.Object)只能由Bootstrap ClassLoader加载 - 即使用户自定义了一个
java.lang.String,也不会被加载,防止恶意篡改核心类
- 核心类(如
- 保证类加载的一致性和唯一性
- 同一个全限定名的类,在整个
JVM中只会有一个Class对象 - 不同类加载器加载的相同类会被认为是不同的类(如不同模块加载的类隔离)
- 同一个全限定名的类,在整个
- 支持模块化与隔离
- 如
OSGi、Tomcat等框架使用自定义类加载器实现模块隔离,但仍然遵循双亲委派原则来加载公共依赖
- 如
优点:安全性高(防止核心类被篡改或替换)、类唯一性(同一个类在整个 JVM 中只有一个实)、资源共享(多个类加载器可以共享已加载的核心类)、可扩展性强(允许自定义类加载器,同时保持统一加载机制)
类加载器层级结构
Bootstrap ClassLoader(启动类加载器):最顶层,由C++实现,负责加载 JVM 核心类库(如rt.jar中的java.lang.*类)Externsion ClassLoader(扩展类加载器):负责加载$JAVA_HOME/jre/lib/ext目录下的类或java.ext.dirs指定路径中的类Apllication ClassLoader(应用程序类加载器):也叫系统类加载器,负责加载用户类路径(classpath)中的类Custom ClassLoader(自定义类加载器):开发者自定义的类加载器,用于实现特殊需求(如热部署、加密类加载等)
双亲委派模型的工作流程
- 应用程序调用
ClassLoader.loadClass("com.exmple.MyClass") - 当前类加载器首先检查是否已经加载过该类(通过
findLoadedClass(name)) - 如果未加载,则调用父类加载器的
loadClass()加载 - 父类加载器重复上述过程,直到达到
Bootstrap ClassLoader - 如果所有父类加载器都无法加载该类,才由当前类加载器尝试加载(调用
findClass())
注意:这个过程是递归的,确保核心类加载器始终由最上级加载器加载
- 如何打破双亲委派模型?
虽然双亲委派是一个良好的设计原则,但是在某些场景下也需要“破坏”它,例如:
-
JDBC的SPI机制:JDBC驱动(如MySQL、Oracle)由Application ClassLoader加载,但DriverManager是由Bootstrap ClassLoader加载的。为了解决这个问题,Java 使用了线程上下文类加载器(Thread.currentThread().getContextClassLoader()),绕过了双亲委派机制。 -
OSGi模块化框架:OSGi实现了自己的类加载策略,允许同一个类被多个Bundle(模块)加载,互不影响 -
Tomcat自定义类加载器:Tomcat使用自己的WebAppClassLoader来加载每个Web应用的类,并打破了双亲委派,使得子应用优先加载自己的类,而不是父类加载器的类
打破方式
重写 ClassLoader.loadClass() 方法,不先调用 super.loadClass(),而是直接调用 findClass()
GC 与垃圾回收
Java中有哪些常用的垃圾回收器?
Serial GC
- 特点:单线程;使用复制算法(
Copying);STW(Stop the World)期间暂停所有用户线程 - 适用场景:客户端模式下运行的小型程序(如桌面应用)
- 启用参数:
-XX:+UseSerialGC
Serial Old GC
- 特点:
Serial GC的老年代版本;使用标记-整理算法(Mark-Compact) - 适用场景:与
Serial配合使用,或作为CMS后备方案 - 启用参数:
-XX:+UseSerialGC(自动启用Serial Old)
Parallel Scavenge GC
- 特点:多线程;吞吐量优先(
Throughput First);可控制最大停顿时间和吞吐量目标 - 适用场景:后台服务、批量处理、科学计算等对吞吐量敏感的应用
- 启用参数:
-XX:+UseParallelGC
Parallel Old GC
- 特点:
Parallel Scavenge的老年代版本 - 适用场景:多线程 + 标记-整理算法
- 启用参数:
-XX:+UseParallelOldGC
CMS(Concurrent Mark Sweep) GC
- 特点:
- 并发收集,低延迟(
Latency Sensitive) - 分为四个阶段:初始标记、并发标记、重新标记、并发清除
- 存在内存碎片问题
- 并发收集,低延迟(
- 缺点:内存占用较高;并发时失败会退化为
Serial Old,导致长时间STW - 适用场景:
Web服务、实时交易系统等要求响应快的场景 - 启用参数:
-XX:+UseConcMarkSweepGC(JDK 8及以前可用)
G1(Garbage First) GC
- 特点:面向服务端应用设计;将堆划分为多个大小相等的
region;可预测的时间停顿模型(Pause Prediction Model);支持并发和并行收集;自动压缩内存(避免碎片) - 优点:高吞吐 + 低延迟兼顾;适合大堆内存(如几
GB到几十GB) - 适用场景:大数据,微服务,分布式系统
- 启用参数:
-XX:+UseG1GC(JDK 7+,JDK 9默认)
ZGC(Z Garbage Collector)
- 特点:超低延迟(<
10ms);支持TB级别堆内存;并发执行大部分工作(包括标记、重定位、引用处理等) - 优点:几乎不暂停应用线程;高可伸缩性
- 适用场景:实时系统、大规模缓存服务、金融高频交易系统
- 启用参数:
-XX:+UseZGC(JDK 11+)
Shenandoah GC
- 特点:由
Red Hat主导开发;类似ZGC,强调低延迟;支持并发压缩,减少STW时间 - 优点:适合需要快速响应且堆较大的应用
- 适用场景:
Web服务、云原生服务、大数据分析 - 启用参数:
-XX:+UseShenandoahGC(JDK 12+)
对比表格总结
| 回收器 | 新生代/老年代 | 是否并发 | 延迟 | 吞吐 | 是否压缩 | 适用场景 |
|---|---|---|---|---|---|---|
Serial | 新生代 | 否 | 高 | 一般 | 是 | 小内存,客户端程序 |
Serial Old | 老年代 | 否 | 高 | 一般 | 是 | 小内存,CMS备用 |
Parallel Scavenge | 新生代 | 否 | 一般 | 高 | 否 | 吞吐优先 |
Parallel Old | 老年代 | 否 | 一般 | 高 | 是 | 吞吐优先 |
CMS | 老年代 | 是 | 低 | 一般 | 否 | 响应优先 |
G1 | 整体 | 是 | 较低 | 较高 | 是 | 通用,大堆 |
ZGC | 整体 | 是 | 很低 | 高 | 是 | 超低延迟,大堆 |
Shenandoah | 整体 | 是 | 很低 | 高 | 是 | 超低延迟,大堆 |
如何选择合适的垃圾回收器?
| 应用类型 | 推荐GC |
|---|---|
| 吞吐优先(如离线计算、批处理) | Parallel Scavenge + Parallel Old |
响应优先(如 Web 服务、API) | G1 GC / CMS(JDK 8) |
超低延迟(<10ms) | ZGC / Shenandoah |
| 小内存、嵌入式系统 | Serial GC |
| 微服务、容器化部署 | G1 GC / ZGC |
G1收集器的工作原理?
G1(Garbage-First)垃圾收集器 是 Java 中一种面向服务端应用的垃圾回收器,适用于大堆内存(几 GB到几十GB),目标是 在高吞吐的同时实现可预测的低延迟。它打破了传统分代模型(新生代 + 老年代),采用了一种 分区式(Region-based) 的内存管理方式。
核心设计思想
堆内存划分:Region 分区
G1将整个Java堆划分为多个大小相等的独立区域(Region),默认大小为1MB ~ 32MB(由堆大小决定)- 每个
Region可以是:Eden区、Survivor区、老年代(Old Region)、大对象区(Humongous Region)
注意:这种设计使得 G1 不再严格区分新生代和老年代,而是根据需要动态分配 Region 类型
并行与并发结合
G1是 多线程并行 + 并发收集器:- 并行(
Parallel):多个GC线程同时工作,提高效率 - 并发(
Concurrent):部分阶段可以与用户线程一起执行,降低STW(Stop-The-World)时间
- 并行(
可预测的停顿时间模型
- 用户可通过
-XX:MaxGCPauseMillis=N设置期望的最大GC停顿时间(如 200ms) G1会根据历史数据选择最优的Region回收集合(Collection Set,CSet),优先回收垃圾最多的Region
主要工作流程
初始标记
- 作用:标记从根节点(
GC Roots)直接可达的对象 - 特点:需要
Stop the World;时间非常短(通常<1ms)
并发标记
- 作用:从根节点开始遍历所有存活对象,进行可达性分析
- 特点:与用户线程并发运行;标记过程中可能有对象被修改(使用
SATB算法记录变化)
最终标记
- 作用:处理并发标记期间因程序运行而变动的对象引用
- 特点:需要
Stop the World;使用SATB(Snapshot-At-The-Begin)记录的变更进行修正
筛选回收:
- 作用:
- 对各个 region 的回收价值和成本排序(
Garbage First) - 选择一组
region组成Collection Set(CSet) - 将存活对象复制到新的空
region中 - 清除旧
region中的所有对象
- 对各个 region 的回收价值和成本排序(
- 特点:可能伴随着
Stop the World;实现了增量回收(Incremental Collection)
G1 的关键机制
RSet(Remembered Set)
- 每个
Region都有一个RSet,用于记录 外部指向该Region内对象的引用。 - 在并发标记或回收时,避免扫描整个堆来查找跨
Region引用。 - 提升
GC性能,但占用额外内存。
SATB(Snapshot-At-The-Beginning)
- 在并发标记开始前对堆做一个快照(
Snapshot) - 所有在并发标记期间被修改的对象都会被记录下来
- 最终标记阶段会对这些变化进行补偿处理,确保正确性
Humongous Region(巨型对象区)
- 用于存放大于等于
Region一半大小的对象 - 占用连续的多个
Region - 回收代价较高,建议尽量避免创建过大的对象
G1 的优势总结
| 特性 | 描述 |
|---|---|
| 低延迟 + 高吞吐兼顾 | 可预测停顿时间,适合对响应敏感的应用 |
| 分区式管理 | 更灵活地管理内存,支持大堆 |
| 并行与并发结合 | 多线程 + 并发标记,提升性能 |
| 压缩整理内存 | 减少内存碎片,避免 Full GC |
| 自适应回收策略 | 根据垃圾多少动态选择回收区域 |
- 如何判断一个对象是否可以被回收?
在 Java 中,判断一个对象是否可以被回收,主要依赖于 垃圾回收器(GC) 对对象的“可达性分析”(Reachability Analysis)。JVM 通过一系列称为 GC Roots 的根对象出发,沿着引用链向下查找,如果某个对象无法通过任何路径从 GC Roots 到达,则该对象被认为是不可达的,即可被回收的对象
常见的 GC Roots 类型
| 类型 | 实例 |
|---|---|
| 虚拟机栈中的局部变量表中的引用对象 | 方法中定义的 Object o = new Object() |
| 静态属性引用的对象 | public static String cache; |
常量引用对象(final static) | public static final String VERSION = "1.0"; |
本地方法(Native)引用的对象 | 如 JNI 引用的 ClassLoader, FileDescriptor 等 |
活跃线程(Thread)对象本身 | 正在运行的线程对象 |
CMS和G1的区别?
| 特性 | CMS | G1 |
|---|---|---|
| 主要目标适用年代 | 低延迟(Low Latency),适合响应敏感型应用 | 可预测的低延迟 + 高吞吐量,兼顾响应与吞吐 |
| 适用年代 | 老年代回收器 | 整体管理堆(新生代 + 老年代) |
| 堆结构 | 分代模型(新生代 + 老年代) | 分区模型(Region-based),不再严格分代 |
GC 算法 | 标记-清除(Mark-Sweep) | 标记-整理 + 复制算法 |
| 内存碎片问题 | 存在内存碎片 | 无内存碎片(回收时整理) |
| 并发阶段 | 支持并发标记,减少 STW 时间 | 并行 + 并发执行 |
| 吞吐量 | 相对较低 | 较高 |
| 适用堆大小 | 几百 MB 到 几 GB | 从几 MB 到几十 GB,甚至 TB 级 |
是否推荐使用(JDK9+) | 不再默认启用,逐步淘汰 | JDK9 默认 GC,持续优化中 |
Full GC 触发机制 | 并发失败或晋升失败会导致 Full GC | 回收效率高,Full GC 次数少 |
OOM 排查与调优
- 常见的
OOM类型有哪些?如何定位?
OOM类型 | 描述 | 常见原因 |
|---|---|---|
Java heap space | 堆内存溢出 | 对象创建过多且未释放,内存泄漏或堆设置过小 |
GC overhead limit exceeded | GC 时间占比过高 | 系统花大量时间进行垃圾回收,但回收效果差 |
PermGen space(JDK 7 及以前) | 永久代溢出 | 加载类过多(如动态代理、反射、JSP 编译等) |
Metaspace(JDK 8+) | 元空间溢出 | 类元数据加载过多,未正确卸载类加载器 |
unable to create new native thread | 无法创建新线程 | 线程数超过系统限制,或线程栈过大 |
Direct buffer memory | 直接内存溢出 | 使用 NIO 的 ByteBuffer.allocateDirect() 分配过多直接内存 |
OutOfMemoryError: Requested array size exceeds VM limit | 请求的数组大小超过虚拟机限制 | 创建超大数组(如 new int[Integer.MAX_VALUE]) |
Map failed | 内存映射失败 | 使用 mmap() 或内存映射文件时内存不足 |
如何定位和分析?
方法一:查看异常堆栈信息
当发生 OOM 时,JVM 会打印异常堆栈信息,包含类型和大致位置:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
...
注意:注意堆栈中的关键类和方法名,有助于判断是哪部分代码导致的问题。
方法二:生成并分析 Heap Dump 文件
- 启动参数添加自动生成
Heap Dump:-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/path/to/dump.hprof
- 使用工具分析
Heap Dump:MAT(Eclipse Memory Analyzer):- 查看最大对象占用
- 找出
GC Roots路径 - 分析内存泄漏路径
VisualVM/JProfiler/YourKit- 图形化展示内存快照
- 支持对比多个
dump文件
- Arthas(阿里开源)
方法三:使用 JVM 工具实时监控
jstat查看GC情况:jstat -gc <pid> 1000 10,观察EU,OU(Eden和Old区使用率),以及YGC,FGC频率。
jmap查看堆内对象分布:jmap -histo <pid> | head -n 20,显示前 20 个占用内存最多的类。
jconsole/VisualVM实时查看堆内存、线程、类加载情况
- 如何生成并分析
Head Dump?
生成 Head Dump 的方式
- JVM 启动参数自动生成
jmap -dump:format=b,file=<filename>.hprof <pid>
- 使用命令手动触发(运行中程序)
jcmd <pid> GC.heap_dump /path/to/heapdump.hprof
- 通过 Arthas 实时导出
# 连接目标进程
arthas-boot <pid>
# 导出 heap dump 文件
heapdump /data/dumps/heapdump.hprof
如何分析 Head Dump
MAT(Eclipse Memory Analyzer)
- 打开 MAT,点击 "
Open a Histogram" 或 "Open a Dominator Tree" - 查看占用内存最多的类
- 右键某个类 → "
Merge Shortest Path to GC Roots" → 分析引用链
VisualVM(JDK)
- 在终端输入
jvisualvm启动 - 加载
Heap Dump文件 - 查看
Summary、Classes、Instances等标签页 - 使用 "
GC Roots" 查看对象引用路径
JProfiler
YourKit
分析常用技巧
| 技巧 | 描述 |
|---|---|
Histogram 视图 | 查看每个类的实例数量和占用内存大小 |
Dominator Tree 视图 | 查看哪些对象占用了最多内存,并能追踪其引用来源 |
Leak Suspects 报告 | MAT 自动生成可疑内存泄漏报告 |
Path to GC Roots | 查看某个对象为什么没有被回收 |
Compare with Another Dump | 对比两个 dump 文件,查看对象增长趋势 |
常见内存泄漏场景分析
| 场景 | 表现 | 解决方法 |
|---|---|---|
| 集合类未释放 | HashMap、ArrayList 实例数异常增长 | 检查是否作为缓存、监听器等长期持有 |
ThreadLocal 泄漏 | ThreadLocalMap 占用大量内存 | 使用完后调用 remove() |
| 监听器未注销 | 如事件监听器、观察者模式对象不释放 | 检查注册逻辑,确保注销机制 |
ClassLoader 泄漏 | ClassLoader 实例持续增加 | 检查动态加载类是否卸载 |
| 缓存未清理 | 缓存对象持续增长 | 使用弱引用(WeakHashMap)、设置过期策略 |
JVM常用调优参数有哪些?
堆内存相关参数
| 参数 | 说明 | 实例 |
|---|---|---|
-Xms | 初始堆大小(默认物理内存的1/64) | -Xms512m |
-Xmx | 最大堆大小(默认物理内存的1/4) | -Xmx2g |
-XX:NewSize | 新生代初始大小 | -XX:NewSize=256m |
-XX:MaxNewSize | 新生代最大大小 | -XX:MaxNewSize=1g |
-XX:NewRatio | 老年代与新生代比例(默认 2) | -XX:NewRatio=3(老年代:新生代 = 3:1) |
-XX:SurvivorRatio | Eden 区与 Survivor 区比例(默认 8) | -XX:SurvivorRatio=4(Eden:Survivor = 4:1) |
推荐:设置 Xms == Xmx,避免堆动态伸缩带来的性能波动。
GC G1 相关参数
| 参数 | 说明 | 示例 |
|---|---|---|
-XX:+UseSerialGC | 使用 Serial GC(单线程,适合小内存) | - |
-XX:+UseParallelGC | 使用 Parallel Scavenge(多线程,吞吐优先) | |
-XX:+UseConcMarkSweepGC | 使用 CMS(低延迟,已废弃) | |
-XX:+UseG1GC | 使用 G1 GC(推荐,默认 JDK9+) | |
-XX:MaxGCPauseMillis | 设置目标最大停顿时间(毫秒) | -XX:MaxGCPauseMillis=20 |
-XX:G1HeapRegionSize | 设置 G1 Region 大小(1\~32MB) | -XX:G1HeapRegionSize=4M |
-XX:ParallelGCThreads | 并行 GC 线程数 | -XX:ParallelGCThreads=8 |
-XX:ConcGCThreads | 并发 GC 线程数 | -XX:ConcGCThreads=4 |
推荐:使用 G1 GC,兼顾低延迟和高吞吐。
元空间相关参数
| 参数 | 说明 | 示例 |
|---|---|---|
-XX:MetaspaceSize | 初始元空间大小 | -XX:MetaspaceSize=128m |
-XX:MaxMetaspaceSize | 元空间最大值(不设上限可能 OOM) | -XX:MaxMetaspaceSize=512m |
-XX:+UseCompressedClassPointers | 是否压缩类指针(节省内存) | 默认开启 |
-XX:CompressedClassSpaceSize | 压缩类指针空间大小 | -XX:CompressedClassSpaceSize=256m |
推荐:生产环境建议设置 MaxMetaspaceSize,防止元空间无限增长。
栈内存相关参数
| 参数 | 说明 | 示例 |
|---|---|---|
-Xss | 每个线程的栈大小(影响最大线程数) | -Xss512k |
-XX:ThreadStackSize | 同上(部分 JVM 可能使用该参数) | -XX:ThreadStackSize=512(单位 KB) |
注意:栈大小不宜过大,否则可能导致 unable to create new native thread
JVM 工具使用
- 你常用的
JVM监控和诊断工具有哪些?
jstat:实时查看GC状态、堆内存使用情况jmap:生成Heap Dump、查看对象内存分布jstack:查看线程堆栈信息,排查死锁、阻塞等问题jcm:执行多种JVM操作(如GC、Heap Dump)VisualVM:图形化监控JVM内存、线程、GC,支持插件扩展MAT:分析Heap Dump文件,定位内存泄漏Arthas:线上JVM实时诊断,支持类加载、方法追踪、线程分析等Prometheus+Grafana+JVM Exporter:生产环境可视化监控JVM指标(GC、内存、线程等)
- 如何使用
jstat、jmap、jstack?
jstat(查看JVM内存和GC状态)
# 查看 GC 情况,每 1 秒刷新一次
jstat -gc <pid> 1000
关键指标:
S0U/S1U:Survivor 使用
EU/OU:Eden 和 Old 区使用
YGC/FGC:Young 和 Full GC 次数与耗时
jmap(查看堆内存和生成dump)
# 查看堆内存概览
jmap -heap <pid>
# 查看对象数量分布(前 20)
jmap -histo <pid> | head -n 20
# 生成 Heap Dump 文件
jmap -dump:format=b,file=dump.hprof <pid>
jstack(查看线程堆栈)
# 导出所有线程堆栈
jstack <pid> > thread_dump.log
# 查看是否有死锁信息
jstack <pid> | grep -A 20 "deadlock"
3. Arthas 能做什么?举几个常用命令?
Arthas 是阿里巴巴开源的 Java 诊断工具,被誉为“Java 程序员的瑞士军刀”,能够在不修改代码、不停机的情况下,实时诊断和分析运行中的 Java 程序
- 实时查看
JVM状态:内存、线程、GC、系统属性等 - 方法执行监控:查看方法调用次数、耗时、入参、返回值
- 类加载信息:查看已加载类、反编译类文件
- 性能分析:方法耗时统计(
trace/profile) - 动态修改日志级别:无需重启即可调整
log4j/logback的日志级别 - 排查线上问题:死锁、
CPU飙高、内存泄漏、方法异常等
| 命令 | 用途 |
|---|---|
dashboard | 实时查看系统、JVM、线程、内存概览 |
thread | 查看所有线程状态,查找阻塞/死锁线程 thread -n 3 查看 CPU 使用最高的 3 个线程 |
jvm | 查看 JVM 参数、内存模型、类加载器等信息 |
sc / sm | 查找已加载类(sc * Controller)、方法(sm com.example.UserController.*) |
jad | 反编译类(如:jad com.example.UserService) |
watch | 监控方法参数、返回值、异常(如:watch com.example.UserService getUser "{params, returnObj}") |
trace | 方法内部调用链路和耗时分析(如:trace com.example.OrderService createOrder) |
heapdump | 导出堆快照用于内存分析(类似 jmap) |
logger | 查看和修改日志级别(如:logger -n ROOT -l DEBUG) |
profiler | CPU/内存采样,生成火焰图分析热点方法 |
字节码与执行
- 说说
Class文件的结构?
一个 Java Class 文件是编译后的二进制文件,结构清晰,主要包括以下部分(按顺序)
| 结构项 | 说明 |
|---|---|
魔数(Magic) | 固定值 0xCAFEBABE,标识这是一个 Java Class 文件 |
版本号(Version) | 包括次版本号和主版本号,如 52.0 表示 JDK 8 |
常量池(Constant Pool) | 存放字面量(如字符串、常量)和符号引用(如类名、方法名) |
访问标志(Access Flags) | 标识类的访问权限,如 public、abstract、final 等 |
| 类索引、父类索引、接口索引 | 指向常量池,描述当前类、父类、实现的接口 |
字段表集合(Fields) | 描述类中的变量(包括静态变量、实例变量) |
方法表集合(Methods) | 描述类中的方法(包括构造方法、静态方法、实例方法) |
属性表(Attributes) | 如 Code 属性:存放方法的字节码指令 |
javap -v *.class可查看Class文件的反编译结构。
javap工具的作用是什么?
javap 是 JDK 自带的一个 Java 反汇编工具,主要用于查看 .class 文件的 字节码结构和类信息。它可以帮助你理解程序底层实现、调试优化问题、分析第三方库等。
- 🛠️ 调试代码行为:查看是否真的执行了某段逻辑(如内联优化)
- 📈 性能优化:查看方法调用方式(虚方法 vs 静态绑定)
- 🔐 安全审计:检查敏感操作是否被正确编译
- 🧪 学习
JVM字节码:理解Java编译器如何将Java代码翻译为JVM指令
- 什么是
JIT编译?它对性能有什么影响?
JIT(Just-In-Time)编译 是 JVM 在程序运行时将字节码动态编译为本地机器码的技术,以提升执行效率。
Java程序最初以字节码运行在解释器上;JVM会监控方法的执行频率,热点代码(HotSpot) 会被JIT编译为高效的机器码;- 编译后的代码缓存在
CodeCache中,后续调用直接执行机器码。
对性能的影响
- ⬆️ 提升执行速度:本地机器码比解释执行快很多,尤其对频繁调用的方法
- 🚀 启动慢、运行快:初期解释执行较慢,热点方法被
JIT编译后性能大幅提升 - 🔁 延迟优化:可能延迟编译,优先执行解释模式,适合长期运行的服务
- 🧠 占用额外内存:编译后的机器码存放在
CodeCache,占用本地内存
推荐记忆推荐
画图法:
- 绘制
JVM内存结构图、类加载流程图、GC流程图。
口诀法:
- “
JVM五区域:堆栈方法区,程序计数器,本地方法栈” - “类加载五步走:加验准解初”
对比记忆:
- 对比
Serial、Parallel、CMS、G1四种垃圾收集器的特点 - 对比
PermGen和Metaspace
实战演练:
- 模拟
OOM场景并分析日志 - 使用
Arthas实时查看线程、内存、GC状态