一、内存溢出
俗称 OOM,是指当程序请求分配内存时,由于没有足够的内存空间,从而抛出 OutOfMemoryError。
List<String> list = new ArrayList<>();
while (true) {
list.add("OutOfMemory".repeat(1000)); // 无限增加内存
}
可能是因为堆、元空间、栈或直接内存不足导致的。可以通过优化内存配置、减少对象分配来解决。
二、内存泄漏
是指程序在使用完内存后,未能及时释放,导致占用的内存无法再被使用。随着时间的推移,内存泄漏会导致可用内存逐渐减少,最终导致内存溢出。
内存泄漏通常是因为长期存活的对象持有短期存活对象的引用,又没有及时释放,从而导致短期存活对象无法被回收而导致的。
class MemoryLeakExample {
private static List<Object> staticList = new ArrayList<>();
public void addObject() {
staticList.add(new Object()); // 对象不会被回收
}
}
三、内存泄漏原因
①、静态的集合中添加的对象越来越多,但却没有及时清理;静态变量的生命周期与应用程序相同,如果静态变量持有对象的引用,这些对象将无法被 GC 回收。
②、单例模式下对象持有的外部引用无法及时释放;单例对象在整个应用程序的生命周期中存活,如果单例对象持有其他对象的引用,这些对象将无法被回收。
③、数据库、IO、Socket 等连接资源没有及时关闭;
④、 ThreadLocal 的引用未被清理,线程退出后仍然持有对象引用;在线程执行完后,要调用 ThreadLocal 的 remove 方法进行清理。
四、内存泄漏排查
1、使用 jps -l
查看运行的 Java 进程 ID。
2、使用top -p [pid]
查看进程使用 CPU 和内存占用情况。
3、使用 top -Hp [pid]
查看进程下的所有线程占用 CPU 和内存情况。
4、抓取线程栈:jstack -F 666 > 666.txt
,可以多抓几次做个对比。
666 为 pid,顺带作为文件名。
看看有没有线程死锁、死循环或长时间等待这些问题。
5、可以使用jstat -gcutil [pid] 5000 10
每隔 5 秒输出 GC 信息,输出 10 次,查看 YGC 和 Full GC 次数。
通常会出现 YGC 不增加或增加缓慢,而 Full GC 增加很快。
或使用 jstat -gccause [pid] 5000
输出 GC 摘要信息。
或使用 jmap -heap [pid]
查看堆的摘要信息,关注老年代内存使用是否达到阀值,若达到阀值就会执行 Full GC。
如果发现 Full GC
次数太多,就很大概率存在内存泄漏了。
6、生成 dump
文件,然后借助可视化工具分析哪个对象非常多,基本就能定位到问题根源了。
执行命令 jmap -dump:format=b,file=heap.hprof 666
会输出进程 666的堆快照信息,保存到文件 heap.hprof 中。
7、使用图形化工具分析,如 JDK 自带的 VisualVM,商业软件Jprofile、mat等等
然后在结果观察内存占用最多的对象,找到内存泄漏的源头。
五、内存溢出问题排查
使用 jmap 命令手动生成 Heap Dump 文件:
jmap -dump:format=b,file=heap.hprof <pid>
然后使用 MAT、JProfiler 等工具进行分析,查看内存中的对象占用情况。
六、栈溢出
栈溢出发生在程序调用栈的深度超过 JVM 允许的最大深度时。
栈溢出的本质是因为线程的栈空间不足,导致无法再为新的栈帧分配内存。
当一个方法被调用时,JVM 会在栈中分配一个栈帧,用于存储该方法的执行信息。如果方法调用嵌套太深,栈帧不断压入栈中,最终会导致栈空间耗尽,抛出 StackOverflowError。
栈溢出场景
递归调用,尤其是没有正确的终止条件下,会导致递归无限进行。
如果方法中定义了特别大的局部变量,栈帧会变得很大,导致栈空间更容易耗尽。
评论区