Cómo diagnosticar fugas de subprocesos de aplicaciones Java

A menudo escuchas sobre fugas de memoria, entonces, ¿qué significa fuga de subprocesos?

La fuga de subprocesos significa que hay más y más subprocesos en la JVM, y estos subprocesos recién creados ya no se usan después del uso inicial, pero no se destruyen. Por lo general, este tipo de problema es causado por un código incorrecto.

Generalmente, este tipo de problema se puede descubrir al monitorear los indicadores relacionados con la cantidad de subprocesos en las aplicaciones Java. Si no hay buenas medidas de monitoreo para estos indicadores, o no se establece información de alarma, es posible que no sea posible hasta que se ejecuten los subprocesos. fuera de la memoria del sistema operativo y exponga OOM.

ejemplo más común

En el entorno de producción, he visto muchas veces similares al siguiente ejemplo:

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
}
复制代码

Este código está procesando una solicitud comercial, que contiene muchas tareas pequeñas, así que pensé en usar un grupo de subprocesos para procesar cada tarea pequeña, así que creé una y luego procesé ExecutorServicelas tareas pequeñas.

errores y correcciones

Al ver este código, todos sentirán que es imposible. ¿Cómo puede alguien usar el grupo de subprocesos de esta manera? ¿No se usa el grupo de subprocesos de esta manera? Hay un signo de interrogación en la cara. Pero la realidad es: siempre hay novatos que escriben dicho código.

Después de que algunos novatos señalaron este problema, revisaron la documentación y descubrieron que ExecutorServicehay métodosshutdown() y , por lo que lo agregaron después del ciclo for . Por supuesto, esto resolverá el problema de la fuga de subprocesos. Pero no es el uso correcto de el grupo de subprocesos, porque aunque se evita la fuga de subprocesos, el grupo de subprocesos aún se crea cada vez y se crea un nuevo subproceso, lo que no mejora el rendimiento.shutdownNow()executor.shutdown()

La forma correcta de usarlo es crear un grupo de subprocesos global en lugar de un grupo de subprocesos variable local y, a continuación, cerrar el grupo de subprocesos antes de que se cierre la aplicación.

Sin embargo, pudimos solucionarlo rápidamente, sabiendo dónde estaba este fragmento de código.Si tiene una aplicación Java compleja con un número cada vez mayor de subprocesos, ¿cómo podemos encontrar el bloque de código que está causando la fuga de subprocesos?

Reconstrucción de escena

通常情况下, 我们会有每个应用的线程数量的指标, 如果某个应用的线程数量启动后, 不管分配的 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 拦截创建新线程的代码位置;

Supongo que te gusta

Origin juejin.im/post/7219695336422621221
Recomendado
Clasificación