![alt](https://img-blog.csdnimg.cn/img_convert/31aa2e7f522b914c21d25a0ef9f1ea66.png)
IT行业中目前java技术栈仍然占据着主导的地位,在生产环境抢修中,还有一些非常常见的生产问题,依然是JVM相关的问题占比非常高,今年我们就来整理探讨一下这方面的问题
常见的JVM问题类型
在参与多年的生产抢修过程中,下面列举的故障都是十分严重的,每一个故障都会导致生产系统不可用,给企业造成不可估量的损失,常见的故障问题主要是如下几种:
-
OOM内存溢出 -
CPU资源开销非常高,超过90% -
线程死锁 -
线程等待
CPU资源开销高问题剖析
-
OOM内存溢出在上篇文章中已经详细分享过,可以查看《JAVA内存溢出问题深入剖析》
-
CPU资源开销高问题 这个问题很好理解,就是操作系统的CPU时间片资源都被java进程占用了,下面是一个代码示例,通过创建多个线程来抢占CPU时间片,来达到消耗光cpu资源的目的。(这里注意单个执行任务的线程无法做到),我们来看下相关的示例代码:
public class Highcpu {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName());
try {
// Thread.sleep(30 * 60 * 1000);
int sum = 0;
while (true) {
// sum++;
}
} catch (Exception e) {
e.printStackTrace();
}
});
thread.setName("thread-" + i);
thread.start();
}
}
}
该代码运行后,cpu资源性能立马飙到了100%,只要线程一直运行,占用资源就一直不会被释放
![alt](https://img-blog.csdnimg.cn/img_convert/927e719490a953c47f542579322ebc8f.png)
现在我们来看一下,如何从运行中的线程信息中定位到问题代码
主要是如下几步:
-
确认是java进程占用cpu高 -
找到java进程下cpu占用高的线程,把十进制线程号转换成十六进制 -
使用jstack工具打印出java线程堆栈信息,定位问题代码
-
下面是实操环节
-
确认是java进程占用cpu高(我的环境是windows,linux系统使用的工具会有区别)
![alt](https://img-blog.csdnimg.cn/img_convert/31f33cba27432a5786711e2fb7677be0.png)
-
找到java进程下cpu占用高的线程,把十进制线程号转换成十六进制
这里我使用的是process explorer这个工具
![alt](https://img-blog.csdnimg.cn/img_convert/c414d5ad85f5e8e33261ee2f8222e1bf.png)
这么多线程中找前两个线程进行分析,分别是20312和5900这两个线程,通过计算机转换成16进制数为:4F58和170C
![alt](https://img-blog.csdnimg.cn/img_convert/650a945675ae4dd5d73598044be51844.png)
-
使用jstack工具打印出java线程堆栈信息,定位问题代码 jstack 14432 > d:\heapdump\2023101601.log
![alt](https://img-blog.csdnimg.cn/img_convert/b44f0018a4ced16cb1474719b37b454f.png)
找到问题代码段,搞定!
线程死锁问题剖析
线程死锁问题简单解释如下:两个线程A,B A一直持有资源1,B一直持有资源2。这时A想要再持有资源1,B想要再持有资源2,一直获取不到,导致死锁。下面我们就用程序演示还原一下这个场景
public class DeadLockDemo {
public static void main(String[] args) {
Resource resource1 = new Resource("资源1");
Resource resource2 = new Resource("资源2");
doSomething(resource1, resource2);
doSomething(resource2, resource1);
}
private static void doSomething(Resource resource1, Resource resource2) {
new Thread(() -> {
// 获取资源1的锁
System.out.println(Thread.currentThread().getName() + " 请求" + resource1 + "的锁");
synchronized (resource1) {
System.out.println(Thread.currentThread().getName() + " 获取到" + resource1 + "的锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取资源2的锁
System.out.println(Thread.currentThread().getName() + " 请求" + resource2 + "的锁");
synchronized (resource2) {
System.out.println(Thread.currentThread().getName() + " 获取到" + resource2 + "的锁");
}
}
}).start();
}
}
class Resource {
private String name;
public Resource(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
}
如上代码运行后,成功的实现了死锁的场景。两个线程都再等待对方资源导致无法继续执行后续任务。
![alt](https://img-blog.csdnimg.cn/img_convert/bfe71e8d71786007505198cedc9de123.png)
首先使用jps找到对应进程pid
![alt](https://img-blog.csdnimg.cn/img_convert/df46d44de70241942e7503bd563b0d06.png)
再使用jstack 17908,找到了两个死锁的线程,成功定位到了问题代码段
![alt](https://img-blog.csdnimg.cn/img_convert/d92c5a75ac0ed7ae29f1de97d04b1fc8.png)
线程等待问题剖析
线程等待问题是当某一个线程一直持有锁不释放,导致其他线程无法获得该锁,一直处于等待状态。我们来看一下导致该问题的代码:
import java.util.LinkedList;
import java.util.Queue;
public class ObjectMethodTest {
Queue<String> queue = new LinkedList<String>();
int MAX_SIZE = 1; // 假设队列长度只有1 , 只能存放一条数据
public void produce() {
synchronized (queue) {
// 队列满则等待队列空间
while (queue.size() == MAX_SIZE) {
// 挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里面的元素。
try {
queue.wait();
System.out.println("-----等待消费----");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产元素, 并通知唤醒消费者
queue.add("hahaha");
}
}
public void consume() {
synchronized (queue) {
while (queue.size() == 0) {
// 挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,然后获取队列里面的元素。
try {
queue.wait();
System.out.println("-----等待生产-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费元素, 并通知唤醒生产者
System.out.println("消费成功:" + queue.poll());
queue.notifyAll();
}
}
public static void main(String[] args) {
ObjectMethodTest objectMethodTest = new ObjectMethodTest();
// 10 个生产线程
for (int i = 0; i < 10; i++) {
new Thread(objectMethodTest::produce).start();
}
// 10 个消费线程
for (int i = 0; i < 10; i++) {
new Thread(objectMethodTest::consume).start();
}
}
}
代码实现逻辑说明:
定义了一个队列(Queue queue)和队列最大长度(int MAX_SIZE = 1)。
produce 方法用于生产元素。它通过synchronized (queue)来获取队列的锁,确保生产和消费操作的互斥执行。如果队列已满,生产者会进入等待状态,并一直持有该队列锁。
consume 方法用于消费元素。同样,它也使用synchronized (queue)来获取队列的锁。如果队列为空,消费者会进入等待状态,释放队列的锁,以便生产者可以获取锁并生产元素。
在 main 方法中,创建了一个 ObjectMethodTest 对象,并启动了10个生产者线程和10个消费者线程。
该场景会导致当持有该队列锁的线程,队列大小达到最大后。该线程会进入挂起状态,并一直持有该队列锁。导致其他线程无法获取,一直等待。
我们来定位一下问题代码段,主要步骤和排查死锁问题一样
-
通过jps找到进程pid
-
使用jstack pid 打印出线程堆栈
我们可以看到消费者线程都在等待 - waiting on <0x000000076c0bf8b8>这个锁资源,并且我们知道这个锁是一个java.util.LinkedList类型的资源锁。
并且定位到了问题代码段:at com.example.demo.controller.ObjectMethodTest.consume(ObjectMethodTest.java:38)
同时生产者线程也同样在等待这个锁资源 - waiting on <0x000000076c0bf8b8> (a java.util.LinkedList) 同时也指出了问题代码段
![alt](https://img-blog.csdnimg.cn/img_convert/fe52d03964cd78cbe62338ea59e4e781.png)
总结
知其然,知其所以然。我们通过代码示例还原了如下几个常见的生产问题场景,并实操了如何进行问题排查,定位到问题代码
-
CPU资源开销非常高,超过90% -
线程死锁 -
线程等待
希望上面的案例能够帮助到你,我会带来更多更干的干货分享,欢迎关注我!
本文由 mdnice 多平台发布