tools - memory leak java




Creando una pérdida de memoria con Java (20)

A continuación, habrá un caso no obvio en el que las fugas de Java, además del caso estándar de escuchas olvidadas, referencias estáticas, claves falsas / modificables en hashmaps, o solo hilos atascados sin ninguna posibilidad de terminar su ciclo de vida.

  • File.deleteOnExit() : siempre File.deleteOnExit() la cadena, si la cadena es una subcadena, la fuga es aún peor (el carácter subyacente [] también se filtra) - en la subcadena Java 7 también copia el char[] , por lo que no se aplica más adelante ; @Daniel, sin embargo, no hay necesidad de votos.

Me concentraré en los hilos para mostrar el peligro de los hilos no administrados en su mayoría, no deseo siquiera tocar el swing.

  • Runtime.addShutdownHook y no elimina ... e incluso con removeShutdownHook debido a un error en la clase ThreadGroup con respecto a subprocesos no iniciados que pueden no recopilarse, efectivamente filtrar ThreadGroup. JGroup tiene la fuga en GossipRouter.

  • Al crear, pero no comenzar, un Thread entra en la misma categoría que arriba.

  • La creación de un subproceso hereda el ContextClassLoader y AccessControlContext , más el ThreadGroup y cualquier InheritedThreadLocal , todas esas referencias son posibles fugas, junto con todas las clases cargadas por el cargador de clases y todas las referencias estáticas, y ja-ja. El efecto es especialmente visible con todo el marco jucExecutor que presenta una interfaz ThreadFactory super simple, pero la mayoría de los desarrolladores no tienen ni idea del peligro que acecha. También muchas bibliotecas inician subprocesos a pedido (también muchas bibliotecas populares de la industria).

  • ThreadLocal caches; Esos son malos en muchos casos. Estoy seguro de que todo el mundo ha visto bastantes cachés simples basados ​​en ThreadLocal, bueno, la mala noticia: si el hilo continúa más de lo esperado la vida del contexto ClassLoader, es una pequeña fuga. No utilice cachés ThreadLocal a menos que sea realmente necesario.

  • Llamar a ThreadGroup.destroy() cuando ThreadGroup no tiene subprocesos en sí, pero aún mantiene ThreadGroups secundarios. Una mala fuga que evitará que ThreadGroup se elimine de su padre, pero todos los niños se vuelven insumibles.

  • El uso de WeakHashMap y el valor (en) hace referencia directamente a la clave. Esto es difícil de encontrar sin un volcado de pila. Eso se aplica a todas las referencias Weak/SoftReference extendidas que podrían mantener una referencia difícil al objeto protegido.

  • Usando java.net.URL con el protocolo HTTP (S) y cargando el recurso desde (!). Este es especial, KeepAliveCache crea un nuevo subproceso en el sistema ThreadGroup que pierde el cargador de clases de contexto del subproceso actual. El hilo se crea en la primera solicitud cuando no existe un hilo vivo, por lo que puede tener suerte o simplemente perder. La fuga ya está reparada en Java 7 y el código que crea el subproceso elimina correctamente el cargador de clases de contexto. Hay pocos casos más ( como ImageFetcher , también arreglado ) de crear hilos similares.

  • Usando InflaterInputStream pasando un new java.util.zip.Inflater() en el constructor ( PNGImageDecoder por ejemplo) y no llamando a end() del inflater. Bueno, si pasas el constructor con solo algo new , no hay posibilidad ... Y sí, llamar a close() en la secuencia no cierra el inflater si se pasa manualmente como parámetro de constructor. Esto no es una fuga verdadera, ya que sería lanzado por el finalizador ... cuando lo considere necesario. Hasta ese momento, se come la memoria nativa y puede hacer que Linux oom_killer mate el proceso con impunidad. El principal problema es que la finalización en Java es muy poco confiable y G1 lo empeoró hasta la 7.0.2. Moraleja de la historia: libera recursos nativos tan pronto como puedas; el finalizador es demasiado pobre.

  • El mismo caso con java.util.zip.Deflater . Este es mucho peor ya que Deflater está hambriento de memoria en Java, es decir, siempre usa 15 bits (máximo) y 8 niveles de memoria (9 es máximo) asignando varios cientos de KB de memoria nativa. Afortunadamente, Deflater no se usa mucho y, según mi conocimiento, JDK no contiene malos usos. Siempre llame a end() si crea manualmente un Deflater o Inflater . La mejor parte de los dos últimos: no puede encontrarlos a través de las herramientas de perfiles normales disponibles.

