Java アプリケーションのスレッド リークを診断する方法

メモリ リークについてよく耳にしますが、スレッド リークとはどういう意味ですか?

スレッド リークとは、JVM 内にますます多くのスレッドが存在することを意味し、これらの新しく作成されたスレッドは、最初の使用後に使用されなくなりますが、破棄されません. 通常、この種の問題は間違ったコードによって引き起こされます.

一般に、この種の問題は、Java アプリケーションのスレッド数に関する指標を監視することで発見できますが、これらの指標に対する適切な監視手段がない場合、またはアラーム情報が設定されていない場合は、スレッドが実行されるまで発見できない可能性があります。オペレーティング システムのメモリが不足し、OOM が公開されます。

最も一般的な例

実稼働環境では、次の例のようなものを何度も見てきました。

public void handleRequest(List<String> requestPayload) {
	if (requestPayload.size() > 0) {
		ExecutorService executor = Executors.newFixedThreadPool(2);
		
		for (String str : requestPayload) {
			final String s = str;
			executor.submit(new Runnable() {
				@Override
				public void run() {
					// print 模拟做很多事情
					System.out.println(s);
				}
			});
		}
	}
	// do some other things
}
复制代码

このコードは、多数の小さなタスクを含むビジネス リクエストを処理しているため、スレッド プールを使用してそれぞれの小さなタスクを処理することを考えたので、スレッド プールを作成してから、小さなタスクを処理しましたExecutorService

エラーと修正

このコードを見ると、誰もがそれは不可能だと感じるでしょう. 誰かがこのようにスレッドプールをどのように使用できるでしょうか? スレッドプールはこのように使用されているのではないでしょうか? 顔には疑問符があります. しかし現実には: 常にある.そのようなコードを書く初心者。

何人かの初心者がこの問題を指摘した後、ドキュメントを確認したところ、メソッドとメソッドExecutorServiceがあることがわかったので、 for ループの後に追加しました. もちろん、これによりスレッドリークの問題は解決します. しかし、それは正しい使用法ではありませんスレッド リークは回避されますが、スレッド プールは毎回作成され、新しいスレッドが作成されるため、パフォーマンスは向上しません。shutdown()shutdownNow()executor.shutdown()

これを使用する正しい方法は、ローカル変数スレッド プールの代わりにグローバル スレッド プールを作成し、アプリケーションが終了する前にスレッド プールをフックでシャットダウンすることです。

しかし、このコードがどこにあるかがわかっていたので、すぐに修正することができました. スレッド数が増え続ける複雑な Java アプリケーションを使用している場合、スレッド リークの原因となっているコード ブロックをどのように見つけることができるでしょうか?

シーン再構成

通常情况下, 我们会有每个应用的线程数量的指标, 如果某个应用的线程数量启动后, 不管分配的 CPU 个数, 一直保持上升趋势, 那么就危险了. 这个时候, 我们就会去查看线程的 Thread dump, 去查看到底哪些线程在持续的增加, 为什么这些线程会不断创建, 创建新线程的代码在哪?

找到出问题的代码

在 Thread dump 里面, 都有线程创建的顺序, 还有线程的名字. 如果新创建的线程都有一个自己定义的名字, 那么就很容易的找到创建的地方了, 我们可以根据这些名字去查找出问题的代码.

根据线程名去搜代码

比如下面创建的线程的方式, 就给了每个线程统一的名字:

Thread t = new Thread(new Runnable() {
	@Override
	public void run() {
	}
}, "ProcessingTaskThread");
t.setDaemon(true);
t.start();
复制代码

如果这些线程启动之前不设置名字, 系统都会分配一个统一的名字, 比如thread-n, pool-m-thread-n, 这个时候通过名字就很难去找到出错的代码.

根据线程处理的业务逻辑去查代码

大多数时候, 这些线程在 Thread dump 里都表现为没有任何事情可做, 但有些时候, 你可以能发现这些新创建的线程还在处理某些业务逻辑, 这时候, 根据这些业务逻辑的代码向上查找创建线程的代码, 也不失为一种策略.

