一大坨知识-2-JVM篇
JVM内存划分
1. JVM运行时数据区域
堆、方法区(元空间)、虚拟机栈、本地方法栈、程序计数器

1.1 Heap(堆)
对象的实例以及数组的内存都在堆上进行分配,堆是线程共享的区域,用来存放对象实例,也是垃圾回收(GC)的主要区域。开启逃逸分析后,某些未逃逸的对象可以通过标量替换的方式在栈中分配。
堆细分:
- 新生代、老年代
- 新生代分为:Eden区、Survivor1区、Survivor2区
1.2 方法区(元空间)
JVM的方法区也称为永久区,存储已被Java虚拟机加载的类信息、常量、静态变量。JDK 1.8以后取消了方法区概念,称之为元空间(MetaSpace)。
当应用中的Java类过多时(如Spring等使用动态代理的框架生成很多类),如果占用空间超出设定值,会发生元空间溢出。
1.3 虚拟机栈
虚拟机栈是线程私有的,其生命周期与线程生命周期一致。每个方法执行时都会创建一个栈帧,栈帧中存放:
- 局部变量表
- 操作数栈
- 动态链接
- 返回地址
异常状况:
- 线程请求栈深度大于虚拟机允许深度:抛出 StackOverflowError
- 虚拟机栈动态扩展时无法申请足够内存:抛出 OutOfMemoryError
1.3.1 局部变量表
一组变量值存储空间,用来存放方法参数、方法内部定义的局部变量,底层是变量槽(variable slot)
1.3.2 操作数栈
记录方法执行过程中字节码指令向操作数栈进行入栈和出栈的过程。大小在编译时确定,方法开始执行时操作数栈为空,执行过程中各种字节码指令往操作数栈中入栈和出栈。
1.3.3 动态链接
字节码文件中的符号引用:
- 一部分在类加载的解析阶段或第一次使用时转化成直接引用(静态解析)
- 另一部分在运行期间转化为直接引用(动态链接)
1.3.4 返回地址(returnAddress)
指向字节码指令地址的类型。
1.4 JIT即时编译器
Just In Time Compiler,简称JIT编译器。
为了提高热点代码执行效率,在运行时将代码编译成与本地平台相关的机器码,并进行各种层次的优化(如锁粗化等)。
1.5 本地方法栈
本地方法栈和虚拟机栈类似,不同的是虚拟机栈服务Java方法,而本地方法栈服务Native方法。
在HotSpot虚拟机实现中,本地方法栈和虚拟机栈合二为一,同样会抛出StackOverflowError和OOM异常。
1.6 PC程序计数器
Program Counter,存放下一条指令位置的指针。是较小的内存空间,线程私有。
由于线程切换,CPU需要记住原线程下一条指令的位置,所以每个线程都需要自己的PC。
2. 堆内存分配策略

