Pregunta Creando una pérdida de memoria con Java


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

¿Qué sería un ejemplo?


2634


origen


Respuestas:


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

  1. La aplicación crea un hilo de larga ejecución (o utiliza un grupo de subprocesos para filtrar incluso más rápido).
  2. El hilo carga una clase a través de un ClassLoader (opcionalmente personalizado).
  3. La clase asigna una gran cantidad de memoria (p. new byte[1000000]), almacena una referencia fuerte en un campo estático y luego almacena una referencia a sí mismo en un ThreadLocal. La asignación de la memoria extra es opcional (la fuga de la instancia de Clase es suficiente), pero hará que la fuga funcione mucho más rápido.
  4. El subproceso borra todas las referencias a la clase personalizada o al ClassLoader del que se cargó.
  5. Repetir.

Esto funciona porque ThreadLocal guarda una referencia al objeto, que guarda una referencia a su Clase, que a su vez guarda 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 Classes y ClassLoaders se asignaron directamente a permgen y nunca se obtuvieron GC. Sin embargo, independientemente de cómo maneje JVM la descarga de clases, un ThreadLocal evitará una Objeto de clase de ser reclamado.)

Una variación de este patrón es la razón por la que los contenedores de aplicaciones (como Tomcat) pueden filtrar la memoria como un tamiz si con frecuencia vuelves a desplegar aplicaciones que usan ThreadLocals de alguna manera. (Dado que el contenedor de aplicaciones usa Threads como se describe, y cada vez que vuelva a implementar la aplicación, se usa un nuevo ClassLoader).

Actualizar: Como mucha gente sigue pidiéndolo, aquí hay un código de ejemplo que muestra este comportamiento en acción.


1931



Campo estático que contiene referencia de objeto [esp campo final]

class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}

Vocación String.intern() en una cadena larga

String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();

(No cerrado) abrir streams (archivo, red, etc.)

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Conexiones no cerradas

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

Áreas que son inalcanzables del recolector de basura de JVM, como memoria asignada a través de métodos nativos

En las aplicaciones web, algunos objetos se almacenan en el ámbito de la aplicación hasta que la aplicación se detiene o elimina explícitamente.

getServletContext().setAttribute("SOME_MAP", map);

Opciones de JVM incorrectas o inapropiadas, tales como el noclassgc opción en IBM JDK que evita la recolección de basura de la clase no utilizada

Ver Configuración jdk de IBM.


1059



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

Si desea que estas claves / elementos malos se queden, puede usar 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.

390



A continuación, habrá un caso no obvio en el que se filtra Java, además del caso estándar de oyentes olvidados, referencias estáticas, claves falsas / modificables en hashmaps, o simplemente hilos atrapados sin posibilidad de finalizar su ciclo de vida.

  • File.deleteOnExit()- Siempre pierde el hilo, si la cadena es una subcadena, la fuga es aún peor (el char subyacente [] también se filtró) - en la subcadena Java 7 también copia el char[], entonces el último no aplica; @Daniel, sin necesidad de votos, sin embargo.

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

  • Runtime.addShutdownHook y no eliminar ... y luego incluso con removeShutdownHook debido a un error en la clase ThreadGroup con respecto a los hilos no iniciados que pueden no recogerse, se produce un escape efectivo del ThreadGroup. JGroup tiene la fuga en GossipRouter.

  • Creando, pero no comenzando, un Thread entra en la misma categoría que arriba.

  • Crear un hilo hereda el ContextClassLoader y AccessControlContext, más el ThreadGroup y cualquier InheritedThreadLocal, todas esas referencias son filtraciones potenciales, junto con las clases completas cargadas por el cargador de clases y todas las referencias estáticas, y ja-ja. El efecto es especialmente visible con todo el marco j.u.c.Executor que cuenta con un super simple ThreadFactory interfaz, sin embargo, la mayoría de los desarrolladores no tienen idea del peligro que acecha. Además, muchas bibliotecas inician subprocesos a petición (demasiadas bibliotecas populares de la industria).

  • ThreadLocal cachés; esos son malvados en muchos casos. Estoy seguro de que todo el mundo ha visto bastantes cachés simples basados ​​en ThreadLocal, y las malas noticias: si el hilo sigue funcionando más de lo esperado, la vida del contexto ClassLoader es una pequeña filtración pura y agradable. No use cachés de ThreadLocal a menos que realmente lo necesite.

  • Vocación ThreadGroup.destroy() cuando el ThreadGroup no tiene subprocesos, pero aún mantiene secundarios ThreadGroups. Una fuga deficiente que evitará que ThreadGroup se elimine de su principal, pero todos los elementos secundarios no se podrán enumerar.

  • El uso de WeakHashMap y el valor (en) hace referencia directamente a la clave. Es difícil encontrar uno sin un montón de basura. Eso aplica a todos los extendidos Weak/SoftReference eso podría mantener una referencia dura de nuevo al objeto guardado.

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

  • Utilizando InflaterInputStream paso new java.util.zip.Inflater() en el constructor (PNGImageDecoder por ejemplo) y no llamar end() del inflador Bueno, si pasas el constructor con solo new, no hay oportunidad ... Y sí, llamando close() en la secuencia no cierra el inflador si se pasa manualmente como parámetro de constructor. Esta no es una verdadera filtración ya que sería liberada por el finalizador ... cuando lo considere necesario. Hasta ese momento se come tanto la memoria nativa que puede causar que Linux oom_killer mate el proceso con impunidad. El problema principal es que la finalización en Java es muy poco confiable y G1 empeoró hasta 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 tiene mucha 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. Por suerte, Deflater no se usa ampliamente y, que yo sepa, JDK no contiene ningún uso indebido. Siempre llama end() si creas manualmente un Deflater o Inflater. La mejor parte de los dos últimos: no puede encontrarlos a través de las herramientas normales de creación de perfiles disponibles.

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

