¿Cómo crear una pérdida de memoria en Java?


3224

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 sin tener idea de cómo empezar a crear uno.

¿Cuál sería un ejemplo?


275
Les diría que Java utiliza un recolector de basura, y pedirles que ser un poco más específico acerca de su definición de "pérdida de memoria", explicando que - salvo errores JVM - Java no puede perder memoria en todo el camino misma C / C ++ puede. Tienes que tener una referencia al objeto en alguna parte .
Darien

371
Me parece curioso que en la mayoría de las respuestas las personas estén buscando esos casos y trucos de borde y parezcan estar perdiendo completamente el punto (IMO). Podrían simplemente mostrar código que mantenga referencias inútiles a objetos que nunca volverán a usar, y al mismo tiempo nunca descartarán esas referencias; se puede decir que esos casos no son pérdidas de memoria "verdaderas" porque todavía hay referencias a esos objetos, pero si el programa nunca usa esas referencias nuevamente y tampoco las descarta, es completamente equivalente a (y tan malo como) un " verdadera pérdida de memoria ".
ehabkost

6262
Honestamente, no puedo creer que la pregunta similar que hice sobre "Ir" se haya votado a -1. Aquí: stackoverflow.com/questions/4400311/... Básicamente, las pérdidas de memoria de las que estaba hablando son las que obtuvieron +200 votos a favor del OP y, sin embargo, fui atacado e insultado por preguntar si "Go" tenía el mismo problema. De alguna manera, no estoy seguro de que todo el wiki funcione tan bien.
SyntaxT3rr0r

74
@ SyntaxT3rr0r: la respuesta de Darien no es fanboyism. él admitió explícitamente que ciertas JVM pueden tener errores que significan que la memoria se pierde. Esto es diferente de la especificación del lenguaje en sí, permitiendo pérdidas de memoria.
Peter Recore

31
@ehabkost: No, no son equivalentes. (1) Usted posee la capacidad de recuperar la memoria, mientras que en una "verdadera fuga" su programa C / C ++ olvida el rango asignado, no hay una forma segura de recuperarse. (2) Puede detectar fácilmente el problema con la creación de perfiles, porque puede ver qué objetos implica la "hinchazón". (3) Una "fuga verdadera" es un error inequívoco, mientras que un programa que mantiene muchos objetos alrededor hasta que se termina puede ser una parte deliberada de cómo debe funcionar.
Darien

Respuestas:


2309

Aquí hay una buena manera 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 subproceso de larga ejecución (o usa un grupo de subprocesos para filtrar aún más rápido).
  2. El hilo carga una clase a través de un (opcionalmente personalizado) ClassLoader.
  3. La clase asigna una gran cantidad de memoria (por ejemplo new byte[1000000]), almacena una referencia fuerte a ella en un campo estático y luego almacena una referencia a sí misma en a ThreadLocal. La asignación de memoria adicional es opcional (filtrar la instancia de clase es suficiente), pero hará que la fuga funcione mucho más rápido.
  4. La aplicación borra todas las referencias a la clase personalizada o desde la ClassLoaderque se cargó.
  5. Repetir.

Debido a la forma en que ThreadLocalse implementa en el JDK de Oracle, esto crea una pérdida de memoria:

  • Cada uno Threadtiene un campo privado threadLocals, que en realidad almacena los valores locales del hilo.
  • Cada clave en este mapa es una referencia débil a un ThreadLocalobjeto, por lo que después de que ese ThreadLocalobjeto es recolectado como basura, su entrada se elimina del mapa.
  • Pero cada valor es una referencia fuerte, por lo que cuando un valor (directa o indirectamente) apunta al ThreadLocalobjeto que es su clave , ese objeto no se recogerá ni se eliminará del mapa mientras el hilo viva.

En este ejemplo, la cadena de referencias fuertes se ve así:

Threadobjeto → threadLocalsmapa → instancia de clase de ejemplo → clase de ejemplo → ThreadLocalcampo estático → ThreadLocalobjeto.