2.1 分配规则
对象优先在Eden区分配
- Eden区空间不足时,虚拟机执行Minor GC
- 存活对象进入Survivor的From区(From区内存不足时,直接进入Old区)
大对象直接进入老年代
- 需要大量连续内存空间的对象
- 目的是避免在Eden区和两个Survivor区之间发生大量内存拷贝
长期存活对象进入老年代
- 虚拟机为每个对象定义年龄(Age Count)计数器
- 每经过一次Minor GC,年龄加1
- 达到阈值(默认15次)时进入老年代
Full GC触发条件
- Minor GC或大对象进入老年代时,计算所需空间
- 如小于老年代剩余空间大小,执行Full GC
2.2 动态对象年龄判定
程序从年龄最小的对象开始累加,如果累加对象大小大于Survivor区的一半,则将当前对象的age作为新阈值,年龄大于此阈值的对象直接进入老年代。
3. 创建对象步骤
流程:类加载检查 → 分配内存 → 初始化零值 → 设置对象头 → 执行init方法
3.1 类加载检查
虚拟机遇到 new 指令时,首先检查常量池中是否能定位到类的符号引用,并检查该类是否已被加载、解析和初始化过。如未执行过,必须先执行相应的类加载过程。
3.2 分配内存
类加载检查通过后,虚拟机为新对象分配内存。分配方式有:
- 指针碰撞:当Java堆规整时使用
- 空闲列表:当Java堆不规整时使用
分配方式的选择由Java堆是否规整决定,而Java堆规整与否由垃圾收集器是否带有压缩整理功能决定。
3.3 初始化零值
内存分配完成后,虚拟机将分配到的内存空间初始化为零值。这一步保证了对象实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到字段数据类型对应的零值。
3.4 设置对象头
初始化零值完成后,虚拟机对对象进行必要设置:
- 类的实例信息
- 元数据信息查找方式
- 对象哈希码
- 对象GC分代年龄
- 对象头中根据虚拟机当前运行状态(如是否启用偏向锁)的不同设置
3.5 执行init方法
从虚拟机视角看,新对象已经产生,但从Java程序视角看,方法还未执行,所有字段都为零。除循环依赖外,执行new指令后会接着执行init方法,这样才产生真正可用的对象。
4. 对象引用类型
4.1 强引用(Strong Reference)
普通的对象引用关系就是强引用。如 Object obj = new Object(),只要强引用存在,垃圾回收器就不会回收被引用的对象。
4.2 软引用(Soft Reference)
用于维护可有可无的对象。只有在内存不足时,系统才会回收软引用对象。如果回收软引用对象后仍无足够内存,才抛出内存溢出异常。
4.3 弱引用(Weak Reference)
相比软引用更无用,拥有更短生命周期。当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
4.4 虚引用(Phantom Reference)
形同虚设的引用,在现实场景中使用不多,主要用来跟踪对象被垃圾回收的活动。
JVM类加载过程
加载流程:加载 → 验证 → 准备 → 解析 → 初始化

1. 加载阶段
- 通过类的全限定名获取定义此类的二进制字节流
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成代表这个类的java.lang.Class对象,作为方法区数据的访问入口
2. 验证阶段
验证字节码文件的安全性和正确性:
- 文件格式验证:是否符合Class文件格式规范,能否被当前版本虚拟机处理
- 元数据验证:字节码描述信息的语义分析,确保符合Java语言规范要求
- 字节码验证:保证方法运行时不会做出危害虚拟机安全的行为
- 符号引用验证:虚拟机将符号引用转化为直接引用时发生
3. 准备阶段
正式为类变量分配内存并设置类变量初始值的阶段,将对象初始化为零值。
4. 解析阶段
虚拟机将常量池内的符号引用替换为直接引用的过程。
常量池对比:
- 字符串常量池:位于堆上,默认class文件的静态常量池
- 运行时常量池:位于方法区,属于元空间
5. 初始化阶段
加载过程的最后一步,真正开始执行类中定义的Java程序代码。
6. 双亲委派机制
每个类都有对应的类加载器。系统中的ClassLoader协同工作时默认使用双亲委派模型:
- 系统首先判断当前类是否已被加载过
- 已加载的类直接返回,否则尝试加载
- 将请求委派给父类加载器的loadClass()处理
- 所有请求最终传送到顶层的启动类加载器BootstrapClassLoader
- 父类加载器无法处理时,才由自己处理
- 父类加载器为null时,使用BootstrapClassLoader作为父类加载器

