咱们经常听到内存走漏, 那么线程走漏是指什么呢?

线程走漏是指 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, 接着去处理小使命去了.

过错及改正

看到这段代码, 咱们会觉的不或许啊, 怎么会有人这么运用线程池呢? 线程池不是这么用的啊? 一脸问号. 可是现实情况是: 总有新手写出这样的代码.

有的新手被指出这个问题之后, 就去查文档, 发现 ExecutorServiceshutdown()shutdownNow() 方法啊, 于是就在 for 循环后边加了 executor.shutdown(). 当然, 这会处理线程走漏的问题. 但却不是线程池正确的用法, 因为这样尽管避免了线程走漏, 却还是每次都要创立线程池, 创立新线程, 并没有提升性能.

正确的运用方法是做一个全局的线程池, 而不是一个局部变量的线程池, 然后在运用退出前经过 hook 的方法 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(java.base@11.0.18/Native Method)
	- waiting on <0x00000007c5320a88> (a java.lang.ProcessImpl)
	at java.lang.Object.wait(java.base@11.0.18/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(java.base@11.0.18/Executors.java:515)
	at java.util.concurrent.FutureTask.run(java.base@11.0.18/FutureTask.java:264)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(java.base@11.0.18/ThreadPoolExecutor.java:1128)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(java.base@11.0.18/ThreadPoolExecutor.java:628)
	at java.lang.Thread.run(java.base@11.0.18/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 阻拦创立新线程的代码方位;