Java非功能问题分析方法
线程堆栈分析
用于哪些问题?
- 系统无缘无故CPU过高
- 系统挂起无响应
- 系统运行越来越慢
- 性能瓶颈(如无法充分使用CPU)
- 线程死锁、死循环、饿死等
- 线程数量太多导致系统失败(如无法创建线程等)
具体知识
如何输出线程堆栈?
在windoes上:使用jvm指令,jps和jstack
我们看如下一个实例,一个普通的死循环。
public static void main(String[] args) {
while (true) {
}
}
执行jps指令,查看pid,
PS C:\CT\WorkSpace\Test> jps
13408
18704 org.eclipse.equinox.launcher_1.6.900.v20240613-2009.jar
23456 Recusion
10104 Jps
然后再根据pid用jstack查看堆栈
jstack 23456
在Linux上:ps -ef | grip java p stack
在linux的方法与windows上类似,这里不做示例。
如何解读线程堆栈?
对线程堆栈的解读,我们主要从三个方面展开,第一、线程解读,第二、锁的解读,第三、线程状态解读
线程解读
在dump的线程堆栈日志中,
搜索
搜索
锁的解读
wait
在线程执行到wait时会释放监视锁,在wait执行完成后再持有该锁。
sleep
当线程执行到sleep时会继续持有监视锁 -> 该方法是线程的一个静态方法,与锁无关
lock
- 当占有锁时:locked<0x25156....>
- 当等待其它线程释放该锁:wait to lock<0x22552....>
- 当一个线程占有一个锁,但又执行到该锁的wait上:先locked,后waiting on<0x666225....>
线程状态解读
首先我们看下,一个线程有哪些状态
注:当线程挂起时,都不消耗CPU,即当线程处于TIMED_WAITING和WAITING状态时肯定不消耗CPU;
当线程处于RUNNABLE状态时,需要看具体状态,一般线程是socket、IO操作和挂起时,不消耗CPU,在做运算时才消耗CPU
RUNNABLE
当locked时,一般不消耗CPU资源
TIMED_WAITING
- on object monitor -> 表示线程被挂起一段时间,表示正在执行object.wait(int)方法
- sleeping -> 表示线程被挂起一段时间,表示正在执行Thread.sleep(long)方法
- parking -> 表示线程被挂起一段时间,表示正在执行Lock support.parkNanos(long)方法
WAITING(on object monitor)
表示线程被挂起一段时间,正在执行object.wait()方法,此时线程只能被notify唤醒
线程堆栈不能分析什么类型的问题?
- 线程为什么跑飞(跑飞:通常指的是一个线程在执行过程中,由于某种原因偏离了预定的执行路径)
- 并发的BUG导致的数据混乱
- 数据库锁表问题
如何借助线程堆栈进行问题分析?
信息收集
- 堆栈局部信息 -> 一般用于线程死锁分析
- 每个线程的调用关系
- 每个线程的状态
- 一次堆栈的统计信息 -> 笨贼理论:出现最多的线程,性能有待推敲分析,资源不足、性能瓶颈分析
- 锁争用情况
- 是否有死锁
- 是否达到性能瓶颈导致锁竞争
- 结合代码分析是否有死循环
- 大部分线程在做什么
- 线程总数量
- 多次线程的比对信息 -> 线程过高、死循环、性能瓶颈分析
- 一个线程是否在长期执行
- 某个线程是否长期获取不到锁
死锁分析
发生场景:当两个或多个线程正在等待被对方占用的锁时,死锁就会发生
分析:死锁的情况一般在堆栈信息中会有明确打印"Found on java-level deadlock"
CPU过高
-
死循环导致 -> 计算机总有一个核的CPU达到100%,通过top指令查看
分析多次堆栈信息,去除wait和lock的线程,每次都存在的线程都有风险
- 系统设计不合理,譬如不间断轮询、过于复杂的算法
- JNI中有死循环
-
不恰当的堆内存设置,导致频繁GC从而CPU过高
举个例子:应用启动时在一个大循环中创建对象,导致GC的场景
- 32位JDK下,堆内存设置太大造成频繁GC
- JDK自身死循环BUG
资源不足导致性能下降
表象:越来越慢,并最终停止响应
特点:大量线程停在同样的调用上下文中
原因:
- 资源数量配置太少
- 获得资源的线程把持太久
- 设计不合理
- 慢SQL、索引等
- 资源用完后没有及时关闭或者回收导致资源泄露或者减少
线程不退出导致系统挂死
分析方法:多次打印线程堆栈,找出一直活跃的线程
常见原因:
- 该线程存在死循环
- 资源不足或者资源泄露,造成当前线程阻塞在锁对象上
- 与外部通信,外部系统无返回
性能瓶颈分析
目的
用更少的资源做更多的事
不消耗CPU的操作
- 磁盘IO
- 网络IO
- 带有3D加速卡的图形运算
性能瓶颈
什么是一个好的程序?
我们都说性能调优,那我们进行性能调优的目的是什么呢?
这里可以给个答案:逐步增大压力,系统的CPU利用率能够接近100%。
所以我们可以两个方面来评估我们的程序
- 增加压力,CPU的使用率能不能接近100%
- 在CPU使用率到100%时,代码是否可以优化
常见场景:
- 不恰当的同步导致的资源争用
- sleep滥用
- string + 的滥用
- 不恰当的线程模型
- 效率低下的SQL
- 不恰当的数据库设计
- 不恰当的GC参数
- 线程数量不足
- 内存泄露导致频繁GC
- 过度磁盘IO
内存泄露和堆内存设置
垃圾回收算法
不管什么垃圾回收算法,其实就做两件事
- 检测出垃圾对象
- 回收垃圾对象使用的堆空间
内存泄露
现象:不在需要的对象存在外部引用,导致虚拟机认为这些对象不是垃圾
场景
- 全局容器
- java虚拟机自身管理的对象
- 调用外部函数
java的三种内存
- 堆内存
- 本地内存
- 加载类的perm
症状
- 系统越来越慢
- 抛出OOM异常
- 检查方向
- 应用程序中缓存功能
- 大量长期活动对象
- 堆内存泄露
- 本地内存泄露
- core dump
定位
- 堆内存
- 堆内存设置大小
- 代码泄露问题
- 设计原因,如过多缓存数据库中数据
- 本地内存
- JNI调用
- JDK的BUG
- 操作系统的BUG
- perm内存