6.1 使用好处
- 保证JDK核心类的优先加载
- 避免类的重复加载,确保Java程序稳定运行
- 保证Java核心API不被篡改
如果不使用双亲委派模型,每个类加载器加载自己的类,会导致如编写java.lang.Object类时出现多个不同的Object类。
6.2 破坏双亲委派机制
- 自定义类加载器,重写loadClass方法
- Tomcat加载自己目录下的class文件,不传递给父类加载器
- Java的SPI:BootstrapClassLoader已是最上层,直接获取AppClassLoader进行驱动加载
7. Tomcat类加载机制
Tomcat的WebAppClassLoader故意打破JVM双亲委派机制,加载步骤:
- 本地缓存检查:查找Tomcat是否已加载该类
- 系统类加载器缓存:从系统类加载器的cache中查找
- ExtClassLoader加载:绕开AppClassLoader,直接使用ExtClassLoader
- ExtClassLoader的ExtClassLoader → Bootstrap ClassLoader链保证核心类不被重复加载
- 如加载Object类:WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader
- findClass方法:BootstrapClassLoader加载失败时,调用自己的findClass方法
- AppClassLoader加载:加载失败时才使用AppClassLoader
- 异常抛出:所有加载器都失败时抛出异常
关键点:WebAppClassLoader绕过AppClassLoader,直接使用ExtClassLoader加载类。
JVM垃圾回收
1. 存活算法和两次标记过程
1.1 引用计数法
给对象添加引用计数器,引用时加1,失效时减1,计数器为0的对象不可用。
优点:实现简单,判定效率高
缺点:无法解决对象间相互循环引用问题,基本被抛弃
1.2 可达性分析法
以”GC Roots”(活动线程相关引用、虚拟机栈帧引用、静态变量引用、JNI引用)作为起始点,从这些节点开始向下搜索,搜索路径形成引用链。对象到GC Roots无引用链相连时证明对象不可用。
1.3 两次标记过程
对象被回收前,finalize()方法会被调用:
- 第一次标记:标记不在”关系网”中的对象
- 第二次标记:
- 对象未实现finalize():直接标记为可回收
- 对象实现finalize():放入队列,由低优先级线程执行
- 第二次小规模标记后,对象被真正回收
2. 垃圾回收算法
垃圾回收算法:复制算法、标记清除、标记整理、分代收集
2.1 复制算法(年轻代)
将内存分为大小相同的两块,每次使用其中一块。内存使用完后,将存活对象复制到另一块,清理使用过的空间。
优点:实现简单,内存效率高,不易产生碎片
缺点:内存压缩一半,存活对象多时效率大大降低
2.2 标记清除算法(CMS)
标记所有需要回收的对象,标记完成后统一回收。
缺点:效率低,产生大量不连续碎片,需预留空间处理浮动垃圾
2.3 标记整理算法(老年代)
标记过程同标记清除算法,让所有存活对象向一端移动,清理端边界外内存。
优点:解决了碎片问题
2.4 分代收集算法
根据各年代特点选择合适算法:
新生代:采用复制算法
- 每次垃圾回收回收大部分对象,存活对象少
- 划分为Eden空间和两个Survivor空间(From Space、To Space)
- 使用Eden和一个Survivor,回收时复制存活对象到另一个Survivor
老年代:对象存活率高,无额外分配担保空间
- 选择”标记清除”或”标记整理”算法
2.5 Safepoint
发生GC时,用户线程必须全部停下进行垃圾回收,此时JVM处于安全状态。如果有线程迟迟无法进入safepoint,整个JVM等待该阻塞线程,造成GC时间变长。

2.6 MinorGC、MajorGC、FullGC
MinorGC:年轻代空间不足时发生
MajorGC:老年代的GC,通常伴随MinorGC发生
FullGC触发条件:
- 老年代无法再分配内存
- 元空间不足
- 显式调用System.gc()
- CMS垃圾回收器出现promotion failure
2.7 分配策略
- 对象优先在Eden区分配:大多数对象在新生代Eden区分配,空间不足时发起Minor GC
- 大对象直接进入老年代:需要连续内存空间的对象(如长字符串、数组),避免在Eden区和Survivor区间大量内存复制
- 长期存活对象进入老年代:
- Eden区出生 → Survivor区
- 每次Minor GC年龄+1,达到阈值(默认15次)进入老年代
- 动态对象年龄判定:Survivor区相同年龄对象空间总和大于Survivor区一半时,该年龄及以上对象直接进入老年代
- 空间分配担保:Minor GC前检查老年代最大可用连续空间是否大于新生代所有对象空间总和
3. 垃圾收集器