(El ClassLoaderrealmente no juega un papel en la creación de la fuga, solo empeora la fuga debido a esta cadena de referencia adicional: clase de ejemplo → ClassLoader→ todas las clases que ha cargado. Fue aún peor en muchas implementaciones de JVM, especialmente antes de Java 7, porque las clases y ClassLoaders se asignaron directamente a permgen y nunca se recolectaron basura.

Una variación en este patrón es la razón por la cual los contenedores de aplicaciones (como Tomcat) pueden perder memoria como un tamiz si con frecuencia ThreadLocalvuelve a implementar aplicaciones que utilizan s que de alguna manera apuntan a sí mismas. Esto puede suceder por varias razones sutiles y, a menudo, es difícil de depurar y / o corregir.

Actualización : Dado que muchas personas lo siguen pidiendo, aquí hay un código de ejemplo que muestra este comportamiento en acción .


186
Las pérdidas de +1 ClassLoader son algunas de las pérdidas de memoria más dolorosas en el mundo JEE, a menudo causadas por bibliotecas de terceros que transforman datos (BeanUtils, códecs XML / JSON). Esto puede suceder cuando la biblioteca se carga fuera del cargador de clases raíz de su aplicación pero contiene referencias a sus clases (por ejemplo, mediante almacenamiento en caché). Cuando cancela la implementación / la implementación de la aplicación, la JVM no puede recolectar basura del cargador de clases de la aplicación (y, por lo tanto, de todas las clases cargadas por ella), por lo que con las implementaciones repetidas, el servidor de la aplicación finalmente falla. Si tienes suerte, obtienes una pista con ClassCastException zxyAbc no se puede enviar a zxyAbc
earcam

77
tomcat usa trucos y nulas TODAS las variables estáticas en TODAS las clases cargadas, aunque tomcat tiene muchas razas de datos y codificación incorrecta (necesita tiempo y enviar arreglos), además de todo el alucinante ConcurrentLinkedQueue como caché para objetos internos (pequeños), tan pequeño que incluso el ConcurrentLinkedQueue.Node ocupa más memoria.
bestsss

57
+1: las filtraciones de Classloader son una pesadilla. Pasé semanas tratando de resolverlos. Lo triste es que, como ha dicho @earcam, en su mayoría son causados ​​por bibliotecas de terceros y la mayoría de los perfiladores no pueden detectar estas filtraciones. Hay una buena y clara explicación en este blog sobre las filtraciones de Classloader. blogs.oracle.com/fkieviet/entry/…
Adrian M

44
@Nicolas: ¿Estás seguro? JRockit hace objetos de clase GC de forma predeterminada, y HotSpot no, pero AFAIK JRockit todavía no puede GC una clase o cargador de clase al que hace referencia ThreadLocal.
Daniel Pryden

66
Tomcat intentará detectar estas fugas por usted y advertirles: wiki.apache.org/tomcat/MemoryLeakProtection . La versión más reciente a veces incluso arreglará la fuga por usted.
Matthijs Bierman

1212

Referencia de objeto de retención de campo estático [especialmente campo final]

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

Invocando String.intern()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();

Secuencias abiertas (no cerradas) (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 a las que no se puede acceder desde el recolector de basura de JVM , como la memoria asignada a través de métodos nativos

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

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

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

Consulte la configuración de IBM jdk .


178
No estoy de acuerdo con que el contexto y los atributos de sesión sean "filtraciones". Son solo variables de larga duración. Y el campo final estático es más o menos solo una constante. Tal vez deberían evitarse las constantes grandes, pero no creo que sea justo llamarlo una pérdida de memoria.
Ian McLaird

80
Las secuencias abiertas (no cerradas) (archivo, red, etc.) no se filtran de verdad, durante la finalización (que será después del próximo ciclo de GC) se programará el cierre () ( close()generalmente no se invoca en el finalizador hilo ya que podría ser una operación de bloqueo). Es una mala práctica no cerrar, pero no causa una fuga. La java.sql.Connection no cerrada es la misma.
bestsss

33
En la mayoría de las JVM sensatas, parece que la clase String solo tiene una referencia débil en su interncontenido de tabla hash. Como tal, es basura recolectada adecuadamente y no una fuga. (pero IANAJP) mindprod.com/jgloss/interned.html#GC
Matt B.

42
¿Cómo el campo estático que contiene la referencia del objeto [especialmente el campo final] es una pérdida de memoria?
Kanagavelu Sugumar

55
@cHao Cierto. El peligro con el que me he encontrado no son los problemas derivados de la pérdida de memoria por parte de Streams. El problema es que no pierden suficiente memoria. Puede perder muchas asas, pero aún tiene mucha memoria. El recolector de basura podría decidir no molestarse en hacer una recolección completa porque todavía tiene mucha memoria. Esto significa que no se llama al finalizador, por lo que se queda sin identificadores. El problema es que, los finalizadores (generalmente) se ejecutarán antes de que se quede sin memoria de las secuencias que se pierden, pero es posible que no se llame antes de que se quede sin algo más que memoria.
Patrick M

460

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

Si desea que estas claves / elementos defectuosos permanezcan, 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.

68
En realidad, puede eliminar los elementos de un HashSet incluso si la clase de elemento obtiene hashCode y es igual a incorrecto; solo obtenga un iterador para el conjunto y use su método remove, ya que el iterador realmente opera en las entradas subyacentes y no en los elementos. (Tenga en cuenta que un hashCode / igual no implementado no es suficiente para provocar una fuga; los valores predeterminados implementan una identidad de objeto simple y, por lo tanto, puede obtener los elementos y eliminarlos normalmente.)
Donal Fellows

12
@Donal lo que estoy tratando de decir, supongo, es que no estoy de acuerdo con tu definición de pérdida de memoria. Consideraría (para continuar con la analogía) que su técnica de eliminación de iteradores es una bandeja de goteo debajo de una fuga; la fuga aún existe independientemente de la bandeja de goteo.
corsiKa

94
Estoy de acuerdo, esta no es una "fuga" de memoria, porque puedes eliminar las referencias al hashset y esperar a que el GC se active, ¡y listo! El recuerdo vuelve.
user541686

12
@ SyntaxT3rr0r, interpreté su pregunta como preguntando si hay algo en el lenguaje que naturalmente lleve a pérdidas de memoria. La respuesta es no. Esta pregunta pregunta si es posible idear una situación para crear algo así como una pérdida de memoria. Ninguno de estos ejemplos son pérdidas de memoria de la manera en que un programador de C / C ++ lo entendería.
Peter Lawrey

11
@Peter Lawrey: también, ¿qué piensas sobre esto: "No hay nada en el lenguaje C que se filtre naturalmente a la pérdida de memoria si no te olvidas de liberar manualmente la memoria que asignaste" . ¿Cómo sería eso para la deshonestidad intelectual? De todos modos, estoy cansado: puedes tener la última palabra.
SyntaxT3rr0r

271

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

  • File.deleteOnExit() - siempre pierde la cuerda, si la cadena es una subcadena, la fuga es aún peor (el carácter subyacente [] también se filtra)- en Java 7 la subcadena también copia el char[], por lo que el último no se aplica ; @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, ni siquiera deseo tocar el swing.

  • Runtime.addShutdownHooky no eliminar ... e incluso con removeShutdownHook debido a un error en la clase ThreadGroup con respecto a los subprocesos no iniciados que no se pueden recopilar, filtrar efectivamente ThreadGroup. JGroup tiene la fuga en GossipRouter.

  • Crear, pero no comenzar, a Threadentra en la misma categoría que la anterior.

  • La creación de un subproceso hereda ContextClassLoadery AccessControlContext, además de ThreadGroupy 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 ThreadFactoryinterfaz súper simple , aunque la mayoría de los desarrolladores no tienen idea del peligro que acecha. Además, muchas bibliotecas inician subprocesos a pedido (demasiadas bibliotecas populares de la industria).

  • ThreadLocalcachés esos son malvados en muchos casos. Estoy seguro de que todos han visto bastantes cachés simples basados ​​en ThreadLocal, bueno, las malas noticias: si el hilo continúa más de lo esperado durante la vida útil del contexto ClassLoader, es una pequeña filtración pura y agradable. No use cachés ThreadLocal a menos que sea realmente necesario.

  • Llamar ThreadGroup.destroy()cuando ThreadGroup no tiene subprocesos en sí, pero aún mantiene los ThreadGroups secundarios. Una fuga incorrecta que evitará que ThreadGroup se elimine de su elemento primario, pero todos los elementos secundarios no se pueden enumerar.

  • El uso de WeakHashMap y el valor (in) hace referencia directa a la clave. Es difícil de encontrar sin un volcado de almacenamiento dinámico. Eso se aplica a todos los extendidos Weak/SoftReferenceque podrían mantener una referencia dura al objeto protegido.

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

  • Usando InflaterInputStreampasar new java.util.zip.Inflater()el constructor ( PNGImageDecoderpor ejemplo) y no llamar end()al inflador. Bueno, si pasa el constructor con solo new, no hay posibilidad ... Y sí, llamar close()al flujo no cierra el inflador si se pasa manualmente como parámetro del constructor. Esta no es una verdadera fuga, ya que sería lanzada por el finalizador ... cuando lo considere necesario. Hasta ese momento, come tanta memoria nativa que puede hacer 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 lo empeoró hasta 7.0.2. Moraleja de la historia: liberar recursos nativos tan pronto como sea posible; El finalizador es demasiado pobre.

  • El mismo caso con java.util.zip.Deflater. Esto es mucho peor ya que Deflater necesita 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. Afortunadamente, Deflaterno se usa mucho y, que yo sepa, JDK no contiene usos indebidos. Siempre llame end()si crea manualmente una Deflatero Inflater. La mejor parte de los dos últimos: no puede encontrarlos a través de las herramientas de creación 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!


23
Creating but not starting a Thread...¡Vaya, me mordió mucho este hace algunos siglos! (Java 1.3)
leonbloy

@leonbloy, antes de que fuera aún peor ya que el hilo se agregó directamente al grupo de hilos, no comenzar significaba una fuga muy dura. No sólo aumenta el unstartedconteo, pero que impide que el grupo de hilos de la destrucción (mal menor, pero aún una fuga)
bestsss

¡Gracias! "Llamar ThreadGroup.destroy()cuando ThreadGroup no tiene hilos en sí mismo ..." es un error increíblemente sutil; Estuve persiguiendo esto durante horas, perdí el rumbo porque enumerar el hilo en mi GUI de control no mostró nada, pero el grupo de hilos y, presumiblemente, al menos un grupo secundario no desaparecería.
Lawrence Dol

1
@bestsss: Tengo curiosidad, ¿por qué querrías eliminar un gancho de apagado, dado que se ejecuta en, bueno, apagado JVM?
Lawrence Dol

203

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 fue mordido por un caso de esquina de JVM / JAVA (carga de clase con static ...). 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 hace referencia. Nosotros, como desarrolladores de Java, no nos importa la memoria. Lo asignamos cuando sea necesario y dejamos que se libere automáticamente. Multa.

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

Por supuesto, todo tipo de oyentes (como los oyentes de IU), los cachés o cualquier estado compartido de larga duración tienden a producir pérdidas de memoria si no se manejan adecuadamente. Lo que debe entenderse 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 es necesario. Almacenamos objetos en caché, pero no tenemos una estrategia para eliminarlos del caché.

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

Como si tuviéramos que cerrar conexiones o archivos SQL. Necesitamos establecer referencias adecuadas a 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 que se notifique a un escucha deben proporcionar un método addListener y removeListener. Y cuando estos notificadores ya no se usan, deben borrar su lista de oyentes.

Una pérdida de memoria es realmente posible y es perfectamente predecible. No es necesario contar con funciones de idioma especiales o casos de esquina. Las pérdidas de memoria son un indicador de que algo falta o incluso de problemas de diseño.


24
Me parece curioso que en otras respuestas la gente esté buscando esos casos y trucos de borde y parece estar perdiendo completamente el punto. Podrían simplemente mostrar código que mantenga referencias inútiles a objetos que nunca volverán a usar, y nunca eliminar esas referencias; se puede decir que esos casos no son pérdidas de memoria "verdaderas" porque todavía hay referencias a esos objetos, pero si el programa nunca usa esas referencias nuevamente y tampoco las descarta, es completamente equivalente a (y tan malo como) un " verdadera pérdida de memoria ".
ehabkost

@Nicolas Bousquet: "La pérdida de memoria es realmente posible" Muchas gracias. +15 votos a favor. Agradable. Me gritaron aquí por afirmar ese hecho, como premisa de una pregunta sobre el lenguaje Go: stackoverflow.com/questions/4400311 Esta pregunta todavía tiene
votos

El GC en Java y .NET se basa en cierto sentido en el supuesto de que los objetos que contienen referencias a otros objetos son los mismos que los objetos que "se preocupan" por otros objetos. En realidad, es posible que existan bordes en el gráfico de referencia que no representan relaciones de "cuidado", y es posible que un objeto se preocupe por la existencia de otro objeto incluso si no existe una ruta de referencia directa o indirecta (incluso usando WeakReference) de uno a otro. Si una referencia de objeto tuviera un bit de repuesto, podría ser útil tener un indicador "se preocupa por el objetivo" ...
supercat

... y que el sistema proporcione notificaciones (por medios similares a PhantomReference) si se descubrió que un objeto no tiene a nadie que le importe. WeakReferencese acerca un poco, pero debe convertirse en una referencia fuerte antes de que pueda usarse; Si se produce un ciclo GC mientras existe la referencia fuerte, se supondrá que el objetivo es útil.
supercat

En mi opinión, esta es la respuesta correcta. Escribimos una simulación hace años. De alguna manera, vinculamos accidentalmente el estado anterior al estado actual creando una pérdida de memoria. Debido a una fecha límite, nunca resolvimos la pérdida de memoria, sino que la convertimos en una "característica" al documentarla.
nalply

163

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

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

¿Pero hay múltiples meta-preguntas que pueden haberse hecho?

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

Estoy leyendo su metapregunta como "¿Qué respuesta podría haber utilizado en esta situación de entrevista?". Y por lo tanto, me voy a 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 se filtre. Entonces, con suerte, esto ayudará.

Una de las habilidades más importantes que puede desarrollar para la entrevista es aprender a escuchar activamente las preguntas y trabajar con el entrevistador para extraer su intención. Esto no solo le permite responder a sus preguntas de la manera que deseen, sino que también muestra que tiene algunas habilidades vitales de comunicación. Y cuando se trata de elegir entre muchos desarrolladores igualmente talentosos, contrataré al que escucha, piensa y comprende antes de responder cada vez.


22
Cada vez que he hecho esa pregunta, estoy buscando una respuesta bastante simple: seguir aumentando la cola, finalmente no cerrar db, etc., sin detalles extraños del cargador de clases / hilo, implica que entienden lo que el gc puede y no puede hacer por usted. Depende del trabajo para el que estés entrevistando, supongo.
DaveC

Por favor, eche un vistazo a mi pregunta, gracias stackoverflow.com/questions/31108772/…
Daniel Newtown

130

El siguiente es un ejemplo bastante inútil, si no comprende JDBC . O al menos cómo JDBC espera que se cierre un desarrollador Connection, Statementy las ResultSetinstancias antes de descartarlas o perder referencias a ellas, 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 el Connectionobjeto no está cerrado y, por lo tanto, la conexión física permanecerá abierta, hasta que el recolector de basura se dé vuelta y vea que no se puede alcanzar. GC invocará el finalizemétodo, pero hay controladores JDBC que no implementan finalize, al menos no de la misma manera que Connection.closese implementa. El comportamiento resultante es que, si bien se recuperará la memoria debido a la recopilación de objetos inalcanzables, los recursos (incluida la memoria) asociados con el Connectionobjeto podrían simplemente no recuperarse.

En un caso en el que Connectionel finalizemétodo del 's no limpia todo, uno podría descubrir 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 hace), y debe estar cerrado.

Incluso si el controlador JDBC se implementara finalize, es posible que se generen excepciones durante la finalización. El comportamiento resultante es que cualquier memoria asociada con el objeto ahora "inactivo" no será reclamada, ya que finalizese garantiza que se invocará solo una vez.

El escenario anterior de encontrar excepciones durante la finalización del objeto está relacionado con otro escenario que podría conducir a una pérdida de memoria: la resurrección del objeto. La resurrección de objetos a menudo se hace intencionalmente creando una fuerte referencia al objeto de ser finalizado, desde otro objeto. Cuando el uso de la resurrección de objetos se usa incorrectamente, provocará una pérdida de memoria en combinación con otras fuentes de pérdidas de memoria.

Hay muchos más ejemplos que puedes evocar, como

  • Administrar una Listinstancia donde solo está agregando a la lista y no eliminando de ella (aunque debería deshacerse de los elementos que ya no necesita), o
  • Abrir Sockets o Files, pero no cerrarlos cuando ya no son necesarios (similar al ejemplo anterior que involucra a la Connectionclase).
  • No descargar Singletons cuando se desactiva una aplicación Java EE. Aparentemente, el cargador de clases que cargó la clase singleton retendrá una referencia a la clase y, por lo tanto, la instancia singleton nunca se recopilará. Cuando se despliega una nueva instancia de la aplicación, generalmente se crea un nuevo cargador de clases, y el cargador de clases anterior seguirá existiendo debido al singleton.

98
Alcanzará el límite máximo de conexión abierta antes de alcanzar los límites de memoria por lo general. No me preguntes por qué sé ...
Hardwareguy

El controlador Oracle JDBC es conocido por hacer esto.
chotchki

@Hardwareguy Llegué mucho a los límites de conexión de las bases de datos SQL hasta que Connection.closefinalmente puse el bloque de todas mis llamadas SQL. Por diversión adicional, llamé a algunos procedimientos almacenados de Oracle de larga ejecución que requerían bloqueos en el lado de Java para evitar demasiadas llamadas a la base de datos.
Michael Shopsin

@Hardwareguy Eso es interesante, pero no es realmente necesario que se alcancen los límites de conexión reales para todos los entornos. Por ejemplo, para una aplicación implementada en el servidor de aplicaciones weblogic 11g, he visto fugas de conexión a gran escala. Pero debido a la opción de obtener conexiones filtradas, las conexiones de la base de datos permanecieron disponibles mientras se introducían las pérdidas de memoria. No estoy seguro de todos los entornos.
Aseem Bansal

En mi experiencia, tiene una pérdida de memoria incluso si cierra la conexión. Primero debe cerrar ResultSet y PreparedStatement. Tuve un servidor que se bloqueó repetidamente después de horas o incluso días de trabajar bien, debido a OutOfMemoryErrors, hasta que comencé a hacerlo.
Bjørn Stenfeldt

119

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


55
¿Y dónde está la pérdida de memoria aquí?
rds

28
@maniek: no quise decir que este código exhibe una pérdida de memoria. Lo cité para mostrar que a veces se requiere un código no obvio para evitar la retención accidental de objetos.
meriton

¿Qué es RangeCheck (index); ?
Koray Tugay

66
Joshua Bloch dio este ejemplo en Effective Java mostrando una implementación simple de Stacks. Una muy buena respuesta.
alquila el

Pero eso no sería una pérdida de memoria REAL, incluso si se olvida. El elemento aún se accedería SEGURAMENTE con Reflection, simplemente no sería obvio y directamente accesible a través de la interfaz de Lista, pero el objeto y la referencia aún están allí, y se puede acceder de forma segura.
DGoiko

68

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


14
No creo que esto sea una "fuga". Es un error, y es por diseño del programa y lenguaje. Una fuga sería un objeto dando vueltas sin ninguna referencia a ella.
user541686

29
@Mehrdad: Esa es solo una definición limitada que no se aplica completamente a todos los idiomas. Yo diría que cualquier pérdida de memoria es un error causado por un mal diseño del programa.
Bill the Lizard

99
@Mehrdad: ...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language. No veo cómo estás sacando esa conclusión. Hay menos formas de crear una pérdida de memoria en Java por cualquier definición. Definitivamente sigue siendo una pregunta válida.
Bill the Lizard

77
@ 31eee384: Si su programa mantiene objetos en la memoria que nunca podrá usar, técnicamente es una pérdida de memoria. El hecho de que tengas problemas más grandes realmente no cambia eso.
Bill the Lizard

8
@ 31eee384: Si sabes con certeza que no será, entonces no puede ser. El programa, tal como está escrito, nunca accederá a los datos.
Bill the Lizard

51

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

El código no se compila en Eclipse IDE: 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();
        }
    }

}

