Java web应用性能分析之【java进程问题分析概叙】-CSDN博客
Java web应用性能分析之【java进程问题分析工具】-CSDN博客
Java web应用性能分析之【jvisualvm远程连接云服务器】-CSDN博客
由于篇幅限制、前面三篇讲了准备工作和分析小结,这里将详细操作java进程问题分析定位。
1.java进程常见问题汇总
Java Web应用中的java进程问题从系统表象来看归结起来总共有四方面:CPU、内存、磁盘、网络。具体包括:
CPU问题:CPU 使用率峰值突然飚高问题(cpu占用率、ws上下文切换频繁、load avg飙高),死循环或者循环嵌套,锁等待问题、死锁问题,GC问题、线程池大小配置是否合理等等。
内存问题:物理内存不足、溢出 (泄露)、OOM(堆、栈、元空间、线程)、找jvm中的大对象、jvm配置是否合理(堆大小是否合适、元空间大小是否合适、NewRatio和SurvivorRatio是否合适、线程栈大小是否合适、如何选择GC收集器、等等jvm调优问题)
磁盘问题:磁盘空间不足或者满了、log4j日志写磁盘慢阻塞业务请求、物理磁盘性能差、操作系统的io调度算法(elevator=deadline,这个算法试图把每次请求的延迟降至最低。该算法重排了请求的顺序来提高性能);
网络问题:网络流量异常、网络攻击、服务器带宽不足、网络io(零拷贝,dubbo支持零拷贝)、web服务器io模型(Nginx、Tomcat等)。
业务问题:业务高并发带来的PV 量过高、服务调用耗时异常、线程死锁、多线程并发问题、频繁进行 Full GC等问题;业务逻辑问题(具体场景具体分析)、如何定位某个方法耗时很久,如何统计业务逻辑内每个环节(方法)的耗时。
备注:上面把问题分成5个方面,其实这些问题之间都是相互关联的,在我们分析问题时不应该将其独立对待,简单的归类分析。比如jvm的堆内存不足,会导致频繁的fullGC,此时的服务器表象可能是cpu飙高,甚至大于100%;磁盘io性能不足,阻塞cpu,也可能导致cpu繁忙。
2.问题分析定位一般操作步骤
一般我们的问题都来自监控的报警,cpu、内存、磁盘、业务服务接口耗时等等超过了监控平台的阈值,就会有短信、邮件发给相关责任人。当然这个时候一般就比较严重了,因为已经是生产问题,可能涉及到你的kpi和白花花的银子。而且在生产环境排查问题,比较麻烦,很有可能你连登录服务器的权限都没有。所以尽量在开发环境、测试环境,提前发现和解决问题。
在开发环境、测试环境一般没有生产环境的数据,尽量模拟产生大量数据的生产环境。还有一个就是通过ab等压测工具模拟高并发,尽量还原生产环境的现场。针对环境,一般分成离线分析和在线分析。
压测工具参考:Java web应用性能分析之【压测工具ab】-CSDN博客
Java web应用性能分析之【高并发之缓存-多级缓存】-CSDN博客
Java web应用性能分析之【高并发之限流】_web服务限流-CSDN博客
Java web应用性能分析之【高并发之降级】-CSDN博客
2.1离线分析
1.查看日志文件:检查java应用程序的日志文件,通常位于/logs目录下,或者由日志配置文件指定。一般可以通过启动springboot进程的bash脚本来查看具体日志文件路径。
2.分析堆栈跟踪:如果应用程序崩溃,在日志文件中查找异常堆栈跟踪。如oom后的内存dump文件,在java进程启动时添加参数,确保发生oom时保存现场 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=log/
分析内存使用情况:使用诸如JVisualVM, JProfiler, 或MAT (Memory Analyzer Tool) 等工具分析内存使用情况和潜在的内存泄漏。
分析线程活动:使用线程分析工具(如JVisualVM, YourKit, 或MAT)来查看线程的状态和死锁。
3.分析JVM参数:如果可用,分析启动Spring Boot应用程序时使用的JVM参数。一般启动springboot进程的bash脚本,在bash脚本中可以查看jvm参数设置,如果时查询线上的 则可以用ps -ef|grep java 或者 jinfo pid来查看。
4.分析系统属性和环境变量:查看系统属性和环境变量,这些可能影响应用程序的行为。
5.分析配置文件:检查application.properties或application.yml配置文件,查看是否有不正确的配置。
6.分析GC日志:如果有GC日志,分析垃圾收集器的行为和内存回收情况。要求jvm启动参数配置-XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintHeapAtGC -XX:+PrintCommandLineFlags -Xloggc:log/gc.log 将gc日志单独保存出来。分析jvm的堆栈元空间大小是否合适?结合分析堆栈跟踪文件,定位oom、死锁触发的点,再去调整代码。
7.历史监控:如发生问题时间段的zabbix、prometheus等等,通过监控分析这段时间的cpu、内存、io、网络、qps统计、耗时统计。
8.分析应用的外部依赖:如数据库、Redis缓存、消息队列KFK、网络服务等等,确认是否有问题。
综合上述情况,再分析定位问题。
2.2 在线分析
再次申明“在线分析”这个只针对开发环境和测试环境,如果是生产环境,出了问题概不负责。在线分析一般步骤如下:通过top命令找出资源占用高的java进程,然后通过top -Hp或者pidstat -p 定位高消耗的线程,再通过jstack或者jmap 导出堆栈信息,最后用分析工具(如JVisualVM, YourKit, 或MAT)来分析线程的状态和死锁,以及分析内存使用情况和潜在的内存泄漏,更加这些问题触发点,再去调整代码,然后再接着更新代码验证问题是否解决。
当然,在已知要分析功能服务的情况下,可以直接用JVisualVM远程连接该进程,查看具体的资源消耗情况,这个图形工具更直观。
JVisualVM参考:Java web应用性能分析之【jvisualvm远程连接云服务器】-CSDN博客
ab压测,模拟并发环境,参考:Java web应用性能分析之【压测工具ab】-CSDN博客
步骤1.找出耗时线程 ,18281,转成16进制 printf “%x\n” 18281
pidstat -p 18227 -t 1 10 (和top比,pidstat是留痕的,每次打印都是能看到,top是实时刷新;但是top可以选择按照cpu或者内存来排序)
步骤2.定位线程在干嘛 jstack 18227 |grep '4769' -C5 --color
步骤3.查看代码“at com.zxx.study.web.task.FullCPUTask.run(FullCPUTask.java:15)”,找到触发点,分析原因,再进行修正,而且更新代码,验证问题是否修复。
2.用JVisualVM来监控分析,就简单多了,可以很清晰看到cpu和内存消耗情况
抽样器中很直接就能找到占用cpu时间较多的线程
很容易找到触发点,和上面的jstack找的位置一样“at com.zxx.study.web.task.FullCPUTask.run(FullCPUTask.java:15)”,找到触发点,分析原因,再进行修正,而且更新代码,验证问题是否修复。
3.用arthas来分析定位,稍后补充。
2.CPU问题
CPU问题:CPU 使用率峰值突然飚高问题(cpu占用率、ws上下文切换频繁、load avg飙高),死循环或者循环嵌套,锁等待问题、死锁问题,GC问题、线程池大小配置是否合理等等。
cpu飙升带来的影响
响应时间延长:CPU使用率过高会导致进程无法及时处理请求,从而导致Web应用的响应时间延长,影响用户体验。
性能下降:高CPU使用率会消耗系统资源,导致其他进程或服务的性能下降,可能引起整体系统的不稳定性。
可用性降低:CPU使用率过高持续一段时间,可能导致Web应用崩溃或无法正常运行,从而降低系统可用性。
java进程导致cpu飙升的原因和解决方法:
Java进程导致CPU使用率飙升的原因可能有多种,以下是一些常见原因及其解决方法:
大量并发请求:大量并发请求时,可能会导致CPU使用率升高,特别是在处理复杂的请求或计算密集型任务时。
解决方法:架构优化、引入负载均衡
无限循环或长时间执行的代码:检查是否有死循环或者复杂的算法导致CPU长时间被占用。
解决方法:优化代码逻辑,确保循环有明确的退出条件。
过多的线程竞争:如果有多个线程竞争同一资源或者执行同步块,可能会导致CPU使用率升高。
解决方法:减少线程数量,优化同步策略,使用更高效的并发工具。
线程死锁:竞争资源:多个线程同时竞争有限的资源,当资源分配和释放不当时,可能会导致死锁;循环等待:线程之间相互等待对方释放资源,形成循环等待的局面。
解决方法:重新定义竞争的资源,调整循环等待。
内存泄漏:如果Java进程中存在内存泄漏,可能会导致GC频繁运行,消耗大量CPU资源。
解决方法:使用内存分析工具检查并修复内存泄漏问题。
JIT (Just-In-Time) 编译问题:Java虚拟机的JIT编译器可能会花费更多时间优化代码。
解决方法:监控应用运行情况,如果发现编译耗时过长,可能需要优化代码结构。
gc问题:频繁gc导致cpu飙升
解决方法:调整垃圾收集器的参数,或者使用不同的垃圾收集器;jvm堆设置不合理,调整堆大小设置;大对象导致频繁gc,可以分批加载数据,控制进入jvm的数据量(如mybatis中的fetchSize ;以及 jdbc:mysql://localhost:3306/mydb?cursorScrollable=true&defaultFetchSize=100
)。
外部库或依赖引起的问题:某些外部库可能在后台执行大量的计算或等待资源,导致CPU使用率升高。
解决方法:检查依赖库的文档,确保它们被正确管理和使用。
系统调用或IO操作:过多的系统调用或IO操作可能会导致CPU等待硬件资源,从而使CPU使用率升高。
解决方法:优化IO操作,减少不必要的系统调用。
外部因素:如果Java进程依赖于外部服务或资源,并且这些服务出现瓶颈或不稳定,可能会导致CPU使用率上升。
解决方法:监控和确保外部依赖的稳定性和性能。
在实际处理时,可以使用如下工具和技术来诊断和解决问题:
使用top或htop命令查看哪个Java进程的CPU使用率高。
使用jstack工具获取Java线程的堆栈信息。
使用jstat工具监控JVM的各种运行状态,如垃圾收集信息。
使用Java性能分析工具(如VisualVM, JProfiler, YourKit)进行详细分析。
优化代码逻辑,减少不必要的计算或等待。
升级或更换不够高效的组件和库。
调整系统配置,如分配更多的资源给Java进程。
总之,要解决Java进程导致的CPU使用率高的问题,需要定位具体的原因,并根据原因采取相应的解决措施。
Mybatis的游标大小fetchSize
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@Select("select domain from illegal_domain where icpstatus != #{icpstatus}")
Cursor<IllegalDomain> getDayJobDomain(@Param("icpstatus") Integer icpstatus);
注意:游标是可以前后移动的。如果resultSetType = TYPE_SCROLL_INSENSITIVE ,就是设置游标就可以前后移动。
Mybatis为了保证可以前后移动,Mybatis会把之前查询的数据一直保存在内存中。
所以并不能根本解决OOM,所以我们这里需要设置为@Options(resultSetType = ResultSetType.FORWARD_ONLY)(其实默认就是ResultSetType.FORWARD_ONLY)
3.内存问题
内存问题:物理内存不足、溢出 (泄露)、OOM(堆、栈、元空间、线程)、找jvm中的大对象、jvm配置是否合理(堆大小是否合适、元空间大小是否合适、NewRatio和SurvivorRatio是否合适、线程栈大小是否合适、如何选择GC收集器、等等jvm调优问题)
内存飙升分析思路:
1.排查进程占用内存
- 使用top命令、JVisualVM工具、或者普罗米修斯等工具查看java进程内存占用情况。
-
使用jstat或者JVisualVM工具查看Full GC情况,分析full gc次数是否频繁,确认应用本身是否有问题。
-
使用jmap、jcmd查看当前应用进程使用内存,分析是否存在内存飙升等问题。(从JDK 7开始提供,jcmd拥有jmap的大部分功能,官方推荐使用jcmd命令替代jmap命令。)
-
JVisualVM导出jvm进行分析。
-
分析gc日志,确认gc次数和单次耗时,用来判断是否有问题。
jvm调优,稍后补充。
导致内存飙升的原因和解决方法
原因可能包括:
内存泄漏(Memory Leak):对象不再被使用,但垃圾回收器无法回收,因为还有活跃的引用。
大量对象创建:比如在循环中创建大量的临时对象。
分配了过大的数组或集合:可能超过了JVM堆大小限制。
线程堆栈大小不足:每个线程默认有1MB的堆栈大小,过多线程可能会导致内存增加。
JNI调用:如果使用了Native方法,可能会有内存分配在C/C++层面,而不被JVM管理。
系统虚拟内存不足:系统分配的虚拟内存大小超过了物理内存加交换空间的总和。
解决方法:
使用内存分析工具(如MAT, VisualVM等)找出内存泄漏的位置,并修复代码。
优化代码,减少不必要的对象创建。
监控和调整JVM的堆大小参数,例如 -Xms 和 -Xmx 来限制JVM使用的内存。
大对象跳过年轻代、直接放入老年代。
分析线程使用情况,必要时减少线程数量或增加线程堆栈大小。
重新分配或优化JNI调用,确保正确管理内存。
检查系统资源,增加物理内存或调整交换空间的大小。
具体解决方案取决于实际情况的分析结果。
内存飙升常见原因:
其他应用(Redis、Kafka)占用总内存;
解决:top命令排除。
启动参数内存值设定的过小;
解决:-Xms,-Xmx。
内存中加载的数据量过于庞大(数据缓存、PDF字体缓存、文件传输);
解决:代码走查,观察内存波动。
List、MAP等集合中对对象的引用,使用完后未清空,使得JVM不能回收;
解决:代码走查,观察内存波动。
代码中存在死循环或循环产生过多重复的对象实体;
解决:代码走查。
4.磁盘问题
磁盘问题:磁盘空间不足或者满了、log4j日志写磁盘慢阻塞业务请求、物理磁盘性能差、操作系统的io调度算法(elevator=deadline,这个算法试图把每次请求的延迟降至最低。该算法重排了请求的顺序来提高性能);
java进程导致io飙升的原因和解决方法:
Java进程导致IO飙升可能是由以下几个原因造成的:
文件读写不当:可能是因为Java进程在进行大量的读写操作,尤其是在创建和写入临时文件时。
网络操作:Java进程可能在进行大量的网络输入输出,尤其是在进行大量的网络通讯或下载文件时。
内存映射文件:内存映射文件会占用较多的IO资源,尤其是在进行大文件读写时。
日志记录:过多的日志记录可能会导致IO性能下降,尤其是当日志文件过大时。
配置问题:错误的文件系统配置或者IO调度策略可能导致IO性能问题。
解决方法:
优化文件读写:减少不必要的文件读写操作,使用缓冲和合适的数据结构来减少IO次数。
优化网络操作:减少不必要的网络操作,优化网络传输效率,使用NIO来提高网络通讯性能。
优化内存映射文件:适当管理内存映射文件的使用,确保文件映射在不需要时能够正确关闭。
优化日志记录:合理控制日志级别,使用异步日志记录来避免IO性能瓶颈,适当地轮换日志文件。
调整系统配置:根据具体的IO需求调整文件系统的配置,比如调整IO调度策略等。
具体解决方案需要根据实际情况分析确定
5.网络问题
网络问题:网络流量异常、网络攻击、服务器带宽不足、网络io(零拷贝,dubbo支持零拷贝)、web服务器io模型(Nginx、Tomcat等)。
Java进程导致网络峰值增高的原因可能有多种,常见的原因包括:
高频发送数据:Java应用可能会因为频繁发送网络请求或处理大量数据而导致网络使用率提高。
网络延迟:Java应用可能因为网络延迟或者频繁的网络连接/断开造成网络流量的增加。
内部缓冲区溢出:Java进程在处理网络数据时可能因为缓冲区不足导致不断发送数据包,增加网络负载。
解决方法:
优化网络通信:减少不必要的网络通信,实现更高效的数据传输协议。
流量控制:使用流量控制机制,比如TCP拥塞控制,限制发送速度。
缓冲区管理:确保Java应用有足够的缓冲区空间,避免溢出问题。
网络监控与分析:使用网络监控工具分析Java进程的网络使用情况,找出峰值所在,进行相应优化。
具体解决方案需要根据实际的网络使用情况和Java应用的具体行为来制定。
6.业务问题
业务问题:业务高并发带来的PV 量过高、服务调用耗时异常、线程死锁、多线程并发问题、频繁进行 Full GC等问题;业务逻辑问题(具体场景具体分析)、如何定位某个方法耗时很久,如何统计业务逻辑内每个环节(方法)的耗时。
业务问题,这个原因很多,解决办法倒是很直接:功能设计评审和代码走查。
功能设计评审:复杂的业务逻辑,在编码前,给出设计逻辑,提交专家组进行评审。
代码走查:看人
7.实验的代码
入口controller
package com.zxx.study.web.controller;
import com.zxx.study.web.task.FullCPUTask;
import com.zxx.study.web.task.FullIOTask;
import com.zxx.study.web.task.LazyTask;
import com.zxx.study.web.task.SyncThreadTask;
import com.zxx.study.web.util.ApiResult;
import com.zxx.study.web.util.ZhouxxTool;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* @author zhouxx
* @Description:
* @date 2024/6/2 18:33
*/
@RestController
@Slf4j
@Validated
@RequestMapping("/api/v1/lock")
public class DeadlockController {
@Value("${upload.file-path}")
private String filePath;
@Value("${server.port}")
private String serverPort;
@SneakyThrows
@GetMapping("/deadlock")
public ApiResult deadlock(@RequestParam(value ="name", required = false) String name) {
//log.info("hi==========");
/**
* 死锁是两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候。
*
* 例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。
* 线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),
* 它们将永远阻塞下去。这种情况就是一个死锁。
*
* */
Object a = new Object();
Object b = new Object();
new Thread(new SyncThreadTask(a, b)).start();
ZhouxxTool.sleep(5000);
if(name!=null&&"zhouxx".equals(name)) {
Thread thread = new Thread(new SyncThreadTask(b, a));
thread.start();
thread.join();
}
HashMap data=new HashMap();
data.put("threadName",Thread.currentThread().getName());
data.put("serverPort",serverPort);
data.put("name",name);
return ApiResult.success(data);
}
@SneakyThrows
@GetMapping("/fullcpu")
public ApiResult fullcpu(@RequestParam(value ="name", required = false) String name) {
//log.info("hi==========");
if("zhouxx".equals(name)){
//cpu高占用线程
new Thread(new FullCPUTask()).start();
//空闲线程
new Thread(new LazyTask()).start();
new Thread(new LazyTask()).start();
new Thread(new LazyTask()).start();
}
HashMap data=new HashMap();
data.put("threadName",Thread.currentThread().getName());
data.put("serverPort",serverPort);
data.put("name",name);
return ApiResult.success(data);
}
@SneakyThrows
@GetMapping("/fullio")
public ApiResult fullio(@RequestParam(value ="name", required = false) String name) {
//log.info("hi==========");
if("zhouxx".equals(name)){
//IO高占用线程
new Thread(new FullIOTask()).start();
//空闲线程
new Thread(new LazyTask()).start();
new Thread(new LazyTask()).start();
new Thread(new LazyTask()).start();
}
HashMap data=new HashMap();
data.put("threadName",Thread.currentThread().getName());
data.put("serverPort",serverPort);
data.put("name",name);
return ApiResult.success(data);
}
}
模拟cpu飙高
package com.zxx.study.web.task;
/**
* 这是一个占有大量CPU资源的任务
*
* @author lfg
* @version 1.0
*/
public class FullCPUTask implements Runnable {
@Override
public void run() {
while (true) {
double a = Math.random() * Math.random();
}
}
}
package com.zxx.study.web.task;
import com.zxx.study.web.util.ZhouxxTool;
/**
* 空闲线程
*
* @author lfg
* @version 1.0
*/
public class LazyTask implements Runnable {
@Override
public void run() {
while (true) {
ZhouxxTool.sleep(10000);
}
}
}
模拟死锁
package com.zxx.study.web.task;
import com.zxx.study.web.util.ZhouxxTool;
/**
* @author zhouxx
* @Description:
* @date 2024/6/3 14:38
*/
public class SyncThreadTask implements Runnable {
private Object a;
private Object b;
public SyncThreadTask(Object a, Object b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
ZhouxxTool.printTimeAndThread(name + " 准备获取锁 " + a);
synchronized (a) {
ZhouxxTool.printTimeAndThread(name + " 已获取锁 " + a);
doWork();
ZhouxxTool.printTimeAndThread(name + " 准备获取锁 " + b);
synchronized (b) {
ZhouxxTool.printTimeAndThread(name + " 已获取锁 " + b);
doWork();
}
ZhouxxTool.printTimeAndThread(name + " 释放锁 " + b);
}
ZhouxxTool.printTimeAndThread(name + " 释放锁 " + a);
ZhouxxTool.printTimeAndThread(name + " 结束");
}
void doWork() {
ZhouxxTool.sleep(5000);
}
}
模拟io忙
package com.zxx.study.web.task;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* IO 操作频繁的任务
*
* @author lfg
* @version 1.0
*/
public class FullIOTask implements Runnable {
@Override
public void run() {
while (true) {
try {
FileOutputStream fileOutputStream = new FileOutputStream("tempFile.txt");
for (int i = 0; i < 10000; i++) {
fileOutputStream.write(i);
}
fileOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("tempFile.txt");
while (fileInputStream.read() != -1) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
模拟oom
参考:
Java web应用性能分析之【6种OOM模拟】_java 模拟oom-CSDN博客
Java web应用性能分析之【6种OOM监控和分析】-CSDN博客