CPU 占用过高排查
- 1. CPU占用过高排查思路
- 2. 代码
- 3. 运行程序
- 4. 先通过 top 命令找到消耗 cpu 很高的进程 id
- 5. 执行 top -p 27579 单独监控该进程
- 6. 在第 5 步的监控界面输入 H,获取当前进程下的所有线程信息
- 7. 找到消耗 cpu 特别高的线程编号,27581和27582
- 8. 将线程号 27581 转成 16 进制是 6bbd
- 9. 执行 jstack 27579|grep 6bbd 输出与线程 6bbd 相关的线程
- 10. 查看gc信息
- 11. 查看空间占用情况,jmap -histo 27579|head -20
- 12. 问题总结
- 13. 常见问题分析
- 14. 内存泄漏和内存溢出辨析
1. CPU占用过高排查思路
- Top命令分析进程
- 定位CPU占用高的进程
- 用jstack输出线程信息
- 定位占用CPU高的线程
- 解读线程信息,定位具体代码
2. 代码
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* VM参数: -XX:+PrintGC -Xms200M -Xmx200M
* GC调优---生产服务器推荐开启(默认是关闭的)
* -XX:+HeapDumpOnOutOfMemoryError
*/
public class FullGCProblem {
//线程池
private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
new ThreadPoolExecutor.DiscardOldestPolicy());
public static void main(String[] args) throws Exception {
//50个线程
executor.setMaximumPoolSize(50);
while (true){
calc();
Thread.sleep(100);
}
}
//多线程执行任务计算
private static void calc(){
List<UserInfo> taskList = getAllCardInfo();
taskList.forEach(userInfo -> {
executor.scheduleWithFixedDelay(() -> {
userInfo.user();
}, 2, 3, TimeUnit.SECONDS);
});
}
//模拟从数据库读取数据,返回
private static List<UserInfo> getAllCardInfo(){
List<UserInfo> taskList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
UserInfo userInfo = new UserInfo();
taskList.add(userInfo);
}
return taskList;
}
private static class UserInfo {
String name = "fisher";
int age = 18;
BigDecimal money = new BigDecimal(999999.99);
public void user() {
//
}
}
}
3. 运行程序
执行
# FullGC.jar里的FullGCProblem方法,如果不是src目录下,前面需要加包名,例如com.csdn.FullGCProblem
java -cp FullGC.jar -XX:+PrintGC -Xms200M -Xmx200M FullGCProblem
或者
java -jar -XX:+PrintGC -Xms200M -Xmx200M FullGC.jar
指定堆大小为200M,方便快速达到cpu占用100%。
4. 先通过 top 命令找到消耗 cpu 很高的进程 id
运行程序,cpu占用慢慢升高,当程序运行到10分钟左右后,cpu占用超过100%。
5. 执行 top -p 27579 单独监控该进程
6. 在第 5 步的监控界面输入 H,获取当前进程下的所有线程信息
7. 找到消耗 cpu 特别高的线程编号,27581和27582
8. 将线程号 27581 转成 16 进制是 6bbd
9. 执行 jstack 27579|grep 6bbd 输出与线程 6bbd 相关的线程
- 发现 GC 的线程占用过高,开启的参数中,有垃圾回收的日志显示,是垃圾回收的导致的cpu占用过高,不是业务线程导致的。
10. 查看gc信息
jstat -gc 27579 5000 20| awk '{print $13,$14,$15,$16,$17}'
- 出现大量的FullGC,JVM 中默认的垃圾回收器是多线程的,这里JVM疯狂的垃圾回收,导致 CPU 占用过高。
11. 查看空间占用情况,jmap -histo 27579|head -20
很多个70万个对象
12. 问题总结
- 一般来说,前面这几行,就可以看出,到底是哪些对象占用了内存。
- 这些对象回收不掉,导致了 FullGC,里面还有 OutOfMemory。
- 任务数多于线程数,那么任务会进入阻塞队列,就是一个队列,你进去,排队,有机会了,你就上来跑。
- 因为代码中任务数一直多于线程数,所以每 0.1S,就会有 50 个任务进入阻塞对象,50 个任务底下有对象,至少对象送进去了,但是没执行。 所以导致对象一直都在,同时还回收不了。
- 所以堆中,就会有对象 70 万个,阻塞队列中 70 万个任务,futureTask。并且这些对象还回收不了。
- 总结:
在 JVM 出现性能问题的时候。(表现上是 CPU100%,内存一直占用)
1、 如果 CPU 的 100%,要从两个角度出发,一个有可能是业务线程疯狂运行,比如说想很多死循环。还有一种可能性,就是 GC 线程在疯狂的回收,因 为 JVM 中垃圾回收器主流也是多线程的,所以很容易导致 CPU 的 100%
2、 在遇到内存溢出的问题的时候,一般情况下我们要查看系统中哪些对象占用得比较多,这里使用的是一个很简单的代码,在实际的业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因。
13. 常见问题分析
- 超大对象
代码中创建了很多大对象 , 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发 GC 甚至是 OOM - 超过预期访问量
通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。 比如如果一个系统高峰期的内存需求需要 2 个 G 的堆空间,但是堆空间设置比较小,导致内存不够,导致 JVM 发起频繁的 GC 甚至 OOM。 - 过多使用 Finalizer
过度使用终结器(Finalizer),对象没有立即被 GC,Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的 CPU 时间较少,因此它永远也赶不上主线程的步伐,程序消耗了所有的可用资源,最后抛出 OutOfMemoryError 异常。 - 内存泄漏
大量对象引用没有释放,JVM 无法对其自动回收。程序在申请内存后,无法释放已申请的内存空间。
- 长生命周期的对象持有短生命周期对象的引用
例如将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏 - 连接未关闭
如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象。 - 变量作用域不合理
例如,1.一个变量的定义的作用范围大于其使用范围,2.如果没有及时地把对象设置为 null - 内部类持有外部类
Java 的非静态内部类的这种创建方式,会隐式地持有外部类的引用,而且默认情况下这个引用是强引用,因此,如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏
如果内部类的生命周期长于外部类的生命周期,程序很容易就产生内存泄漏(垃圾回收器会回收掉外部类的实例,但由于内部类持有外部类的引用,导致垃圾回收器不能正常工作)
解决方法:你可以在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收。 - Hash 值改变
在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露。
示例
- 代码问题
代码问题和内存泄漏很大的关系,如果观察一个系统,每次进行 FullGC 发现堆空间回收的比例比较小,尤其是老年代,同时对象越来越多,这个时候可 以判断是有可能发生内存泄漏。
14. 内存泄漏和内存溢出辨析
内存溢出:实实在在的内存空间不足导致;
内存泄漏:该释放的对象没有释放,常见于使用容器保存元素的情况下。
如何避免:
内存溢出:检查代码以及设置足够的空间;
内存泄漏:一定是代码有问题。
很多情况下,内存溢出往往是内存泄漏造成的。