3.1 JDK1.3-1.4: Serial、ParNew(关注效率)
3.1.1 Serial
单线程收集器,只使用一个CPU或一条线程完成垃圾收集,必须暂停所有工作线程。适合客户端应用。
3.1.2 ParNew
Serial的多线程版本,使用复制算法,除多线程外行为与Serial完全相同,同样需要暂停所有工作线程。
3.2 JDK1.5: Parallel Scavenge + Serial Old/Parallel Old(关注吞吐量)
3.2.1 Parallel Scavenge(新生代)
关注吞吐量(高效率利用CPU),主要用于后台运算任务,不需太多交互。
3.2.2 Serial Old(老年代)
Serial的老年代版本,单线程收集器,使用标记-整理算法:
- JDK1.5前与Parallel Scavenge搭配使用
- CMS收集器的后备垃圾收集方案
3.2.3 Parallel Old(老年代)
Parallel Scavenge的老年代版本,使用多线程和标记-整理算法。
3.3 JDK1.8: CMS(关注最短垃圾回收停顿时间)
CMS是年老代垃圾收集器,目标获取最短垃圾回收停顿时间,使用多线程标记-清除算法。
3.3.1 工作阶段
- 初始标记:标记GC Roots直接关联的对象(STW,速度快)
- 并发标记:ReferenceChains跟踪过程,与用户线程并发工作
- 重新标记:修正并发标记期间标记变动(STW)
- 并发清除:清除GC Roots不可达对象,与用户线程并发工作
优点:并发收集、低停顿
缺点:对CPU敏感;无法处理浮动垃圾;产生大量空间碎片
3.4 JDK9+: G1(精准控制停顿时间,避免垃圾碎片)
面向服务器的垃圾收集器,针对多处理器及大容量内存机器。
3.4.1 核心改进
- 基于标记-整理算法,不产生内存碎片
- 精确控制停顿时间,低停顿垃圾回收
3.4.2 区域化回收
将堆内存划分为大小固定的独立区域,维护优先级列表,优先回收垃圾最多的区域。
3.4.3 工作阶段
- 初始标记:STW,使用一条初始标记线程
- 并发标记:与用户线程并发执行可达性分析
- 最终标记:STW,多条标记线程并发执行
- 筛选回收:STW,多条筛选回收线程并发执行
3.5 JDK11+: ZGC(超大堆内存,最小停顿时间)
3.5.1 技术特点
- 着色指针技术: 加快标记过程
- 读屏障: 解决GC和应用并发导致的STW问题
3.5.2 性能指标
- 支持TB级堆内存(最大4T,JDK13最大16TB)
- 最大GC停顿10ms
- 对吞吐量影响不超过15%
4. 配置垃圾收集器
- 设置内存区域上限避免溢出问题(如元空间)
- 堆空间设置为操作系统的2/3,超过8GB优先选用G1
- 根据老年代对象提升速度调整年轻代和老年代比例
- 根据系统容量、访问延迟、吞吐量进行专项优化
- 记录详细GC日志,使用GCeasy等工具定位问题
5. JVM性能调优
对应进程的JVM状态以定位问题和解决问题并作出相应的优化。
常用命令: jps、jinfo、jstat、jstack、jmap
5.1 jps:查看java进程及相关信息
jps -l # 输出jar包路径,类全名
jps -m # 输出main参数
jps -v # 输出JVM参数5.2 jinfo:查看JVM参数
jinfo 11666
jinfo -flags 11666
# 参数:Xmx、Xms、Xmn、MetaspaceSize5.3 jstat:查看JVM运行时的状态信息,包括内存状态、垃圾回收
jstat [option] LVMID [interval] [count]
# 参数说明:
# -gc 垃圾回收堆的行为统计
# -gccapacity 各个垃圾回收代容量和空间统计
# -gcutil 垃圾回收统计概述
# -gcnew 新生代行为统计
# -gcold 年老代和永生代行为统计5.4 jstack:查看JVM线程快照,定位线程卡顿原因
jstack [-l] <pid>
# 参数说明:
# -F 强制输出线程堆栈
# -m 同时输出java和本地堆栈
# -l 额外显示锁信息5.5 jmap:查看内存信息
jmap [option] <pid>
# 参数说明:
# -heap 打印java heap摘要
# -dump:<options> 生成java堆的dump文件6. JDK新特性
6.1 JDK8
- 支持Lambda表达式
- 集合的stream操作
- 提升HashMap性能
6.2 JDK9
// Stream API中iterate方法的新重载方法
IntStream.iterate(1, i -> i < 100, i -> i + 1)
.forEach(System.out::println);- 默认G1垃圾回收器
6.3 JDK10
- 完全GC并行改善G1最坏情况等待时间
6.4 JDK11
- ZGC(并发回收策略)4TB
- Lambda参数的局部变量语法
6.5 JDK12
- Shenandoah GC(GC算法)停顿时间与堆大小无关,并行关注停顿响应时间
6.6 JDK13
- ZGC将未使用的堆内存返回给操作系统,16TB
6.7 JDK14
- 删除CMS垃圾回收器
- 弃用ParallelScavenge+SerialOldGC垃圾回收算法组合
- ZGC应用到macOS和windows平台
线上故障排查
1. 硬件故障排查
如果实例出现问题,根据情况决定是否重启。出现CPU、内存飙高或OOM异常时:
故障处理步骤:隔离 → 保留现场 → 问题排查
1.1 隔离
将机器从请求列表中摘除,如设置nginx权重为0。
1.2 现场保留
瞬时态和历史态
查看CPU、系统内存等,通过历史状态体现趋势性问题,依靠监控系统协作获取信息。
1.3 保留信息
(1)系统当前网络连接
ss -antp > $DUMP_DIR/ss.dump 2>&1使用ss命令而非netstat,因为netstat在网络连接多时执行缓慢。可排查TIME_WAIT或CLOSE_WAIT等连接问题。
(2)网络状态统计
netstat -s > $DUMP_DIR/netstat-s.dump 2>&1
sar -n DEV 1 2 > $DUMP_DIR/sar-traffic.dump 2>&1按协议统计输出,把握网络状态。高速模块(Redis、Kafka)容易跑满网卡,表现为网络通信缓慢。
(3)进程资源
lsof -p $PID > $DUMP_DIR/lsof-$PID.dump查看进程打开的文件、资源使用情况、网络连接等。命令输出较慢时需耐心等待。
(4)CPU资源
mpstat > $DUMP_DIR/mpstat.dump 2>&1
vmstat 1 3 > $DUMP_DIR/vmstat.dump 2>&1
sar -p ALL > $DUMP_DIR/sar-cpu.dump 2>&1
uptime > $DUMP_DIR/uptime.dump 2>&1输出系统CPU和负载,便于事后排查。
(5)I/O资源
iostat -x > $DUMP_DIR/iostat.dump 2>&1输出磁盘基本性能信息,排查I/O问题。日志输出过多或磁盘问题时会异常。
(6)内存问题
free -h > $DUMP_DIR/free.dump 2>&1展现操作系统内存概况,排查SWAP影响GC、SLAB区挤占JVM内存等问题。
(7)其他全局信息
ps -ef > $DUMP_DIR/ps.dump 2>&1
dmesg > $DUMP_DIR/dmesg.dump 2>&1
sysctl -a > $DUMP_DIR/sysctl.dump 2>&1dmesg包含死掉服务的最后线索,ps影响系统和JVM的内核配置参数。
(8)进程快照(jinfo)
jinfo $PID > $DUMP_DIR/jinfo.dump 2>&1输出Java基本进程信息,包括环境变量和参数配置,查看配置错误造成的JVM问题。
(9)dump堆信息
jstat -gcutil $PID > $DUMP_DIR/jstat-gcutil.dump 2>&1
jstat -gccapacity $PID > $DUMP_DIR/jstat-gccapacity.dump 2>&1输出当前GC信息,大体判断问题,必要时借助jmap分析。
(10)堆信息
jmap $PID > $DUMP_DIR/jmap.dump 2>&1
jmap -heap $PID > $DUMP_DIR/jmap-heap.dump 2>&1
jmap -histo $PID > $DUMP_DIR/jmap-histo.dump 2>&1
jmap -dump:format=b,file=$DUMP_DIR/heap.bin $PID > /dev/null 2>&1获取Java进程dump信息,第4个命令最有价值但文件大,需下载导入MAT等工具深入分析。
(11)JVM执行栈
jstack $PID > $DUMP_DIR/jstack.dump 2>&1
top -Hp $PID -b -n 1 -c > $DUMP_DIR/top-$PID.dump 2>&1获取执行栈信息,还原Java进程线程情况,获取进程中所有线程的CPU信息。
(12)高级替补方案
kill -3 $PID # jstack的替补方案
gcore -o $DUMP_DIR/core $PID # 生成core文件
jhsdb jmap --exe ${JDK}java --core $DUMP_DIR/core --binaryheap # 处理jmap问题1.4 内存泄漏现象
JDK9后jmap被jhsdb取代:
jhsdb jmap --heap --pid 37340
jhsdb jmap --pid 37288
jhsdb jmap --histo --pid 37340
jhsdb jmap --binaryheap --pid 37340内存溢出表现为Old区占用持续上升,多轮GC无改善。ThreadLocal中的GC Roots,内存泄漏根本原因是对象未切断与GC Roots的联系。
2. 报表异常 | JVM调优
2.1 问题背景
报表系统频繁发生内存溢出,高峰期频繁拒绝服务。结果集字段不全,通过HttpClient循环调用其他服务接口填充数据,使用Guava做JVM内缓存但响应时间长。
2.2 初步排查
JVM资源不足:
- 报表计算涉及几百兆内存,驻留时间长
- 计算耗CPU,分配3GB内存不足,多人访问发生OOM
- 升级为4C8G机器,JVM分配6GB内存,OOM消失
- 但带来频繁GC问题和超长GC时间(平均5秒)
2.3 进一步优化
报表系统对象存活时间长,不能仅通过增加年轻代解决;增加年轻代减少老年代,CMS碎片和浮动垃圾问题导致可用空间减少。
2.3.1 第一步:调整MaxTenuringThreshold
通过分析GC日志对象年龄分布,调整参数到3。让年轻代对象尽快晋升老年代,减少MinorGC次数。
2.3.2 第二步:启用CMSScavengeBeforeRemark
CMS remark前先执行Minor GC清空新生代,配合上一步参数,对象快速晋升老年代,年轻代对象有限,MajorGC时间占用有限。
2.3.3 第三步:启用ParallelRefProcEnabled
大量弱引用GC处理时间长(10秒GC中4.5秒处理weak refs),并行处理Reference加快处理速度。
2.4 再次升级机器
效果不明显,改用8core16g机器,但带来新问题:
- 高性能机器带来大服务吞吐量
- 年轻代分配速率明显提高
- MinorGC时长不可控(有时超过1秒)
- 堆空间加大造成回收时间加长
2.5 改用G1垃圾回收器
- 目标停顿时间200ms
- G1适合大堆内存应用,简化调优工作
- 通过初始堆空间、最大堆空间、最大GC暂停目标获得不错性能
- GC更频繁但停顿时间小,应用运行平滑
2.6 代码优化
要求支持未来两年业务10到100倍增长,使用MAT分析堆快照优化代码:
- 查询优化:select *全量排查,只允许获取必须数据
- 缓存优化:Guava Cache命中率不高,改为弱引用(WeakKeys)
- 请求优化:限制导入文件大小,拆分超大范围查询导出请求
一系列优化后,性能提升数倍。
3. 大屏异常 | JUC调优
3.1 问题场景