(Puedo agregar más pérdidas de tiempo que he encontrado a pedido).

Buena suerte y cuídate; ¡Las fugas son malas!

Acabo de tener una entrevista y me pidieron que creara una pérdida de memoria con Java. No hace falta decir que me sentí bastante tonto al no tener ni idea de cómo empezar a crear uno.

¿Cuál sería un ejemplo?



Esta es una buena manera de crear una verdadera pérdida de memoria (objetos inaccesibles al ejecutar el código pero aún almacenados en la memoria) en Java puro:

  1. La aplicación crea un subproceso de larga duración (o usa un conjunto de subprocesos para filtrarse aún más rápido).
  2. El hilo carga una clase a través de un ClassLoader (opcionalmente personalizado).
  3. La clase asigna una gran parte de la memoria (por ejemplo, new byte[1000000] ), almacena una fuerte referencia a ella en un campo estático y luego almacena una referencia a sí misma en un ThreadLocal. Asignar la memoria adicional es opcional (filtrar la instancia de Clase es suficiente), pero hará que la fuga funcione mucho más rápido.
  4. El hilo borra todas las referencias a la clase personalizada o al ClassLoader desde el que se cargó.
  5. Repetir.

Esto funciona porque ThreadLocal mantiene una referencia al objeto, que mantiene una referencia a su Clase, que a su vez mantiene una referencia a su ClassLoader. El ClassLoader, a su vez, mantiene una referencia a todas las Clases que ha cargado.

(Fue peor en muchas implementaciones de JVM, especialmente antes de Java 7, porque las Clases y los ClassLoaders se asignaron directamente a permgen y nunca recibieron GC. Sin embargo, independientemente de cómo JVM maneja la descarga de clases, un ThreadLocal todavía evitará Objeto de clase de ser reclamado.)

Una variación de este patrón es la razón por la cual los contenedores de aplicaciones (como Tomcat) pueden perder memoria como un tamiz si frecuentemente se vuelven a implementar las aplicaciones que usan ThreadLocals de alguna manera. (Dado que el contenedor de la aplicación utiliza subprocesos como se describe, y cada vez que vuelva a implementar la aplicación, se utiliza un nuevo ClassLoader.)

Actualización : dado que muchas personas siguen pidiéndolo, aquí hay un código de ejemplo que muestra este comportamiento en acción .


La mayoría de los ejemplos aquí son "demasiado complejos". Son casos de borde. Con estos ejemplos, el programador cometió un error (como no redefinir es igual a / hashcode), o ha sido mordido por un caso de esquina de JVM / JAVA (carga de clase con estática ...). Creo que ese no es el tipo de ejemplo que quiere un entrevistador o incluso el caso más común.

Pero hay casos realmente más simples para las fugas de memoria. El recolector de basura solo libera lo que ya no está referenciado. Nosotros, como desarrolladores de Java, no nos importa la memoria. Lo asignamos cuando es necesario y lo liberamos automáticamente. Multa.

Pero cualquier aplicación de larga duración suele tener estado compartido. Puede ser cualquier cosa, estática, singletons ... A menudo, las aplicaciones no triviales tienden a hacer gráficos de objetos complejos. El simple hecho de olvidarse de establecer una referencia en nulo o, con más frecuencia, olvidar eliminar un objeto de una colección es suficiente para provocar una pérdida de memoria.