1
La memoria
asignada

44
La memoria asignada tampoco pertenece a Java.
bestsss

¿Es este jvm de sol / oráculo específico? Por ejemplo, ¿esto funcionará en IBM?
Berlin Brown

2
La memoria ciertamente "pertenece a Java", al menos en el sentido de que i) no está disponible para nadie más, ii) cuando la aplicación Java salga, se devolverá al sistema. Está justo afuera de la JVM.
Michael Anderson

3
Esto se construirá en eclipse (al menos en versiones recientes) pero necesitará cambiar la configuración del compilador: en Ventana> Preferencias> Java> Compilador> Errores / Advertencia> API obsoleta y restringida establece referencia prohibida (reglas de acceso) a "Advertencia ".
Michael Anderson

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 pérdida, en este contexto), ocurre cuando un programa de computadora consume memoria pero no puede liberarla de nuevo al sistema operativo". (Wikipedia)

La respuesta fácil es: no puedes. Java gestiona automáticamente la memoria y liberará recursos que no son necesarios para usted. No puedes evitar que esto suceda. SIEMPRE podrá liberar los recursos. En programas con administración manual de memoria, esto es diferente. No puede obtener algo de memoria en C usando malloc (). Para liberar la memoria, necesita el puntero que devolvió malloc y llamar a free () en él. Pero si ya no tiene el puntero (sobrescrito o se ha excedido la vida útil), desafortunadamente es incapaz de 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 podría desreferenciar los objetos que creó y así liberar la memoria -> SIN FUGAS. Sin embargo, la respuesta de acconrad se acerca bastante, ya que tengo que admitirlo, ya que su solución es efectivamente "estrellar" el recolector de basura forzándolo en un bucle sin fin).

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, por lo tanto, pérdidas de memoria. Si llama a esta biblioteca, su proceso de 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 esté solicitando un código java que se vea afectado por dicho error. Lo siento, no conozco uno y es posible que ya no sea un error en la próxima versión de Java.


