(16)故障排查
1 怎么解决Java包依赖冲突?
如果项目的依赖A和依赖B同时引入了依赖C,依赖C在A和B中的版本不一致就可能依赖冲突。比如 项目 <- A, B, A <- C(1.0),B <- C(1.1),maven如果选择导入高版本C(1.1),这个选择maven会根据不等路径短路径原则和同等路径第一声明原则选取,C(1.0)中的类c在C(1.1)中被修改而不存在了。在编译期可能并不会报错,因为编译的目的只是把业务源代码编译成class文件,所以如果项目源代码中没有引入共有依赖C因升级而缺失的类c,就不会出现编译失败。除非源代码就引入了共有依赖C因升级而缺失的类c则会直接编译失败。在运行期,很有可能出现依赖A在执行过程中调用C(1.0)以前有但是升级到C(1.1)就缺失的类c,导致运行期失败,出现很典型的依赖冲突时的NoClassDefFoundError错误。如果是升级后出现原有的方法被修改而不存在的情况时,就会抛出NoSuchMethodError错误。
解决方案:
(1)首先可以借助Maven查看依赖的依赖树来分析一下: mvn dependency:tree ,或者使用IDEA的插件Dependency Analyzer插件来可视化地分析依赖关系。这个过程后可以明确哪些dependency引入了可能会冲突的依赖。
(2)比如我们的项目引入A的依赖C为 1.1 版本,引入的B会在内部依赖C的1.0版本,那么Dependency Analyzer插件会出现依赖冲突提示,会提示B引入的C的1.0版本和当前选用的C的 1.1 版本冲突因而被忽略(1.0 omitted for conflict with 1.1)。
(3)如果这时候打war包出来启动很有可能会遇到因依赖冲突而出现的 NoClassDefFoundError 或 NoSuchMethodError ,导致编译期正常的代码无法在运行期跑起来。
(4)由于A引入的C的版本高而B依赖的C版本低,我们优先会选择兼容高版本C的方案,即试图把B的版本调高以使得引入的依赖C可以和A引入的依赖A达到一致的版本,以此来解决依赖冲突。当然这是一个理想状况。
(5)如果找到了目前已有的所有的B的版本,均发现其依赖的C没有与A一致的 1.1 版本,比如B是一个许久未升级的旧项目,那么也可以考虑把A的版本拉低以使得C的版本降到与B一致的 1.0 版本,当然这也可能会反过来导致A不能正常工作。
(6)上面已经可以看出来解决依赖冲突这件事情并不简单,很难顾及两边,很多情况下引入不同版本依赖的很可能超过两方而是更多方。
(7)那么来考虑一下妥协的方案,如果A引入的C使用的功能并不跟被抛弃的类或方法有关,而是其他在 1.1 版本中仍然没有改变的类或方法,那么可以考虑直接使用旧的 1.0 版本,那么可以使用 exclusion 标签来在A中排除掉对C的依赖,那么A在使用到C的功能时会使用B引入的 1.0 旧版本C。即A其实向B妥协使用了B依赖的C。
2 如何排查JVM中出现的OOM原因?
- 常见的OOM原因
(1)内存中加载的数据量过于庞大,如一次从数据库取出过多数据
(2)资源使用之后没有及时关闭,导致对象无法被GC回收
(3)代码中存在死循环或循环产生过多重复的对象实体
(4)使用的第三方软件中的BUG
(5)启动参数内存值设定的过小
- OOM发生区域
(1)Java堆溢出:heap
java.lang.OutofMemoryError:Java heap space
原因:内存泄露、JVM内存小、创建太多的对象没有释放。
相关JVM参数:-Xms、-Xmx
(2)Java栈溢出:stack
java.lang.StackOverflowError,常发生于递归。
java.lang.OutofMemoryError: unable to create new native thread,常发生于创建太多线程。
相关JVM参数:-Xss,表示每个线程的堆栈大小。
(3)运行时常量溢出:constant
java.lang.OutofMemoryError: PermGen space。
运行时常量保存在方法区,存放的主要是编译器生成的各种字面量和符号引用,但是运行期间也可能将新的常量放入池中。
-XX:PermSize:设置持久代(perm gen)初始值,默认值为物理内存的1/64
-XX:MaxPermSize:设置持久代最大值,默认为物理内存的1/4
(4)方法区溢出:directMemory
java.lang.OutofMemoryError: PermGen space。
方法区主要存储被虚拟机加载的类信息,如类名、访问修饰符、常量池、字段描述、方法描述等。目前很多框架,如Spring、Hibernate等会在运行过程中动态生成类,所以方法区也有可能发生OOM。
- OOM排查流程
设置JVM参数,设定当发生OOM的时候自动生成dump出堆信息。开启堆快照:-XX:+HeapDumpOnOutOfMemoryError。OOM时日志记录文件位置:-XX:HeapDumpPath=/usr/local/error.hprof
(1)先查看应用pid号:ps -ef|grep 应用名/java。或者使用jps查看JVM中运行的进程状态信息;
(2)查看pid垃圾回收情况:jstat -gc pid 5000(时间间隔)。查看堆内存各部分的使用量,加载类的数量以及GC的情况
(3)使用jmap -histo:live pid 显示堆中的详细信息;
(4)使用jmap -dump:format=b,file=heapdump.phrof pid生成堆存储快照,然后将快照文件使用jvisualvm进行分析;
3 怎么查看Java线程的资源占用?
(1)使用命令top -p <pid> ,显示java进程的内存情况,pid是java进程号,比如123
(2)按H,获取每个线程的内存情况
(3)找到内存和cpu占用最高的线程pid,比如15248
(4)执行 printf 0x%x 15248 得到 0x3b90 ,此为线程id的十六进制
(5)执行 jstack 123|grep -A 10 3b90,得到线程堆栈信息中3b90这个线程所在行的后面10行
4 如何分析thread dump文件?
JVM 线程转储是在特定时间点作为进程一部分的所有线程的状态列表。它包含有关线程堆栈的信息,以堆栈跟踪的形式呈现。由于它是明文编写的,因此可以保存内容以供以后查看。线程转储分析有助于优化 JVM 性能,优化应用程序性能,诊断问题,例如死锁、线程争用等。生成线程转储的最简单方法是使用 JStack工具。
(1) 采用JPS命令列出所有Java进程ID:jps -l
(2)使用jstack命令生成线程转储:jstack -l 26680
,信息示例如下:
[test@localhost ~]# jstack -l 26680
2022-06-27 06:04:53
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.221-b11 mixed mode):
"Attach Listener" #16287 daemon prio=9 os_prio=0 tid=0x00007f0814001800 nid=0x4ff2 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
Locked ownable synchronizers:
- None
"logback-8" #2316 daemon prio=5 os_prio=0 tid=0x00007f07e0033000 nid=0x4792 waiting on condition [0x00007f07baff8000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000006ca9a1fc0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081)
at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
分析阻塞的线程:分析线程转储日志,如果发现它检测到了处于 BLOCKED 状态的线程,这使得应用程序的性能非常缓慢。如果能找到 BLOCKED 线程,可以尝试提取与线程试图获取的锁相关的线程。从当前持有锁的线程分析堆栈跟踪有助于解决问题。
[test@localhost ~]# jstack -l 26680
" DB-Processor-13" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f000]
java.lang.Thread.State: <strong>BLOCKED</strong> (on object monitor)
at beans.ConnectionPool.getConnection(ConnectionPool.java:102)
- waiting to lock <0xe0375410> (a beans.ConnectionPool)
at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111)
at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43)
"DB-Processor-14" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f020]
java.lang.Thread.State: <strong>BLOCKED</strong> (on object monitor)
at beans.ConnectionPool.getConnection(ConnectionPool.java:102)
- waiting to lock <0xe0375410> (a beans.ConnectionPool)
at beans.cus.ServiceCnt.getTodayCount(ServiceCnt.java:111)
at beans.cus.ServiceCnt.insertCount(ServiceCnt.java:43)
分析死锁线程:死锁是涉及至少两个线程的情况,其中一个线程继续执行所需的资源被另一个线程锁定,同时第二个线程所需的资源被第一个线程锁定。如果果存在dreadlocks,则线程转储的最后部分将打印出有关死锁的信息,如下所示:
[test@localhost ~]# jstack -l 26680
"Thread-0":
waiting to lock monitor 0x00000250e4982480 (object 0x00000000894465b0, a java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x00000250e4982380 (object 0x00000000894465a0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
at DeadlockedProgram$DeadlockedRunnableImplementation.run(DeadlockedProgram.java:34)
- waiting to lock <0x00000000894465b0> (a java.lang.Object)
- locked <0x00000000894465a0> (a java.lang.Object)
at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
"Thread-1":
at DeadlockedProgram $DeadlockRunnableImplementation.run(DeadlockedProgram.java:34)
- waiting to lock <0x00000000894465a0> (a java.lang.Object)
- locked <0x00000000894465b0> (a java.lang.Object)
at java.lang.Thread.run(java.base@10.0.1/Thread.java:844)
5 如何排查服务器的CUP使用率过高的原因?
(1)输入top指令,找到比较高的CPU使用率所对应的PID编号
(2)输入ps H -eo pid,tid,%cpu | grep 进程PID 查看进程下面哪些线程TID使用CPU高。
(3)输入printf '%x' TID将线程ID换算为16进制的数
(4)jstack pid | grep 步骤3转换的数据(5b61) -A100寻找具体CPU过高的导致的执行日志
6 如何排查服务器Load高而CPU使用率低的情况?
CPU的wait或idle的值较大,表明CPU利用率不高。Load高代表需要运行的队列累计过多,但队列中的任务实际可能是耗CPU的,也可能是耗IO及其他因素,考虑两个方面:
(1)磁盘IO过高,线程等待:使用 vmstat 命令查看进程、内存、I/O等系统整体运行状态vmstat 2 5
。也可以使用iotop工具,默认显示对IO高低进行倒序实时显示,其中tid即是pid。
(2)线程频繁切换:上下文切换次数较多的情况下,容易导致CPU将大量的时间耗费在寄存器、内核栈、以及虚拟内存等资源的保存和恢复上,进而缩短了真正运行进程的时间造成load高。通过vmstate只能查看总的CPU上下文切换,可通过pidstat命令查看线程层面的上下文切换信息 pidstat -wt 1
。
7 如何排查后台服务变慢的原因?
首先,对这个问题进行更加清晰的定义:
(1)偶尔变慢还是长时间运行后观察到变慢,是否重复出现?
(2)如何定义“慢”,是系统对外部请求的反应耗时变长吗?
排查步骤:
(1)先检查系统级别的资源等情况,监控 CPU、内存等资源被哪些进程大量占用,并且这种占用是否符合系统正常运行状况。
(2)监控 Java 服务,例如 GC 日志里面是否观察到 Full GC 等恶劣情况出现,或者是否 Minor GC 在变长等;利用 jstat 等工具,获取内存使用的统计信息也是个常用手段;利用 jstack 等工具检查是否出现死锁等。
(3) 如果还不能确定具体问题,可以尝试对应用服务进行压测,输出系统性能报告。
参考(摘抄的文字版权属于原作者)
https://www.isolves.com/it/wl/js/2020-06-01/19173.html
https://blog.csdn.net/weixin_37672801/article/details/128774022
https://zhuanlan.zhihu.com/p/458485338