gc调优
常见的gc模式:
- 正常情况:gc后内存大部分可以被回收,整个gc监控图呈现锯齿状
- 内存泄漏:gc后只能回收部分内存,内存占用越来越多,可能存在内存泄漏,有部分对象无法被回收
- 持续的full gc:某个时间点产生多次full gc,cpu使用率飙高,用户请求基本无法处理,一段时间后恢复正常。原因可能是在该段时间内请求量激增。
gc调优的核心目标:gc占用时间少,也就是吞吐量高,程序不能有太长的延迟,也就是是STW的时间尽量短。
常见的调优措施:
- 把-Xms和-Xmx的值设置为相同的值,避免堆内存扩容
内存溢出
堆内存溢出
1、如果发生堆内存溢出,程序会打印错误日志,显示内存溢出发生的位置
2、在生产环境下,可以在程序启动时配置堆转储文件,在发生内存溢出时会自动生成jvm的快照文件,方便排查问题。在jvm启动时添加配置 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${目录},当发生内存溢出时,会在指定目录下会生成堆转储文件,这个文件是二进制文件,存储了程序的快照信息
使用VisualVM来分析堆转储文件:
将文件加载到VisualVM中,查看线程信息,可以看到发生内存溢出的位置,还可以查看发生内存溢出时程序中有哪一类对象过大:
堆内存溢出的处理方法:
- 加大内存:这是最简单的
- 优化代码,比如说不要一次性把太多数据读取到内存,如果预估数据量很大,分批处理。
- 分析是否有内存泄漏,这是最复杂的,随后内存泄漏章节中介绍。
栈内存溢出
通常,方法的无限递归会导致栈内存溢出,查看日志即可:
元空间内存溢出
元空间,就是存放方法区的地方,元空间是堆外内存,也就是操作系统的内存。元空间是可以动态扩展的,理论上,如果不是动态生成了太多的字节码,或者配置了最大元空间,是不会发生元空间内存溢出的。排查元空间内存溢出,需要检查代码是否有大量动态加载类或者反射操作生成过多类信息的情况。
总结
堆内存、栈内存、元空间,是jvm中3个可能会发生内存溢出的地方,堆内存溢出是最常见的,栈内存溢出通常是方法无限递归导致的,元空间溢出通常是动态加载了太多类导致的。
内存泄漏
内存泄漏是指程序在分配内存后,无法释放那些已经不再使用的内存,在Java中,如果不再使用一个对象,但是该对象依然在gc root的引用链上,这个对象就不会被垃圾回收器回收,这种情况称之为内存泄漏
什么样的代码会产生内存泄漏?
内存泄漏通常发生在一些长时间存活的对象上,比如静态集合、单例对象、长生命周期的回调等。
1、静态集合导致的内存泄漏:静态集合的生命周期和类的生命周期相同,而类的生命周期和JVM的生命周期相同,因为类通常是被应用类加载器加载的,而应用类加载器在jvm执行期间不会被回收。如果一直向集合中添加元素而没有对应的清理机制,就会导致内存泄漏。
2、资源没有被正确地关闭导致的内存泄漏:例如,数据库连接,如果使用完后没有关闭,数据库驱动会持有连接对象的引用,导致连接对象无法被回收
还有一些其他情况,要结合代码来看,总之,就是无用对象被gc root引用,导致无法释放内存。
什么样的情况可能是内存泄漏?
如果垃圾回收后的内存使用率依然很高,或者内存使用率持续走高,频繁触发gc,这种情况可能就是出现了内存泄漏。
内存泄漏该如何分析?
排查内存泄漏,通常是通过监控工具,生成堆转储文件,它是jvm在某一刻的快照,通过堆转储文件来分析有哪些对象是应该回收而没有回收的,或者有哪些对象占用内存较大,需要优化。不过要注意,堆转储文件只可以查看内存中有哪些对象,但是这些对象是在代码中的什么位置生成的,需要用户自己分析。
分析堆转储文件的工具,通常是使用visualVM或MAT。visualVM界面简洁,但是功能相较于MAT比较弱一点,MAT的功能更加强大一点,它可以生成内存泄漏检测报告,在报告中指出可疑对象。
使用visualVM分析堆转储文件
查看内存中有哪些大对象,查看这些对象的GC root,再根据GC root查看堆栈信息,分析出对象在代码中的什么位置被生成,尝试优化这些大对象
1、查看内存中的对象
2、分析这些对象是如何生成的:某些GC root对象可以选择它关联的线程对象
分析出大对象是如何生成的之后,尝试优化这些大对象。
程序运行过程中常见问题分析
CPU使用率飙升
在生产环境下,CPU的使用率达到突然飙升,需要定位生产问题。
定位方式:如果CPU占用过多是java程序引起的,通常需要如下几个步骤:
- top命令:定位到cpu使用率过高的进程ID
- top命令:top -Hp 进程id,定位到cpu使用率过高的线程
- jstack 进程id:查看线程的执行情况。根据上一步获取到的线程ID,和jstack命令的结果中的nid进行对比,注意,一个 是十进制,一个是十六进制,需要进行转换,定位到jvm中的线程id和代码
案例1:模拟CPU高负载的情况
代码:
public class CpuLoadTest {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(() -> {
while (true) {
}
});
pool.execute(() -> {
while (true) {
}
});
pool.execute(() -> {
while (true) {
}
});
}
}
排查问题的过程:
1、查询系统情况,执行top命令,查询程序运行时的CPU负载,可以看到,top命令可以定位到进程id
2、定位到具体的进程后,查看进程中线程的CPU使用情况:top -Hp 进程id,参数-H表示开启线程模式,-p指定进程id
3、使用jstack查看线程的堆栈信息: jstack 进程id
案例2:yield + 自旋
这是在网上找到的CPU使用率飙高的案例,博客地址 https://javabetter.cn/jvm/cpu-percent-100.html。yield方法用于让出cpu,使用yield + 自旋,可以充分压榨CPU,遇到的问题是,多个线程,线程数比CPU数多的情况下,多个线程都执行yield方法,但是执行完之后又没有计算任务,计算任务是从消息队列中获取的,导致线程又去执行yield方法,陷入死循环,所以CPU使用率飙高。最后得出的结论就是,在线程只有1个或比CPU个数小的情况下,可以使用yield + 自旋来提高性能,否则使用阻塞,避免CPU使用率飙升。
模拟实际情况:
public class CpuLoadTest2 {
private static final Object LOCK = new Object();
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
pool.execute(() -> {
while (true) {
synchronized (LOCK) {
Thread.yield();
// 执行业务逻辑
}
}
});
}
}
}
程序运行很长时间没有结果
有可能是发生了死锁。使用jstack命令排查线程的运行情况
垃圾回收后,内存占用仍然很高
可能是出现了内存泄漏
参考
- https://javabetter.cn/jvm/cpu-percent-100.html