12
Esa es una definición extremadamente limitada (y no muy útil) de pérdidas de memoria. La única definición que tiene sentido para fines prácticos es "una pérdida de memoria es cualquier condición en la que el programa continúa reteniendo memoria asignada después de que los datos que contiene ya no son necesarios".
Mason Wheeler

1
¿La respuesta del acconrad mencionado fue eliminada?
Tomáš Zato - Restablece a Mónica el

1
@ TomášZato: No, no lo es. Gire la referencia anterior para vincular ahora, para que pueda encontrarla fácilmente.
Yankee

¿Qué es la resurrección de objetos? ¿Cuántas veces se llama a un destructor? ¿Cómo refutan estas preguntas esta respuesta?
autista

1
Bueno, seguro que puede crear una pérdida de memoria dentro de Java, a pesar de GC, y aún así cumplir con la definición anterior. Solo tenga una estructura de datos de solo agregar, protegida contra el acceso externo para que ningún otro código la elimine; el programa no puede liberar la memoria porque no tiene el código.
toolforger el

39

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, el original permanece en la memoria. Por lo tanto, siempre que tenga un StringLeaker en juego, también tiene toda la cadena original en la memoria, aunque pueda pensar que solo se está aferrando a 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();
...

Hacerlo mantendrá tanto la cadena larga original como la subcadena derivada en la memoria, incluso después de que la instancia de StringLeaker se haya descartado.