Por supuesto, todo tipo de escuchas (como escuchas de UI), cachés o cualquier estado compartido de larga duración tienden a producir una pérdida de memoria si no se manejan adecuadamente. Lo que debe entenderse es que esto no es un caso de Java, o un problema con el recolector de basura. Es un problema de diseño. Diseñamos que agregamos un oyente a un objeto de larga duración, pero no lo eliminamos cuando ya no es necesario. Cacheamos objetos, pero no tenemos una estrategia para eliminarlos de la caché.

Tal vez tengamos un gráfico complejo que almacene el estado anterior que es necesario para un cálculo. Pero el estado anterior está vinculado al estado anterior y así sucesivamente.

Como tenemos que cerrar conexiones o archivos de SQL. Necesitamos establecer referencias adecuadas en nulo y eliminar elementos de la colección. Tendremos estrategias de almacenamiento en caché adecuadas (tamaño máximo de memoria, número de elementos o temporizadores). Todos los objetos que permiten notificar a un oyente deben proporcionar tanto un método addListener como un método removeListener. Y cuando estos notificadores ya no se utilizan, deben borrar su lista de oyentes.

Una pérdida de memoria es realmente posible y es perfectamente predecible. No hay necesidad de características especiales de lenguaje o casos de esquina. Las fugas de memoria son un indicador de que falta algo o incluso de problemas de diseño.


Probablemente uno de los ejemplos más simples de una posible pérdida de memoria, y cómo evitarlo, es la implementación de ArrayList.remove (int):

public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}

Si lo estuviera implementando usted mismo, ¿habría pensado borrar el elemento del arreglo que ya no se usa ( elementData[--size] = null )? Esa referencia podría mantener vivo un gran objeto ...


Puedes hacer una pérdida de memoria con la clase sun.misc.Unsafe . De hecho, esta clase de servicio se utiliza en diferentes clases estándar (por ejemplo, en clases java.nio ). No puede crear una instancia de esta clase directamente , pero puede usar la reflexión para hacer eso .

El código no se compila en el IDE de Eclipse: compílelo usando el comando javac (durante la compilación obtendrá advertencias)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;


public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }

}

Una cosa simple a hacer es usar un HashSet con un código de hash incorrecto (o inexistente) hashCode() o equals() , y luego seguir agregando "duplicados". En lugar de ignorar los duplicados como debería, el conjunto solo crecerá y no podrás eliminarlos.

Si desea que estas claves / elementos defectuosos permanezcan cerca, puede utilizar un campo estático como

class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

¿Qué es una pérdida de memoria?

  • Es causado por un error o mal diseño.
  • Es un desperdicio de memoria.
  • Se empeora con el tiempo.
  • El recolector de basura no puede limpiarlo.

Ejemplo típico:

Un caché de objetos es un buen punto de partida para desordenar las cosas.

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

Tu caché crece y crece. Y muy pronto toda la base de datos será absorbida por la memoria. Un diseño mejor utiliza un LRUMap (solo mantiene los objetos utilizados recientemente en la memoria caché).

Claro, puedes hacer las cosas mucho más complicadas:

  • utilizando construcciones ThreadLocal .
  • Añadiendo árboles de referencia más complejos .
  • o fugas causadas por bibliotecas de terceros .

Lo que pasa a menudo:

Si este objeto de información tiene referencias a otros objetos, que también tienen referencias a otros objetos. En cierto modo, también podría considerar que se trata de algún tipo de pérdida de memoria (causada por un mal diseño).


Creo que un ejemplo válido podría ser utilizar variables ThreadLocal en un entorno donde se agrupan los hilos.

Por ejemplo, utilizando las variables ThreadLocal en Servlets para comunicarse con otros componentes web, el contenedor crea las hebras y mantiene las inactivas en un grupo. Las variables ThreadLocal, si no se limpian correctamente, permanecerán allí hasta que, posiblemente, el mismo componente web sobrescriba sus valores.

Por supuesto, una vez identificado, el problema se puede resolver fácilmente.


El entrevistador podría haber estado buscando una solución de referencia circular:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

Este es un problema clásico con el recuento de referencias recolectores de basura. Luego explicaría cortésmente que las JVM utilizan un algoritmo mucho más sofisticado que no tiene esta limitación.

