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?
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?
Respuestas:
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:
ClassLoader
.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.ClassLoader
que se cargó.Debido a la forma en que ThreadLocal
se implementa en el JDK de Oracle, esto crea una pérdida de memoria:
Thread
tiene un campo privado threadLocals
, que en realidad almacena los valores locales del hilo.ThreadLocal
objeto, por lo que después de que ese ThreadLocal
objeto es recolectado como basura, su entrada se elimina del mapa.ThreadLocal
objeto 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í:
Thread
objeto → threadLocals
mapa → instancia de clase de ejemplo → clase de ejemplo → ThreadLocal
campo estático → ThreadLocal
objeto.
(El ClassLoader
realmente 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 ClassLoader
s 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 ThreadLocal
vuelve 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 .
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 noclassgc
opción en IBM JDK que evita la recolección de basura de clase no utilizada
Consulte la configuración de IBM jdk .
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.
intern
contenido de tabla hash. Como tal, es basura recolectada adecuadamente y no una fuga. (pero IANAJP) mindprod.com/jgloss/interned.html#GC
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.
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, 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.addShutdownHook
y 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 Thread
entra en la misma categoría que la anterior.
La creación de un subproceso hereda ContextClassLoader
y AccessControlContext
, además de 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 ThreadFactory
interfaz 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).
ThreadLocal
caché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/SoftReference
que podrían mantener una referencia dura al objeto protegido.
Utilizando java.net.URL
con el protocolo HTTP (S) y cargando el recurso desde (!). Este es especial, KeepAliveCache
crea 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 InflaterInputStream
pasar new java.util.zip.Inflater()
el constructor ( PNGImageDecoder
por 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, Deflater
no se usa mucho y, que yo sepa, JDK no contiene usos indebidos. Siempre llame end()
si crea manualmente una Deflater
o 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!
Creating but not starting a Thread...
¡Vaya, me mordió mucho este hace algunos siglos! (Java 1.3)
unstarted
conteo, pero que impide que el grupo de hilos de la destrucción (mal menor, pero aún una fuga)
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.
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.
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" ...
PhantomReference
) si se descubrió que un objeto no tiene a nadie que le importe. WeakReference
se 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.
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?
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.
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
, Statement
y las ResultSet
instancias 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 Connection
objeto 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 finalize
método, pero hay controladores JDBC que no implementan finalize
, al menos no de la misma manera que Connection.close
se 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 Connection
objeto podrían simplemente no recuperarse.
En un caso en el que Connection
el finalize
mé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 finalize
se 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
List
instancia donde solo está agregando a la lista y no eliminando de ella (aunque debería deshacerse de los elementos que ya no necesita), oSocket
s o File
s, pero no cerrarlos cuando ya no son necesarios (similar al ejemplo anterior que involucra a la Connection
clase).Connection.close
finalmente 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.
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 ...
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.
...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.
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();
}
}
}
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.
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.
muchSmallerString
se libera (porque el StringLeaker
objeto 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.
intern
caso 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.
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.
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.
¿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.
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 ...
¿Qué es una pérdida de memoria?
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:
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).
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.
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.
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.
Todos siempre olvidan la ruta del código nativo. Aquí hay una fórmula simple para una fuga:
malloc
. No llamesfree
.Recuerde, las asignaciones de memoria en código nativo provienen del montón JVM.
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); }
}
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());
}
}
}
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á.
finalize()
no se llamará, pero el objeto se recopilará una vez que no haya más referencias. El recolector de basura tampoco se 'llama'.
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.
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.
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 ...
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 TreeMap
s, pero al observar la implementación, la razón podría ser que: a TreeMap.Entry
almacena referencias a sus hermanos, por lo tanto, si a TreeMap
está 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 TreeMap
estructura de datos grandes . Las personas generalmente usan TreeMap
s 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 Map
devuelta) guarde un Entry
lugar, 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 map
instancias deben estar listas para la recopilación, pero no sucederá, ya que al menos una Entry
está almacenada en otro lugar.
Dependiendo de su jvm
configuración, la aplicación puede bloquearse en la etapa inicial debido a a OutOfMemoryError
.
Puede ver en este visualvm
gráfico cómo la memoria sigue creciendo.
No ocurre lo mismo con una estructura de datos hash ( HashMap
).
Este es el gráfico cuando se usa a 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í .
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 leakMe
nunca morirá.
(* editado *)
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.
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
first
no 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.