44
No llamaría a eso una pérdida de memoria, per se . Cuando muchSmallerStringse libera (porque el StringLeakerobjeto se destruye), la cadena larga también se liberará. Lo que yo llamo pérdida de memoria es memoria que nunca se puede liberar en esta instancia de JVM. Sin embargo, usted ha demostrado a sí mismo cómo liberar la memoria: this.muchSmallerString=new String(this.muchSmallerString). Con una pérdida de memoria real, no hay nada que pueda hacer.
rds

2
@rds, ese es un punto justo. El no interncaso puede ser más una "sorpresa de memoria" que una "pérdida de memoria". .intern()Sin embargo, la subcadena crea una situación en la que la referencia a la cadena más larga se conserva y no se puede liberar.
Jon Chambers

15
El método substring () crea una nueva cadena en java7 (es un comportamiento nuevo)
anstarovoyt

Ni siquiera necesita hacer la subcadena () usted mismo: use un emparejador de expresiones regulares para que coincida con una pequeña parte de una gran entrada, y lleve la cadena "extraída" durante mucho tiempo. La gran entrada se mantiene viva hasta Java 6.
Bananeweizen

37

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 con ámbito estático / de aplicación y luego no se elimina el oyente 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 eventos de incendios, también se llama a todos tus antiguos oyentes.


1
La plataforma de Android ofrece el ejemplo de una pérdida de memoria creada al almacenar en caché un mapa de bits en el campo estático de una vista .
rds

36

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

A menos que alguien haya probado esto, es muy probable que obtenga un OutOfMemoryError después de un par de implementaciones, porque la aplicación no se encargó de limpiar después de sí 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 el contenedor pueda tener a objetos o clases de su aplicación puedan ser recolectadas de basura.