Buena suerte y cuídate; ¡las filtraciones son malvadas!


228



La mayoría de los ejemplos aquí son "demasiado complejos". Son casos extremos. Con estos ejemplos, el programador cometió un error (como no redefinir equals / hashcode), o ha sido mordido por un caso de esquina de la 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 pérdidas de memoria. El recolector de basura solo libera lo que ya no se referencia. 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 tiende a tener estado compartido. Puede ser cualquier cosa, estática, singleton ... A menudo, las aplicaciones no triviales tienden a hacer gráficos de objetos complejos. Solo olvidando establecer una referencia a nulo o más a menudo olvidando eliminar un objeto de una colección es suficiente para hacer una pérdida de memoria.

Por supuesto, todo tipo de oyentes (como los oyentes UI), cachés o cualquier estado compartido de larga duración tienden a producir pérdida de memoria si no se manejan adecuadamente. Lo que se debe entender es que este no es un caso de esquina de Java, o un problema con el recolector de basura. Es un problema de diseño. Diseñamos que agreguemos un oyente a un objeto de larga duración, pero no eliminamos el oyente cuando ya no se necesita. Almacenamos objetos en caché, pero no tenemos ninguna estrategia para eliminarlos del caché.

Es posible que tengamos un gráfico complejo que almacena el estado anterior que necesita un cálculo. Pero el estado previo está vinculado al estado anterior y así sucesivamente.

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

Una fuga de memoria es realmente posible y es perfectamente predecible. No es necesario contar con funciones especiales de idiomas o casos de esquina. Las fugas de memoria son un indicador de que tal vez falta algo o incluso de problemas de diseño.


150



La respuesta depende completamente de lo que el entrevistador pensó que estaban preguntando.

¿Es posible en la práctica hacer que Java escape? Por supuesto que sí, y hay muchos ejemplos en las otras respuestas.

Pero hay múltiples meta preguntas que pueden haberse preguntado.

  • ¿Es teóricamente una implementación Java "perfecta" vulnerable a fugas?
  • ¿El candidato entiende la diferencia entre teoría y realidad?
  • ¿El candidato entiende cómo funciona la recolección de basura?
  • ¿O cómo se supone que la recolección de basura funciona en un caso ideal?
  • ¿Saben que pueden llamar a otros idiomas a través de interfaces nativas?
  • ¿Saben que pierde memoria en esos otros idiomas?
  • ¿El candidato sabe incluso qué es la administración de memoria y qué está sucediendo detrás de la escena en Java?

Estoy leyendo su meta-pregunta como "Cuál es la respuesta que podría haber usado en esta situación de entrevista". Y, por lo tanto, me centraré en las habilidades de entrevista en lugar de Java. Creo que es más probable que repita la situación de no saber la respuesta a una pregunta en una entrevista que estar en un lugar en el que necesita saber cómo hacer que Java escape. Entonces, con suerte, esto ayudará.

Una de las habilidades más importantes que puede desarrollar para entrevistar es aprender a escuchar activamente las preguntas y trabajar con el entrevistador para extraer su intención. Esto no solo te permite responder a su pregunta de la manera que ellos quieren, sino que también muestra que tienes algunas habilidades vitales de comunicación. Y cuando se trata de elegir entre muchos desarrolladores igualmente talentosos, contrataré a quien escuche, piense y entienda antes de responder siempre.


133



El siguiente es un ejemplo bastante inútil, si no entiendes JDBC. O al menos cómo JDBC espera que cierre un desarrollador Connection, Statement y ResultSet instancias antes de descartarlos o perder referencias a ellos, en lugar de confiar en la implementación de finalize.

void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}