数据通过HttpClient获取补全,部分服务提供商响应时间长,造成服务整体阻塞。
3.2 问题分析
- 接口A:HttpClient访问服务2,响应100ms
- 接口B:HttpClient访问服务3,耗时2秒
- HttpClient有最大连接数限制,服务3迟迟不返回导致连接数达上限
- 总结:同一服务中,一个耗时长的接口导致整体服务不可用
3.3 现象分析
jstack栈信息显示大多数线程阻塞在接口A而非接口B,具有迷惑性。原因是接口A速度快,问题发生点进入更多请求,全部阻塞并被打印。
3.4 问题验证
搭建demo工程模拟两个使用同一HttpClient的接口:
- fast接口:访问百度,快速返回
- slow接口:访问谷歌,阻塞超时约10秒
使用ab压测+jstack dump堆栈:
- 过滤nio关键字,发现200个tomcat相关线程(Spring Boot默认maxThreads)
- 大多数线程处于BLOCKED状态,说明线程等待资源超时
- 200个中有150个是blocked的fast进程
3.5 解决方案
- fast和slow争抢连接资源,通过线程池限流或熔断处理
- slow线程不总是slow,加入监控
- 使用CountDownLatch控制线程执行顺序逻辑
4. 接口延迟 | SWAP调优
4.1 问题现象
服务实例经常卡顿,高并发情况下每停顿1秒,几万用户请求感到延迟。
4.2 问题排查
对比其他实例CPU、内存、网络、I/O资源,区别不大,怀疑硬件问题。
对比GC日志发现:
- Minor GC和Major GC时间比其他实例长得多
- GC发生时,vmstat的si、so飙升严重
- free命令确认SWAP分区使用比例非常高
4.3 根本原因
详细分析/proc/meminfo内存分布,slabtop命令显示异常,dentry(目录高速缓冲)占用非常高。
问题定位:运维工程师删除日志时执行命令:
find / | grep "xxx.log"寻找删除日志文件位置,老服务器文件太多,扫描后文件信息缓存到slab区。服务器开启swap,物理内存占满后未立即释放cache,导致每次GC都要和硬盘交互。
4.4 解决方案
关闭SWAP分区
swap是性能场景的万恶之源,建议禁用。高并发场景下SWAP让人体验其”魔鬼性”一面:进程不死,但GC时间长让人无法忍受。
5. 内存溢出 | Cache调优
5.1 案例分析
线上故障,重新启动后使用jstat发现Old区持续增长。使用jmap导出堆栈,MAT分析发现巨大HashMap对象(同事做缓存用),无界缓存无超时或LRU策略,未重写key的hashCode和equals方法,对象无法取出导致堆内存持续上升。改用Guava Cache并设置弱引用后故障消失。
文件处理器应用读取写入文件异常时,close方法未放在finally块,造成文件句柄泄漏,产生严重内存泄漏。
5.2 概念区分
- 内存溢出:结果,内存空间不足、配置错误等因素
- 内存泄漏:原因,错误编程方式导致对象未被回收、未及时切断与GC Roots的联系
5.3 常见内存泄漏示例
5.3.1 HashMap缓存泄漏
HashMap做缓存但无超时或LRU策略,造成数据越来越多。
5.3.2 自定义对象Key泄漏
未重写Key类的hashCode和equals方法,造成放入HashMap的所有对象都无法取出,与外界失联。
// leak example
import java.util.HashMap;
import java.util.Map;
public class HashMapLeakDemo {
public static class Key {
String title;
public Key(String title) {
this.title = title;
}
}
public static void main(String[] args) {
Map<Key, Integer> map = new HashMap<>();
map.put(new Key("1"), 1);
map.put(new Key("2"), 2);
map.put(new Key("3"), 2);
Integer integer = map.get(new Key("2"));
System.out.println(integer); // 输出:null
}
}即使提供equals和hashCode方法,也要小心,尽量避免使用自定义对象作为Key。
5.3.3 文件句柄泄漏
文件处理应用读取写入异常时,close方法未放在finally块,造成文件句柄泄漏。
6. CPU飙高 | 死循环
6.1 问题现象
线上应用运行一段时间后CPU飙升,怀疑业务逻辑计算量大或触发死循环(如HashMap高并发死循环),但排查后发现是GC问题。
6.2 排查步骤
查找CPU最多进程
top # 使用Shift+P按CPU使用率排序,记录pid查找CPU最多线程
top -Hp $PID # 记录线程ID转换线程ID进制
printf %x $tid # 十进制转十六进制查看线程栈
jstack $pid >$pid.log查找问题线程
less $pid.log # 查找十六进制tid的线程上下文
6.3 问题根源
jstack日志搜索DEAD关键字找到CPU使用最多的线程id,问题是堆已满但未发生OOM,GC进程持续回收但效果一般,造成CPU升高应用假死。
需要dump内存,使用MAT等工具进一步分析具体原因。