比如下面的线程栈里可以看出这个线程池在处理我们的业务逻辑代码 AsyncPropertyChangeSupport.run, 然后根据这个关键信息, 我们就可以查找出到底那个地方创建了这个线程:

"pool-2-thread-4" #159 prio=5 os_prio=0 cpu=7.99ms elapsed=354359.32s tid=0x00007f559c6c9000 nid=0x6eb in Object.wait()  [0x00007f55a010a000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait([email protected]/Native Method)
	- waiting on <0x00000007c5320a88> (a java.lang.ProcessImpl)
	at java.lang.Object.wait([email protected]/Object.java:328)
               ... 省略 ...
	at com.tianxiaohui.JvmConfigBean.propertyChange(JvmConfigBean.java:180)
	at com.tianxiaohui.AsyncPropertyChangeSupport.run(AsyncPropertyChangeSupport.java:346)
	at java.util.concurrent.Executors$RunnableAdapter.call([email protected]/Executors.java:515)
	at java.util.concurrent.FutureTask.run([email protected]/FutureTask.java:264)
	at java.util.concurrent.ThreadPoolExecutor.runWorker([email protected]/ThreadPoolExecutor.java:1128)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run([email protected]/ThreadPoolExecutor.java:628)
	at java.lang.Thread.run([email protected]/Thread.java:829)
复制代码

使用 btrace 查找创建线程的代码

在上面2种比较容易的方法已经失效的时候, 还有一种一定能查找到问题代码的方式, 就是使用 btrace 注入拦截代码: 拦截创建新线程的地方, 然后打印当时的线程栈.

我们稍微改下官方的拦截启动新线程的例子, 加入打印当前栈信息:

import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.OnMethod;
import org.openjdk.btrace.core.annotations.Self;

import static org.openjdk.btrace.core.BTraceUtils.*;

@BTrace
public class ThreadStart {
    @OnMethod(
            clazz = "java.lang.Thread",
            method = "start"
    )
    public static void onnewThread(@Self Thread t) {
        D.probe("jthreadstart", Threads.name(t));
        println("starting " + Threads.name(t));
		println(jstackStr());
    }
}
复制代码

然后执行 btrace 注入, 一旦有新线程被创建, 我们就能找到创建新线程的代码, 当然, 我们可能拦截到不是我们想要的线程创建栈, 所以要区分, 哪些才是我们希望找到的, 有时候, 上面的代码中可以加一个判断, 比如线程名字是不是符合我们要找的模式.

$ ./bin/btrace 1036 ThreadStart.java
Attaching BTrace to PID: 1036
starting HandshakeCompletedNotify-Thread
java.base/java.lang.Thread.start(Thread.java)
java.base/sun.security.ssl.TransportContext.finishHandshake(TransportContext.java:632)
java.base/sun.security.ssl.Finished$T12FinishedConsumer.onConsumeFinished(Finished.java:558)
java.base/sun.security.ssl.Finished$T12FinishedConsumer.consume(Finished.java:525)
java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392)
复制代码

上面的代码, 就抓住了一个新创建的线程的地方, 只不过这个可能不是我们想要的.

除了线程会泄漏之外, 线程组(ThreadGroup) 也有可能泄漏, 导致内存被用光, 感兴趣的可以查看生产环境出现的一个真实的问题: 为啥 java.lang.ThreadGroup 把内存干爆了

总结

针对线程泄漏的问题, 诊断的过程还算简单, 基本过程如下:

  1. 先确定是哪些线程在持续不断的增加;
  2. 然后再找出创建这些线程的错误代码;
    1. 根据线程名字去搜错误代码位置;
    2. 根据线程处理的业务逻辑代码去查找错误代码位置;
    3. 使用 btrace 拦截创建新线程的代码位置;

おすすめ

転載: juejin.im/post/7219695336422621221