Si solo hay una referencia que sobrevive al despliegue de su aplicación web, el cargador de clases correspondiente y, en consecuencia, todas las clases de su aplicación web no se pueden recolectar basura.

Los subprocesos iniciados por su aplicación, las variables ThreadLocal, los apéndices de registro son algunos de los sospechosos habituales que causan fugas en el cargador de clases.


1
Esto no se debe a una pérdida de memoria, sino a que el cargador de clases no descarga el conjunto de clases anterior. Por lo tanto, no se recomienda volver a implementar un servidor de aplicaciones sin reiniciar el servidor (no la máquina física, sino el servidor de aplicaciones). He visto el mismo problema con WebSphere.
Sven

35

¿Tal vez mediante el uso de código nativo externo a través de JNI?

Con Java puro, es casi imposible.

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


22
Eso depende de la definición de "pérdida de memoria". Si "la memoria está retenida, pero ya no es necesaria", entonces es fácil de hacer en Java. Si se trata de "memoria asignada pero no accesible por el código", entonces se vuelve un poco más difícil.
Joachim Sauer

@Joachim Sauer: me refería al segundo tipo. El primero es bastante fácil de hacer :)
Rogach

66
"Con Java puro, es casi imposible". Bueno, mi experiencia es otra, especialmente cuando se trata de implementar cachés por personas que no son conscientes de los escollos aquí.
Fabian Barney

44
@Rogach: básicamente hay +400 votos a favor en varias respuestas de personas con +10 000 repeticiones que lo demuestran en ambos casos Joachim Sauer comentó que es muy posible. Entonces su "casi imposible" no tiene sentido.
SyntaxT3rr0r

32

He tenido una buena "pérdida de memoria" en relación con el análisis de PermGen y XML una vez. El analizador XML que utilizamos (no recuerdo cuál era) hizo un String.intern () en los nombres de las etiquetas, para hacer una comparación más rápida. Uno de nuestros clientes tuvo la gran idea de almacenar valores de datos no en atributos XML o texto, sino como nombres de etiquetas, por lo que tuvimos un documento como:

<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>

De hecho, no usaron números, sino identificaciones textuales más largas (alrededor de 20 caracteres), que eran únicas y llegaban a una tasa de 10-15 millones por día. Eso genera 200 MB de basura al día, lo que nunca más se necesita, y nunca GC (ya que está en PermGen). Teníamos permgen configurado en 512 MB, por lo que la excepción de memoria insuficiente (OOME) tardó alrededor de dos días en llegar ...


44
Solo para analizar tu código de ejemplo: creo que los números (o cadenas que comienzan con números) no están permitidos como nombres de elementos en XML.
Paŭlo Ebermann

Tenga en cuenta que esto ya no es cierto para JDK 7+, donde el internamiento de cadenas ocurre en el montón. Consulte este artículo para obtener una reseña detallada: java-performance.info/string-intern-in-java-6-7-8
jmiserez

Entonces, ¿creo que usar StringBuffer en lugar de String resolvería este problema? ¿no?
Anubhs

24

¿Qué es una pérdida de memoria?

  • Es causado por un error o mal diseño.
  • Es una pérdida de memoria.
  • 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 queda absorbida en la memoria. Un mejor diseño usa un LRUMap (solo mantiene los objetos usados ​​recientemente en caché).

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

  • utilizando construcciones ThreadLocal .
  • agregando á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 nuevamente 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).


22

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

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, todavía 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é un rumor de que si tienes 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 ya no se puede acceder al código. Pero eso está completamente sin verificar.


2
Las clases internas rara vez son un problema. Son un caso sencillo y muy fácil de detectar. El rumor es solo un rumor también.
bestsss

2
El "rumor" suena como si alguien leyera a medias sobre cómo funciona el GC generacional. Los objetos longevos pero ahora inalcanzables pueden quedarse y ocupar espacio por un tiempo, porque la JVM los promovió entre las generaciones más jóvenes para que pudiera dejar de revisarlos en cada pase. Evadirán los pases de "limpiar mis 5000 cadenas temporales", por diseño. Pero no son inmortales. Todavía son elegibles para la recopilación, y si la máquina virtual no tiene RAM, eventualmente ejecutará un barrido completo del GC y recuperará esa memoria.
cHao

22

Recientemente me encontré con una situación de pérdida de memoria causada de alguna manera por log4j.

Log4j tiene este mecanismo llamado Contexto de diagnóstico anidado (NDC), que es un instrumento para distinguir la salida de registro intercalada de diferentes fuentes. La granularidad con la que trabaja NDC son los subprocesos, por lo que distingue las salidas de registro de diferentes subprocesos por separado.

Para almacenar etiquetas específicas de subprocesos, la clase NDC de log4j utiliza una tabla hash que está codificada por el objeto Thread (en lugar de decir la identificación del subproceso) y, por lo tanto, hasta que la etiqueta NDC permanezca en la memoria todos los objetos que cuelgan del subproceso El objeto también permanece en la memoria. En nuestra aplicación web, utilizamos NDC para etiquetar las salidas de registro con una identificación de solicitud para distinguir los registros de una sola solicitud por separado. El contenedor que asocia la etiqueta NDC con un hilo, también la elimina mientras devuelve la respuesta de una solicitud. El problema ocurrió cuando durante el proceso de procesar una solicitud, se generó un subproceso secundario, algo así como el siguiente código:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

Entonces, un contexto NDC se asoció con un hilo en línea que se generó. El objeto de hilo que fue la clave para este contexto NDC, es el hilo en línea que tiene el objeto hugeList colgando de él. Por lo tanto, incluso después de que el hilo terminó de hacer lo que estaba haciendo, la referencia a la lista enorme se mantuvo viva por el contexto de NDC Hastable, lo que provocó una pérdida de memoria.


Eso apesta. Debe verificar esta biblioteca de registro que asigna memoria CERO al iniciar sesión en un archivo: mentalog.soliveirajr.com
TraderJoeChicago