-Wes Tarle


Tal vez utilizando código nativo externo a través de JNI?

Con Java puro, es casi imposible.

Pero eso se trata de un tipo "estándar" de pérdida de memoria, cuando ya no puede acceder a la memoria, pero aún es propiedad de la aplicación. En su lugar, puede mantener las referencias a los objetos no utilizados o abrir secuencias sin cerrarlas después.


Aquí hay uno simple / siniestro a través de http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .

public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}

Debido a que la subcadena se refiere a la representación interna de la cadena original, mucho más larga, la original permanece en la memoria. Por lo tanto, siempre que tenga un StringLeaker en juego, también tendrá toda la cadena original en la memoria, aunque piense que solo está sosteniendo una cadena de un solo carácter.

La forma de evitar almacenar una referencia no deseada a la cadena original es hacer algo como esto:

...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...

Para mayor maldad, también puede ser .intern()la subcadena:

...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...

Si lo hace, mantendrá la cadena larga original y la subcadena derivada en la memoria incluso después de que se haya descartado la instancia de StringLeaker.


Cree un mapa estático y siga agregando referencias difíciles. Esos nunca serán GC'd.

public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

El entrevistador probablemente estaba buscando una referencia circular como el código a continuación (que, por cierto, solo pierde memoria en JVM muy antiguas que usaban el conteo de referencias, que ya no es el caso) Pero es una pregunta bastante vaga, por lo que es una excelente oportunidad para demostrar su comprensión de la gestión de memoria JVM.

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

Luego puede explicar que con el recuento de referencias, el código anterior perdería memoria. Pero la mayoría de las JVM modernas ya no usan el conteo de referencias, la mayoría usa un recolector de basura de barrido, que de hecho recopilará esta memoria.

A continuación, puede explicar cómo crear un Objeto que tenga un recurso nativo subyacente, como este:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

Luego, puede explicar que esto es técnicamente una pérdida de memoria, pero en realidad la pérdida es causada por el código nativo en la JVM que asigna recursos nativos subyacentes, que no fueron liberados por su código Java.

Al final del día, con una JVM moderna, necesita escribir algún código Java que asigne un recurso nativo fuera del alcance normal de la conciencia de la JVM.


Los hilos no se recogen hasta que terminan. Sirven como roots de la recolección de basura. Son uno de los pocos objetos que no se recuperarán simplemente olvidándolos o borrando referencias a ellos.

Considere: el patrón básico para terminar un hilo de trabajo es establecer alguna variable de condición vista por el hilo. El hilo puede verificar la variable periódicamente y usarla como una señal para terminar. Si la variable no está declarada volatile, entonces el hilo no verá el cambio en la variable, por lo que no sabrá que termine. O imagínese si algunos subprocesos desean actualizar un objeto compartido, pero un interbloqueo al intentar bloquearlo.

Si solo tiene un puñado de hilos, estos errores probablemente serán obvios porque su programa dejará de funcionar correctamente. Si tiene un grupo de subprocesos que crea más subprocesos según sea necesario, es posible que los subprocesos obsoletos / atascados no se noten y se acumulen indefinidamente, provocando una pérdida de memoria. Es probable que los subprocesos utilicen otros datos en su aplicación, por lo que también evitará que se recopilen los datos a los que hacen referencia directamente.

Como ejemplo de juguete:

static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}

Llama a System.gc()todos los que quieras, pero el objeto que se pasa leakMenunca morirá.

(* editado *)


Me encontré con un tipo más sutil de fuga de recursos recientemente. Abrimos recursos a través del cargador de clases getResourceAsStream y sucedió que los manejadores de flujo de entrada no estaban cerrados.

Uhm, podrías decir, que idiota.

Bueno, lo que lo hace interesante es: de esta manera, puede perder memoria de pila del proceso subyacente, en lugar de hacerlo de la pila de JVM.

Todo lo que necesita es un archivo jar con un archivo dentro del cual se hará referencia desde el código Java. Cuanto más grande es el archivo jar, más rápido se asigna la memoria.