El problema con lo anterior es que Connection objeto no está cerrado, y por lo tanto la conexión física permanecerá abierta, hasta que el recolector de basura se da cuenta de que es inalcanzable. GC invocará el finalize método, pero hay controladores JDBC que no implementan finalize, al menos no de la misma manera que Connection.close está implementado. El comportamiento resultante es que, si bien la memoria se recuperará debido a la recolección de objetos inalcanzables, los recursos (incluida la memoria) asociados con el Connection objeto simplemente no puede ser reclamado.

En tal caso donde el Connectiones finalize método no limpia todo, uno podría encontrar que la conexión física al servidor de la base de datos durará varios ciclos de recolección de basura, hasta que el servidor de la base de datos finalmente descubra que la conexión no está activa (si es así) y debería cerrarse.

Incluso si el controlador JDBC fuera a implementar finalize, es posible que se emitan excepciones durante la finalización. El comportamiento resultante es que cualquier memoria asociada con el objeto ahora "inactivo" no será reclamada, como finalize se garantiza que se invoque solo una vez.

El escenario anterior de encontrar excepciones durante la finalización de un objeto está relacionado con otro escenario que podría provocar una pérdida de memoria: la resurrección del objeto. La resurrección de objetos a menudo se hace intencionalmente al crear una referencia fuerte al objeto desde su finalización, desde otro objeto. Cuando la resurrección de objetos es mal utilizada, se producirá una pérdida de memoria en combinación con otras fuentes de pérdidas de memoria.

Hay muchos más ejemplos que puedes conjurar, como

  • Administrando un List instancia en la que solo está agregando a la lista y no la está eliminando (aunque debería deshacerse de elementos que ya no necesita), o
  • Apertura Sockets o Files, pero no cerrándolos cuando ya no son necesarios (similar al ejemplo anterior relacionado con el Connection clase).
  • No descarga Singletons cuando se baja una aplicación Java EE. Aparentemente, el Classloader que cargó la clase singleton retendrá una referencia a la clase y, por lo tanto, nunca se recopilará la instancia singleton. Cuando se implementa una nueva instancia de la aplicación, generalmente se crea un nuevo cargador de clases, y el anterior cargador de clases continuará existiendo debido al singleton.

113



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 de matriz que ya no se usa (elementData[--size] = null)? Esa referencia podría mantener vivo un gran objeto ...


102



Cada vez que mantiene referencias a objetos que ya no necesita, tiene una pérdida de memoria. Ver Manejo de fugas de memoria en programas Java para ejemplos de cómo las pérdidas de memoria se manifiestan en Java y qué puede hacer al respecto.


63



Usted puede hacer que la memoria se escape sun.misc.Unsafe clase. De hecho, esta clase de servicio se utiliza en diferentes clases estándar (por ejemplo, en java.nio clases). No puedes crear una instancia de esta clase directamentepero puedes usa la reflexión para hacer eso.

El código no se compila en Eclipse IDE - compilarlo usando el comando javac (durante la compilación obtendrás 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();
        }
    }

}

43



Puedo copiar mi respuesta desde aquí: La forma más fácil de causar pérdida de memoria en Java?

"Una pérdida de memoria, en ciencias de la computación (o fugas, en este contexto), ocurre cuando un programa de computadora consume memoria pero no puede volver a liberarla al sistema operativo". (Wikipedia)

La respuesta fácil es: no puedes. Java hace gestión automática de memoria y liberará recursos que no son necesarios para usted. No puedes evitar que esto suceda. SIEMPRE podrá liberar los recursos. En los programas con gestión de memoria manual, esto es diferente. No puedes obtener algo de memoria en C usando malloc (). Para liberar la memoria, necesita el puntero que devuelve malloc y llamar a free () en él. Pero si ya no tiene el puntero (sobrescrito, o se excedió la vida útil), desafortunadamente no puede liberar esta memoria y, por lo tanto, tiene una pérdida de memoria.

Todas las otras respuestas hasta ahora están en mi definición, no son realmente pérdidas de memoria. Todos apuntan a llenar la memoria con cosas sin sentido muy rápido. Pero en cualquier momento, aún se podía desreferenciar los objetos que se crearon y liberar así la memoria -> SIN FUGAS. la respuesta de acconrad se acerca bastante, como tengo que admitir, ya que su solución es simplemente "bloquear" al recolector de basura forzándola en un bucle infinito).

La respuesta larga es: puede obtener una pérdida de memoria escribiendo una biblioteca para Java utilizando el JNI, que puede tener administración de memoria manual y así tener pérdidas de memoria. Si llama a esta biblioteca, su proceso java perderá memoria. O bien, puede tener errores en la JVM, de modo que la JVM pierda memoria. Probablemente hay errores en la JVM, incluso puede haber algunos conocidos, ya que la recolección de basura no es tan trivial, pero sigue siendo un error. Por diseño esto no es posible. Es posible que solicite un código Java que se vea afectado por dicho error. Lo siento, no conozco uno y, de todos modos, podría no ser un error en la próxima versión de Java.


37