+1 ¿Sabe de antemano si hay un problema similar con el MDC en slf4j / logback (productos sucesores del mismo autor)? Estoy a punto de hacer una inmersión profunda en la fuente, pero quería comprobar primero. De cualquier manera, gracias por publicar esto.
sparc_spread

20

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 recuento de referencias, que ya no es el caso). Pero es una pregunta bastante vaga, por lo que es una excelente oportunidad para mostrar su comprensión de la administració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 recuento de referencias, la mayoría usa un recolector de basura de barrido, que de hecho recogerá 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. */
    }
}

Entonces puede explicar que esto es técnicamente una pérdida de memoria, pero realmente 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, debe escribir un código Java que asigne un recurso nativo fuera del alcance normal de la conciencia de la JVM.


19

Todos siempre olvidan la ruta del código nativo. Aquí hay una fórmula simple para una fuga:

  1. Declarar método nativo.
  2. En el método nativo, llame malloc. No llamesfree .
  3. Llamar al método nativo.

Recuerde, las asignaciones de memoria en código nativo provienen del montón JVM.


1
Basado en una historia real.
Reg

18

Cree un mapa estático y continúe agregando referencias duras a él. 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); }
}

87
¿Cómo es eso una fuga? Está haciendo exactamente lo que le pides que haga. Si eso es una fuga, crear y almacenar objetos en cualquier lugar es una fuga.
Falmarri

3
Estoy de acuerdo con @Falmarri. No veo una fuga allí, solo estás creando objetos. Ciertamente podría 'reclamar' la memoria que acaba de asignar con otro método llamado 'removeFromCache'. Una fuga es cuando no puedes recuperar la memoria.
Kyle

3
Mi punto es que alguien que sigue creando objetos, quizás colocándolos en un caché, podría terminar con un error OOM si no tiene cuidado.
duffymo

8
@duffymo: Pero eso no es realmente lo que estaba haciendo la pregunta. No tiene nada que ver con simplemente usar toda su memoria.
Falmarri

3
Absolutamente inválido. Simplemente está recopilando un montón de objetos en una colección de mapas. Sus referencias se mantendrán porque el Mapa las tiene.
gyorgyabraham

16

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 varias instancias. Aquí hay un programa simple que filtra todo el montón en algún momento entre unos segundos y unos minutos, dependiendo del tamaño del 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());
        }
    }
}

15

No creo que nadie haya dicho esto todavía: puede resucitar un objeto anulando el método finalize () de modo que finalize () almacene una referencia de esto en alguna parte. El recolector de basura solo se llamará una vez en el objeto, por lo que el objeto nunca se destruirá.


10
Esto no es cierto. finalize()no se llamará, pero el objeto se recopilará una vez que no haya más referencias. El recolector de basura tampoco se 'llama'.
bestsss

1
Esta respuesta es engañosa, la finalize()JVM solo puede invocar el método una vez, pero esto no significa que no se pueda volver a recolectar basura si el objeto resucita y luego se desreferencia de nuevo. Si hay un código de cierre de recursos en el finalize()método, este código no se ejecutará nuevamente, esto puede causar una pérdida de memoria.
Tom Cammann

15

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

Uhm, se podría decir, qué idiota.

Bueno, lo que hace que esto sea interesante es: de esta manera, puede perder memoria de montón del proceso subyacente, en lugar de hacerlo del montón 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 jar 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();
    }
}

Simplemente pegue en un archivo llamado BigJarCreator.java, compílelo y ejecútelo desde la línea de comandos:

javac BigJarCreator.java
java -cp . BigJarCreator

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

Creemos 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 serán basura recolectada inmediatamente 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 almacenamiento dinámico decente (2 MB):

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