Puede crear fácilmente un frasco de este tipo con la siguiente clase:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

Solo pegue en un archivo llamado BigJarCreator.java, compile y ejecútelo desde la línea de comando:

javac BigJarCreator.java
java -cp . BigJarCreator

Et voilà: encuentra un archivo jar en su directorio de trabajo actual con dos archivos dentro.

Vamos a crear una segunda clase:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

Esta clase básicamente no hace nada, pero crea objetos InputStream sin referencia. Esos objetos se recolectarán de inmediato y, por lo tanto, no contribuyen al tamaño del montón. Es importante para nuestro ejemplo cargar un recurso existente desde un archivo jar, ¡y el tamaño sí importa aquí!

Si tiene dudas, intente compilar e iniciar la clase anterior, pero asegúrese de elegir un tamaño de pila decente (2 MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

No encontrará un error de OOM aquí, ya que no se guardan referencias, la aplicación continuará ejecutándose sin importar el tamaño que elija ITERATIONS en el ejemplo anterior. El consumo de memoria de su proceso (visible en la parte superior (RES / RSS) o en el explorador de procesos) aumenta a menos que la aplicación llegue al comando de espera. En la configuración anterior, asignará alrededor de 150 MB en memoria.

Si desea que la aplicación sea segura, cierre la secuencia de entrada donde se creó:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

y su proceso no superará los 35 MB, independientemente del recuento de iteraciones.

Bastante simple y sorprendente.


Pensé que era interesante que nadie usara los ejemplos internos de clase. Si tienes una clase interna; mantiene inherentemente una referencia a la clase contenedora. Por supuesto, técnicamente no es una pérdida de memoria porque Java finalmente lo limpiará; pero esto puede hacer que las clases permanezcan más tiempo de lo previsto.

public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}

Ahora, si llama a Example1 y obtiene un Example2 descartando Example1, intrínsecamente tendrá un enlace a un objeto Example1.

public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}

También escuché el rumor de que si tiene una variable que existe por más tiempo que una cantidad específica de tiempo; Java asume que siempre existirá y que en realidad nunca intentará limpiarlo si no se puede encontrar en el código. Pero eso está completamente sin verificar.


Puede crear una pérdida de memoria en movimiento creando una nueva instancia de una clase en el método de finalización de esa clase. Puntos de bonificación si el finalizador crea múltiples instancias. Aquí hay un programa simple que pierde todo el montón en algún momento entre unos pocos segundos y unos minutos, según el tamaño de tu montón:

class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

Tome cualquier aplicación web que se ejecute en cualquier contenedor de servlets (Tomcat, Jetty, Glassfish, lo que sea ...). Vuelva a implementar la aplicación 10 o 20 veces seguidas (puede ser suficiente con simplemente tocar el WAR en el directorio de despliegue automático del servidor).

A menos que alguien haya probado esto realmente, es probable que obtenga un OutOfMemoryError después de un par de redistribuciones, ya que la aplicación no se preocupó de limpiar después de la misma. Incluso puede encontrar un error en su servidor con esta prueba.

El problema es que la vida útil del contenedor es más larga que la vida útil de su aplicación. Debe asegurarse de que todas las referencias que pueda tener el contenedor a objetos o clases de su aplicación puedan ser recolectadas en la basura.

Si solo hay una referencia que sobrevive a la desinstalación de su aplicación web, el cargador de clases correspondiente y, por consiguiente, todas las clases de su aplicación web no se pueden recolectar.

Los subprocesos iniciados por su aplicación, las variables ThreadLocal y los agregadores de registros son algunos de los sospechosos habituales que causan fugas en el cargador de clases.


Un ejemplo común de esto en el código GUI es cuando se crea un widget / componente y se agrega un oyente a algún objeto estático / con ámbito de aplicación y luego no se elimina el escucha cuando se destruye el widget. No solo obtienes una pérdida de memoria, sino también un impacto en el rendimiento, ya que cuando escuchas los eventos, todos tus oyentes son llamados también.





memory-leaks