java - ThreadLocal & Memory Leak
multithreading memory-leaks (6)
Se menciona en varias publicaciones: el uso incorrecto de ThreadLocal
provoca ThreadLocal
memoria. Estoy luchando por comprender cómo ocurriría la ThreadLocal
memoria con ThreadLocal
.
El único escenario que he descubierto es el siguiente:
Un servidor web mantiene un grupo de subprocesos (por ejemplo, para servlets). Esos hilos pueden crear pérdida de memoria si las variables en
ThreadLocal
no se eliminan porque los hilos no se mueren.
Este escenario no menciona la fuga de memoria "Perm Space". ¿Es ese el único (principal) caso de fuga de memoria?
No hay nada intrínsecamente incorrecto con los locals de hilo: No causan pérdidas de memoria. Ellos no son lentos Son más locales que sus contrapartes locales que no son de subprocesos (es decir, tienen mejor información para ocultar propiedades). Pueden ser mal utilizados, por supuesto, pero también la mayoría de las otras herramientas de programación ...
Consulte este enlace por Joshua Bloch
Debajo del código, la instancia t en la iteración para no puede ser GC. Este puede ser un ejemplo de ThreadLocal & Memory Leak
public class MemoryLeak {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
TestClass t = new TestClass(i);
t.printId();
t = null;
}
}
}).start();
}
static class TestClass{
private int id;
private int[] arr;
private ThreadLocal<TestClass> threadLocal;
TestClass(int id){
this.id = id;
arr = new int[1000000];
threadLocal = new ThreadLocal<>();
threadLocal.set(this);
}
public void printId(){
System.out.println(threadLocal.get().id);
}
}
}
La respuesta aceptada a esta pregunta y los registros "graves" de Tomcat sobre este tema son engañosos. La cita clave allí es:
Por definición, se mantiene una referencia a un valor de ThreadLocal hasta que el hilo "propietario" muere o si el ThreadLocal en sí no se puede alcanzar. [Mi énfasis].
En este caso, las únicas referencias a ThreadLocal se encuentran en el campo final estático de una clase que ahora se ha convertido en un objetivo para GC y la referencia de los hilos de trabajo. Sin embargo, las referencias de los subprocesos de trabajo a ThreadLocal son WeakReferences .
Sin embargo, los valores de un ThreadLocal no son referencias débiles. Entonces, si tiene referencias en los valores de un ThreadLocal a las clases de la aplicación, estas mantendrán una referencia al ClassLoader y evitarán el GC. Sin embargo, si sus valores ThreadLocal son solo enteros o cadenas o algún otro tipo de objeto básico (por ejemplo, una colección estándar de los anteriores), entonces no debería haber un problema (solo evitarán GC del cargador de clases de arranque / sistema, que es nunca va a suceder de todos modos).
Todavía es una buena práctica limpiar explícitamente un ThreadLocal cuando hayas terminado con él, pero en el caso del error citado log4j el cielo definitivamente no estaba cayendo (como puedes ver en el informe, el valor es un Hashtable vacío).
Aquí hay un código para demostrar. Primero, creamos una implementación de cargador de clases personalizado básico sin padre que imprime a System.out en la finalización:
import java.net.*;
public class CustomClassLoader extends URLClassLoader {
public CustomClassLoader(URL... urls) {
super(urls, null);
}
@Override
protected void finalize() {
System.out.println("*** CustomClassLoader finalized!");
}
}
Luego definimos una aplicación de controlador que crea una nueva instancia de este cargador de clases, la usa para cargar una clase con un ThreadLocal y luego elimina la referencia al cargador de clases, lo que permite que sea GC''ed. En primer lugar, en el caso donde el valor de ThreadLocal es una referencia a una clase cargada por el cargador de clases personalizado:
import java.net.*;
public class Main {
public static void main(String...args) throws Exception {
loadFoo();
while (true) {
System.gc();
Thread.sleep(1000);
}
}
private static void loadFoo() throws Exception {
CustomClassLoader cl = new CustomClassLoader(new URL("file:/tmp/"));
Class<?> clazz = cl.loadClass("Main$Foo");
clazz.newInstance();
cl = null;
}
public static class Foo {
private static final ThreadLocal<Foo> tl = new ThreadLocal<Foo>();
public Foo() {
tl.set(this);
System.out.println("ClassLoader: " + this.getClass().getClassLoader());
}
}
}
Cuando ejecutamos esto, podemos ver que CustomClassLoader no es basura (ya que el hilo local en el hilo principal tiene una referencia a una instancia de Foo que fue cargada por nuestro cargador de clases personalizado):
$ java Main ClassLoader: CustomClassLoader@7a6d084b
Sin embargo, cuando cambiamos el ThreadLocal para que contenga una referencia a un Entero simple en lugar de una instancia de Foo:
public static class Foo {
private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>();
public Foo() {
tl.set(42);
System.out.println("ClassLoader: " + this.getClass().getClassLoader());
}
}
Luego vemos que el cargador de clases personalizado ahora es basura (ya que el hilo local en el hilo principal solo tiene una referencia a un entero cargado por el cargador de clases del sistema):
$ java Main ClassLoader: CustomClassLoader@e76cbf7 *** CustomClassLoader finalized!
(Lo mismo es cierto con Hashtable). Entonces, en el caso de log4j no tenían una pérdida de memoria o ningún tipo de error. Ya estaban limpiando el Hashtable y esto fue suficiente para garantizar GC del cargador de clases. IMO, el error está en Tomcat, que registra indiscriminadamente estos errores "SEVEROS" en el cierre de todos los ThreadLocals que no han sido explícitamente eliminados () d, independientemente de si contienen una referencia fuerte a una clase de aplicación o no. Parece que al menos algunos desarrolladores están invirtiendo tiempo y esfuerzo en "arreglar" las fugas de memoria fantasma en los informes de los registros de Tomcat descuidados.
Las publicaciones anteriores explican el problema pero no proporcionan ninguna solución. Descubrí que no hay forma de "borrar" un ThreadLocal. En un entorno contenedor donde estoy manejando solicitudes, finalmente llamé a .remove () al final de cada solicitud. Me doy cuenta de que podría ser problemático utilizar transacciones gestionadas en contenedores.
Aquí hay una alternativa a ThreadLocal que no tiene el problema de pérdida de memoria:
class BetterThreadLocal<A> {
Map<Thread, A> map = Collections.synchronizedMap(new WeakHashMap());
A get() {
ret map.get(Thread.currentThread());
}
void set(A a) {
if (a == null)
map.remove(Thread.currentThread());
else
map.put(Thread.currentThread(), a);
}
}
Nota: Existe un nuevo escenario de pérdida de memoria, pero es muy poco probable y puede evitarse siguiendo una línea guía simple. El escenario mantiene una referencia fuerte a un objeto Thread en un BetterThreadLocal.
Nunca guardo referencias fuertes a los hilos de todos modos porque siempre quieres permitir que el hilo sea GC cuando su trabajo está terminado ... así que ahí tienes: un ThreadLocal libre de memoria.
Alguien debería comparar esto. Espero que sea tan rápido como el ThreadLocal de Java (ambos básicamente hacen una búsqueda débil del mapa hash, solo uno busca el hilo, el otro el ThreadLocal).
Y una nota final: mi sistema ( JavaX ) también realiza un seguimiento de todos los WeakHashMaps y los limpia regularmente, por lo que el último agujero súper improbable está conectado (WeakHashMaps de larga duración que nunca se consultan, pero que aún tienen entradas obsoletas).
Los agotamientos de PermGen en combinación con ThreadLocal
menudo son causados por fugas del cargador de clases .
Un ejemplo:
Imagine un servidor de aplicaciones que tiene un conjunto de subprocesos de trabajo .
Se mantendrán vivos hasta la finalización del servidor de aplicaciones.
Una aplicación web implementada utiliza un ThreadLocal
estático en una de sus clases para almacenar algunos datos locales de subprocesos, una instancia de otra clase (vamos a llamarla SomeClass
) de la aplicación web. Esto se hace dentro del hilo de trabajo (por ejemplo, esta acción se origina a partir de una solicitud HTTP ).
Importante:
Por definición , se mantiene una referencia a un valor de ThreadLocal
hasta que el hilo "propietario" muere o si el ThreadLocal en sí no se puede alcanzar.
Si la aplicación web no borra la referencia a ThreadLocal
al apagar , sucederán cosas malas:
Debido a que el hilo de trabajo generalmente nunca morirá y la referencia a ThreadLocal
es estática, el valor ThreadLocal
aún hace referencia a la instancia de SomeClass
, una clase de aplicación web, ¡ incluso si la aplicación web se ha detenido!
Como consecuencia, el cargador de clases de la aplicación web no se puede recolectar basura , lo que significa que todas las clases (y todos los datos estáticos) de la aplicación web permanecen cargados (esto afecta al grupo de memoria PermGen así como al montón).
Cada iteración de redistribución de la aplicación web aumentará el uso de permgen (y montón).
=> Esta es la fuga de permgen
Un ejemplo popular de este tipo de fuga es este error en log4j (corregido mientras tanto).