No encontrará un error OOM aquí, ya que no se conservan referencias, la aplicación seguirá ejecutándose sin importar qué tan grande elija ITERATIONS en el ejemplo anterior. El consumo de memoria de su proceso (visible en la parte superior (RES / RSS) o 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 de memoria.

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

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

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

Bastante simple y sorprendente.


14

Como muchas personas han sugerido, las fugas de recursos son bastante fáciles de causar, como los ejemplos de JDBC. Las fugas de memoria reales son un poco más difíciles, especialmente si no confía en pedazos rotos de la JVM para hacerlo por usted ...

Las ideas de crear objetos que tienen una huella muy grande y luego no poder acceder a ellos tampoco son fugas de memoria reales. Si nada puede acceder, entonces será basura recolectada, y si algo puede acceder, entonces no es una fuga ...

Sin embargo, una forma que solía funcionar, y no sé si todavía lo hace, es tener una cadena circular de tres profundidades. Como en el Objeto A tiene una referencia al Objeto B, el Objeto B tiene una referencia al Objeto C y el Objeto C tiene una referencia al Objeto A. El GC fue lo suficientemente inteligente como para saber que una cadena de dos profundas, como en A <--> B - se puede recopilar de forma segura si A y B no son accesibles por otra cosa, pero no pueden manejar la cadena de tres vías ...


77
No ha sido el caso desde hace algún tiempo. Los GC modernos saben cómo manejar referencias circulares.
assylias

13

Otra forma de crear pérdidas de memoria potencialmente enormes es mantener referencias Map.Entry<K,V>de a TreeMap.

Es difícil entender por qué esto se aplica solo a TreeMaps, pero al observar la implementación, la razón podría ser que: a TreeMap.Entryalmacena referencias a sus hermanos, por lo tanto, si a TreeMapestá listo para ser recopilado, pero alguna otra clase tiene una referencia a cualquiera de es Map.Entry, entonces todo el mapa será retenido en la memoria.


Escenario de la vida real:

Imagine tener una consulta db que devuelve una TreeMapestructura de datos grandes . Las personas generalmente usan TreeMaps como se retiene el orden de inserción del elemento.

public static Map<String, Integer> pseudoQueryDatabase();

Si la consulta se llamara muchas veces y, para cada consulta (por lo tanto, para cada una Mapdevuelta) guarde un Entrylugar, la memoria seguirá creciendo constantemente.

Considere la siguiente clase de contenedor:

class EntryHolder {
    Map.Entry<String, Integer> entry;

    EntryHolder(Map.Entry<String, Integer> entry) {
        this.entry = entry;
    }
}

Solicitud:

public class LeakTest {

    private final List<EntryHolder> holdersCache = new ArrayList<>();
    private static final int MAP_SIZE = 100_000;

    public void run() {
        // create 500 entries each holding a reference to an Entry of a TreeMap
        IntStream.range(0, 500).forEach(value -> {
            // create map
            final Map<String, Integer> map = pseudoQueryDatabase();

            final int index = new Random().nextInt(MAP_SIZE);

            // get random entry from map
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                if (entry.getValue().equals(index)) {
                    holdersCache.add(new EntryHolder(entry));
                    break;
                }
            }
            // to observe behavior in visualvm
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

    }

    public static Map<String, Integer> pseudoQueryDatabase() {
        final Map<String, Integer> map = new TreeMap<>();
        IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
        return map;
    }

    public static void main(String[] args) throws Exception {
        new LeakTest().run();
    }
}

Después de cada pseudoQueryDatabase()llamada, las mapinstancias deben estar listas para la recopilación, pero no sucederá, ya que al menos una Entryestá almacenada en otro lugar.

Dependiendo de su jvmconfiguración, la aplicación puede bloquearse en la etapa inicial debido a a OutOfMemoryError.

Puede ver en este visualvmgráfico cómo la memoria sigue creciendo.

Volcado de memoria - TreeMap

No ocurre lo mismo con una estructura de datos hash ( HashMap).

Este es el gráfico cuando se usa a HashMap.

Volcado de memoria - HashMap

¿La solución? Simplemente guarde directamente la clave / valor (como probablemente ya lo haga) en lugar de guardar el Map.Entry.


He escrito un punto de referencia más extenso aquí .


11

Los hilos no se recopilan hasta que terminan. Sirven como raíces de la recolección de basura. Son uno de los pocos objetos que no serán reclamados simplemente olvidándolos o borrando referencias a ellos.

Considere: el patrón básico para terminar un subproceso de trabajo es establecer alguna variable de condición vista por el subproceso. El hilo puede verificar la variable periódicamente y usar eso como una señal para terminar. Si la variable no se declara volatile, es posible que el hilo no vea el cambio en la variable, por lo que no sabrá terminar. O imagine si algunos subprocesos desean actualizar un objeto compartido, pero se interrumpe 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, entonces los subprocesos obsoletos / atascados podrían no notarse y se acumularán indefinidamente, causando una pérdida de memoria. Es probable que los subprocesos utilicen otros datos en su aplicación, por lo que también evitarán que se recopile cualquier cosa a la que hagan 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()todo lo que quieras, pero el objeto pasado leakMenunca morirá.

(* editado *)


1
@Spidey Nada está "atascado". El método de llamada regresa rápidamente, y el objeto pasado nunca será reclamado. Eso es precisamente una fuga.
Boann

1
Tendrá un hilo "en ejecución" (o durmiendo, lo que sea) durante toda la vida de su programa. Eso no cuenta como una fuga para mí. Además, un grupo no cuenta como una fuga, incluso si no lo usa por completo.
Spidey

1
@Spidey "Tendrás una [cosa] durante toda la vida de tu programa. Eso no cuenta como una fuga para mí". Te escuchas
Boann

3
@Spidey Si contara la memoria que el proceso sabe que no se filtró, entonces todas las respuestas aquí son incorrectas, ya que el proceso siempre rastrea qué páginas en su espacio de direcciones virtuales están mapeadas. Cuando finaliza el proceso, el sistema operativo limpia todas las fugas al volver a colocar las páginas en la pila de páginas libre. Para llevar eso al siguiente extremo, uno podría matar a golpes cualquier fuga argumentada al señalar que ninguno de los bits físicos en los chips de RAM o en el espacio de intercambio en el disco se han perdido o destruido físicamente, por lo que puede apagar la computadora y de nuevo para limpiar cualquier fuga.
Boann

1
La definición práctica de una fuga es que su memoria se ha perdido de tal manera que no la conocemos y, por lo tanto, no podemos realizar el procedimiento necesario para reclamarla; Tendríamos que derribar y reconstruir todo el espacio de memoria. Un subproceso deshonesto como este podría surgir naturalmente a través de un punto muerto o una implementación poco fiable de subprocesos. Los objetos a los que hacen referencia tales subprocesos, incluso indirectamente, ahora se evitan que se recopilen, por lo que tenemos memoria que no se recuperará o reutilizará naturalmente durante la vida útil del programa. Yo llamaría a eso un problema; específicamente es una pérdida de memoria.
Boann

10

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

Por ejemplo, usar las variables ThreadLocal en Servlets para comunicarse con otros componentes web, hacer que los hilos sean creados por el contenedor y mantener los inactivos en un grupo. Las variables ThreadLocal, si no se limpian correctamente, vivirán allí hasta que, posiblemente, el mismo componente web sobrescriba sus valores.

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


10

El entrevistador podría estar 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 los colectores de basura de conteo de referencias. A continuación, explicaría cortésmente que las JVM utilizan un algoritmo mucho más sofisticado que no tiene esta limitación.

-Wes Tarle


12
Este es un problema clásico con los colectores de basura de conteo de referencias. Incluso hace 15 años, Java no usaba el conteo de referencias. Árbitro. El conteo también es más lento que el GC.
bestsss

44
No es una pérdida de memoria. Solo un bucle infinito.
Esben Skov Pedersen

2
@Esben En cada iteración, la anterior firstno es útil y debe recolectarse basura. En los recolectores de basura que cuentan referencias , el objeto no se liberaría porque hay una referencia activa en él (por sí mismo). El bucle infinito está aquí para demostrar la fuga: cuando ejecuta el programa, la memoria se elevará indefinidamente.
rds

@rds @ Wesley Tarle supongamos que el bucle no es infinito. ¿Seguiría habiendo una pérdida de memoria?
nz_21
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.