leak - Creando una pérdida de memoria con Java
java visualvm (30)
A continuación, habrá un caso no obvio en el que las fugas de Java, además del caso estándar de escuchas olvidadas, referencias estáticas, claves falsas / modificables en hashmaps, o solo hilos atascados sin ninguna posibilidad de terminar su ciclo de vida.
-
File.deleteOnExit()
: siempreFile.deleteOnExit()
la cadena,si la cadena es una subcadena, la fuga es aún peor (el carácter subyacente [] también se filtra)- en la subcadena Java 7 también copia elchar[]
, por lo que no se aplica más adelante ; @Daniel, sin embargo, no hay necesidad de votos.
Me concentraré en los hilos para mostrar el peligro de los hilos no administrados en su mayoría, no deseo siquiera tocar el swing.
Runtime.addShutdownHook
y no elimina ... e incluso con removeShutdownHook debido a un error en la clase ThreadGroup con respecto a subprocesos no iniciados que pueden no recopilarse, efectivamente filtrar ThreadGroup. JGroup tiene la fuga en GossipRouter.Al crear, pero no comenzar, un
Thread
entra en la misma categoría que arriba.La creación de un subproceso hereda el
ContextClassLoader
yAccessControlContext
, más elThreadGroup
y cualquierInheritedThreadLocal
, 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 interfazThreadFactory
super simple, pero la mayoría de los desarrolladores no tienen ni idea del peligro que acecha. También muchas bibliotecas inician subprocesos a pedido (también muchas bibliotecas populares de la industria).ThreadLocal
caches; Esos son malos en muchos casos. Estoy seguro de que todo el mundo ha visto bastantes cachés simples basados en ThreadLocal, bueno, la mala noticia: si el hilo continúa más de lo esperado la vida del contexto ClassLoader, es una pequeña fuga. No utilice cachés ThreadLocal a menos que sea realmente necesario.Llamar a
ThreadGroup.destroy()
cuando ThreadGroup no tiene subprocesos en sí, pero aún mantiene ThreadGroups secundarios. Una mala fuga que evitará que ThreadGroup se elimine de su padre, pero todos los niños se vuelven insumibles.El uso de WeakHashMap y el valor (en) hace referencia directamente a la clave. Esto es difícil de encontrar sin un volcado de pila. Eso se aplica a todas las referencias
Weak/SoftReference
extendidas que podrían mantener una referencia difícil al objeto protegido.Usando
java.net.URL
con el protocolo HTTP (S) y cargando el recurso desde (!). Este es especial,KeepAliveCache
crea un nuevo subproceso en el sistema ThreadGroup que pierde el cargador de clases de contexto del subproceso actual. El hilo se crea en la primera solicitud cuando no existe un hilo vivo, por lo que puede tener suerte o simplemente perder. La fuga ya está reparada en Java 7 y el código que crea el subproceso elimina correctamente el cargador de clases de contexto. Hay pocos casos más (como ImageFetcher, también arreglado ) de crear hilos similares.Usando
InflaterInputStream
pasando unnew java.util.zip.Inflater()
en el constructor (PNGImageDecoder
por ejemplo) y no llamando aend()
del inflater. Bueno, si pasas el constructor con solo algonew
, no hay posibilidad ... Y sí, llamar aclose()
en la secuencia no cierra el inflater si se pasa manualmente como parámetro de constructor. Esto no es una fuga verdadera, ya que sería lanzado por el finalizador ... cuando lo considere necesario. Hasta ese momento, se come la memoria nativa y puede hacer que Linux oom_killer mate el proceso con impunidad. El principal problema es que la finalización en Java es muy poco confiable y G1 lo empeoró hasta la 7.0.2. Moraleja de la historia: libera recursos nativos tan pronto como puedas; el finalizador es demasiado pobre.El mismo caso con
java.util.zip.Deflater
. Este es mucho peor ya que Deflater está hambriento de memoria en Java, es decir, siempre usa 15 bits (máximo) y 8 niveles de memoria (9 es máximo) asignando varios cientos de KB de memoria nativa. Afortunadamente,Deflater
no se usa mucho y, según mi conocimiento, JDK no contiene malos usos. Siempre llame aend()
si crea manualmente unDeflater
oInflater
. La mejor parte de los dos últimos: no puede encontrarlos a través de las herramientas de perfiles normales disponibles.
(Puedo agregar más pérdidas de tiempo que he encontrado a pedido).
Buena suerte y cuídate; ¡Las fugas son malas!
Acabo de tener una entrevista y me pidieron que creara una pérdida de memoria con Java. No hace falta decir que me sentí bastante tonto al no tener ni idea de cómo empezar a crear uno.
¿Cuál sería un ejemplo?
Cada vez que mantiene referencias a objetos que ya no necesita, tiene una pérdida de memoria. Consulte Manejo de las pérdidas de memoria en los programas Java para ver ejemplos de cómo se manifiestan las pérdidas de memoria en Java y qué puede hacer al respecto.
El siguiente es un ejemplo bastante inútil, si no entiende JDBC . O al menos cómo JDBC espera que un desarrollador cierre las instancias de Connection
, Statement
y ResultSet
antes de descartarlas o perder las 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 objeto Connection
no está cerrado y, por lo tanto, la conexión física permanecerá abierta, hasta que el recolector de basura llegue y vea que no se puede acceder a él. GC invocará el método de finalize
, pero hay controladores JDBC que no implementan la finalize
, al menos no de la misma manera que se implementa Connection.close
. El comportamiento resultante es que, si bien la memoria se reclamará debido a que se están recolectando objetos inalcanzables, los recursos (incluida la memoria) asociados con el objeto de Connection
simplemente no se podrán reclamar.
En un evento de este tipo en el que el método de finalize
la Connection
no limpia todo, uno puede encontrar que la conexión física al servidor de la base de datos durará varios ciclos de recolección de basura, hasta que el servidor de la base de datos finalmente descubra que la conexión no está activa ( Si lo hace), y debe estar cerrado.
Incluso si el controlador JDBC implementara la finalize
, es posible que se produzcan excepciones durante la finalización. El comportamiento resultante es que cualquier memoria asociada con el objeto ahora "inactivo" no se reclamará, ya que se garantiza que la finalize
se invoca solo una vez.
El escenario anterior de encontrar excepciones durante la finalización del objeto está relacionado con otro escenario que posiblemente podría conducir a una pérdida de memoria: la resurrección del objeto. La resurrección de objetos a menudo se hace intencionalmente al crear una referencia fuerte al objeto desde su finalización, desde otro objeto. Cuando se utiliza mal la resurrección de objetos, se producirá una pérdida de memoria en combinación con otras fuentes de fugas de memoria.
Hay muchos más ejemplos que puedes evocar, como
- Administrar una instancia de
List
en la que solo está agregando a la lista y no eliminando de ella (aunque debería deshacerse de los elementos que ya no necesita), o - Abrir
Socket
s oFile
s, pero no cerrarlos cuando ya no son necesarios (similar al ejemplo anterior relacionado con la claseConnection
). - No descargar Singletons al desactivar una aplicación Java EE. Aparentemente, el Classloader que cargó la clase singleton conservará una referencia a la clase y, por lo tanto, la instancia de singleton nunca se recopilará. Cuando se implementa una nueva instancia de la aplicación, generalmente se crea un nuevo cargador de clases, y el cargador de clases anterior continuará existiendo debido al singleton.
Esta es una buena manera de crear una verdadera pérdida de memoria (objetos inaccesibles al ejecutar el código pero aún almacenados en la memoria) en Java puro:
- La aplicación crea un subproceso de larga duración (o usa un conjunto de subprocesos para filtrarse aún más rápido).
- El hilo carga una clase a través de un ClassLoader (opcionalmente personalizado).
- La clase asigna una gran parte de la memoria (por ejemplo,
new byte[1000000]
), almacena una fuerte referencia a ella en un campo estático y luego almacena una referencia a sí misma en un ThreadLocal. Asignar la memoria adicional es opcional (filtrar la instancia de Clase es suficiente), pero hará que la fuga funcione mucho más rápido. - El hilo borra todas las referencias a la clase personalizada o al ClassLoader desde el que se cargó.
- Repetir.
Esto funciona porque ThreadLocal mantiene una referencia al objeto, que mantiene una referencia a su Clase, que a su vez mantiene una referencia a su ClassLoader. El ClassLoader, a su vez, mantiene una referencia a todas las Clases que ha cargado.
(Fue peor en muchas implementaciones de JVM, especialmente antes de Java 7, porque las Clases y los ClassLoaders se asignaron directamente a permgen y nunca recibieron GC. Sin embargo, independientemente de cómo JVM maneja la descarga de clases, un ThreadLocal todavía evitará Objeto de clase de ser reclamado.)
Una variación de este patrón es la razón por la cual los contenedores de aplicaciones (como Tomcat) pueden perder memoria como un tamiz si frecuentemente se vuelven a implementar las aplicaciones que usan ThreadLocals de alguna manera. (Dado que el contenedor de la aplicación utiliza subprocesos como se describe, y cada vez que vuelva a implementar la aplicación, se utiliza un nuevo ClassLoader.)
Actualización : dado que muchas personas siguen pidiéndolo, aquí hay un código de ejemplo que muestra este comportamiento en acción .
La mayoría de los ejemplos aquí son "demasiado complejos". Son casos de borde. Con estos ejemplos, el programador cometió un error (como no redefinir es igual a / hashcode), o ha sido mordido por un caso de esquina de JVM / JAVA (carga de clase con estática ...). Creo que ese no es el tipo de ejemplo que quiere un entrevistador o incluso el caso más común.
Pero hay casos realmente más simples para las fugas de memoria. El recolector de basura solo libera lo que ya no está referenciado. Nosotros, como desarrolladores de Java, no nos importa la memoria. Lo asignamos cuando es necesario y lo liberamos automáticamente. Multa.
Pero cualquier aplicación de larga duración suele tener estado compartido. Puede ser cualquier cosa, estática, singletons ... A menudo, las aplicaciones no triviales tienden a hacer gráficos de objetos complejos. El simple hecho de olvidarse de establecer una referencia en nulo o, con más frecuencia, olvidar eliminar un objeto de una colección es suficiente para provocar una pérdida de memoria.
Por supuesto, todo tipo de escuchas (como escuchas de UI), cachés o cualquier estado compartido de larga duración tienden a producir una pérdida de memoria si no se manejan adecuadamente. Lo que debe entenderse es que esto no es un caso de Java, o un problema con el recolector de basura. Es un problema de diseño. Diseñamos que agregamos un oyente a un objeto de larga duración, pero no lo eliminamos cuando ya no es necesario. Cacheamos objetos, pero no tenemos una estrategia para eliminarlos de la caché.
Tal vez tengamos un gráfico complejo que almacene el estado anterior que es necesario para un cálculo. Pero el estado anterior está vinculado al estado anterior y así sucesivamente.
Como tenemos que cerrar conexiones o archivos de SQL. Necesitamos establecer referencias adecuadas en nulo y eliminar elementos de la colección. Tendremos estrategias de almacenamiento en caché adecuadas (tamaño máximo de memoria, número de elementos o temporizadores). Todos los objetos que permiten notificar a un oyente deben proporcionar tanto un método addListener como un método removeListener. Y cuando estos notificadores ya no se utilizan, deben borrar su lista de oyentes.
Una pérdida de memoria es realmente posible y es perfectamente predecible. No hay necesidad de características especiales de lenguaje o casos de esquina. Las fugas de memoria son un indicador de que falta algo o incluso de problemas de diseño.
La respuesta depende completamente de lo que el entrevistador pensó que estaban preguntando.
¿Es posible en la práctica hacer fugas de Java? Por supuesto que lo es, y hay muchos ejemplos en las otras respuestas.
¿Pero hay múltiples meta-preguntas que pueden haber sido formuladas?
- ¿Es teórica una implementación Java "perfecta" vulnerable a las 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 la recolección de basura funciona en un caso ideal?
- ¿Saben que pueden llamar a otros idiomas a través de interfaces nativas?
- ¿Saben perder memoria en esos otros idiomas?
- ¿Sabe el candidato qué es la administración de la memoria y qué sucede detrás de la escena en Java?
Estoy leyendo tu meta-pregunta como "¿Qué respuesta podría haber usado en esta entrevista"? Y por lo tanto, me centraré en las habilidades de entrevista en lugar de Java. Creo que es más probable que repita la situación de no saber la respuesta a una pregunta en una entrevista que estar en el lugar de necesitar saber cómo hacer que Java se filtre. Así que, con suerte, esto ayudará.
Una de las habilidades más importantes que puede desarrollar para entrevistar es aprender a escuchar activamente las preguntas y trabajar con el entrevistador para extraer su intención. Esto no solo le permite responder a sus preguntas de la manera que desean, sino que también demuestra que tiene algunas habilidades de comunicación vitales. Y cuando se trata de elegir entre muchos desarrolladores igualmente talentosos, contrataré a uno que escuche, piense y entienda antes de que respondan cada vez.
Probablemente uno de los ejemplos más simples de una posible pérdida de memoria, y cómo evitarlo, es la implementación de ArrayList.remove (int):
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index + 1, elementData, index,
numMoved);
elementData[--size] = null; // (!) Let gc do its work
return oldValue;
}
Si lo estuviera implementando usted mismo, ¿habría pensado borrar el elemento del arreglo que ya no se usa ( elementData[--size] = null
)? Esa referencia podría mantener vivo un gran objeto ...
Puedes hacer una pérdida de memoria con la clase sun.misc.Unsafe . De hecho, esta clase de servicio se utiliza en diferentes clases estándar (por ejemplo, en clases java.nio ). No puede crear una instancia de esta clase directamente , pero puede usar la reflexión para hacer eso .
El código no se compila en el IDE de Eclipse: compílelo usando el comando javac
(durante la compilación obtendrá advertencias)
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
public class TestUnsafe {
public static void main(String[] args) throws Exception{
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
System.out.print("4..3..2..1...");
try
{
for(;;)
unsafe.allocateMemory(1024*1024);
} catch(Error e) {
System.out.println("Boom :)");
e.printStackTrace();
}
}
}
Puedo copiar mi respuesta desde aquí: ¿La forma más fácil de causar pérdida de memoria en Java?
"Una pérdida de memoria, en ciencias de la computación (o fugas, en este contexto), ocurre cuando un programa de computadora consume memoria pero no puede devolverla al sistema operativo". (Wikipedia)
La respuesta fácil es: no puedes. Java realiza la administración automática de 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 gestión de memoria manual, esto es diferente. Puedes obtener algo de memoria en C usando malloc (). Para liberar la memoria, necesita el puntero que malloc devolvió y llamar a free (). Pero si ya no tiene el puntero (sobrescrito o se ha superado el tiempo de vida), desafortunadamente no puede liberar esta memoria y, por lo tanto, tiene una pérdida de memoria.
Todas las otras respuestas hasta ahora están en mi definición, no son realmente pérdidas de memoria. Todos apuntan a llenar la memoria con cosas sin sentido muy rápido. Pero en cualquier momento aún podría desreferenciar los objetos que creó y así liberar la memoria -> NO LEAK. Sin embargo, la respuesta de acconrad es bastante cercana, ya que tengo que admitir que su solución es efectivamente "bloquear" al recolector de basura forzándolo en un bucle sin fin.
La respuesta larga es: puede obtener una pérdida de memoria al escribir una biblioteca para Java utilizando el JNI, que puede tener administración de memoria manual y, por lo tanto, tener pérdidas de memoria. Si llama a esta biblioteca, su proceso java perderá memoria. O bien, puede tener errores en la JVM, de modo que la JVM pierde memoria.Probablemente hay errores en la JVM, incluso puede haber algunos conocidos, ya que la recolección de basura no es tan trivial, pero sigue siendo un error. Por diseño esto no es posible. Es posible que solicite algún código Java que se vea afectado por dicho error. Lo siento, no sé uno y puede que ya no sea un error en la próxima versión de Java.
Una cosa simple a hacer es usar un HashSet con un código de hash incorrecto (o inexistente) hashCode()
o equals()
, y luego seguir agregando "duplicados". En lugar de ignorar los duplicados como debería, el conjunto solo crecerá y no podrás eliminarlos.
Si desea que estas claves / elementos defectuosos permanezcan cerca, puede utilizar un campo estático como
class BadKey {
// no hashCode or equals();
public final String key;
public BadKey(String key) { this.key = key; }
}
Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.
¿Qué es una pérdida de memoria?
- Es causado por un error o mal diseño.
- Es un desperdicio de memoria.
- Se empeora con el tiempo.
- El recolector de basura no puede limpiarlo.
Ejemplo típico:
Un caché de objetos es un buen punto de partida para desordenar las cosas.
private static final Map<String, Info> myCache = new HashMap<>();
public void getInfo(String key)
{
// uses cache
Info info = myCache.get(key);
if (info != null) return info;
// if it''s not in cache, then fetch it from the database
info = Database.fetch(key);
if (info == null) return null;
// and store it in the cache
myCache.put(key, info);
return info;
}
Tu caché crece y crece. Y muy pronto toda la base de datos será absorbida por la memoria. Un diseño mejor utiliza un LRUMap (solo mantiene los objetos utilizados recientemente en la memoria caché).
Claro, puedes hacer las cosas mucho más complicadas:
- utilizando construcciones ThreadLocal .
- Añadiendo árboles de referencia más complejos .
- o fugas causadas por bibliotecas de terceros .
Lo que pasa a menudo:
Si este objeto de información tiene referencias a otros objetos, que también tienen referencias a otros objetos. En cierto modo, también podría considerar que se trata de algún tipo de pérdida de memoria (causada por un mal diseño).
Campo estático que contiene la referencia del objeto [esp campo final]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
Llamando a String.intern()
en String largo
String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can''t remove
str.intern();
(No cerrado) flujos abiertos (archivo, red, etc.)
try {
BufferedReader br = new BufferedReader(new FileReader(inputFile));
...
...
} catch (Exception e) {
e.printStacktrace();
}
Conexiones no cerradas
try {
Connection conn = ConnectionFactory.getConnection();
...
...
} catch (Exception e) {
e.printStacktrace();
}
Áreas que son inaccesibles 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 alcance de la aplicación hasta que la aplicación se detiene o elimina explícitamente.
getServletContext().setAttribute("SOME_MAP", map);
Opciones de JVM incorrectas o inapropiadas , como la opción noclassgc
en IBM JDK que evita la recolección de basura de clase no utilizada
Ver la configuración de IBM jdk .
Creo que un ejemplo válido podría ser utilizar variables ThreadLocal en un entorno donde se agrupan los hilos.
Por ejemplo, utilizando las variables ThreadLocal en Servlets para comunicarse con otros componentes web, el contenedor crea las hebras y mantiene las inactivas en un grupo. Las variables ThreadLocal, si no se limpian correctamente, permanecerán allí hasta que, posiblemente, el mismo componente web sobrescriba sus valores.
Por supuesto, una vez identificado, el problema se puede resolver fácilmente.
El entrevistador podría haber estado buscando una solución de referencia circular:
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
Este es un problema clásico con el recuento de referencias recolectores de basura. Luego explicaría cortésmente que las JVM utilizan un algoritmo mucho más sofisticado que no tiene esta limitación.
-Wes Tarle
Recientemente encontré una situación de pérdida de memoria causada de alguna manera por log4j.
Log4j tiene este mecanismo denominado Contexto de diagnóstico anidado (NDC), que es un instrumento para distinguir la salida del registro intercalado de diferentes fuentes. La granularidad en la que funciona 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 Hashtable que está codificada por el propio objeto Thread (en lugar de decir el ID del hilo), y así hasta que la etiqueta NDC permanece en la memoria todos los objetos que cuelgan del hilo El objeto también se queda en la memoria. En nuestra aplicación web, utilizamos NDC para etiquetar las salidas de registro con un ID de solicitud para distinguir los registros de una sola solicitud por separado. El contenedor que asocia la etiqueta NDC con un subproceso, también la elimina al devolver la respuesta de una solicitud. El problema se produjo cuando, durante el proceso de procesamiento de una solicitud, se generó un subproceso secundario, 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();
}
}
Por lo tanto, un contexto NDC se asoció con el hilo en línea que se generó. El objeto de subproceso que fue la clave para este contexto NDC, es el subproceso en línea que tiene el objeto hugeList colgando de él. Por lo tanto, incluso después de que el subproceso terminó de hacer lo que estaba haciendo, la referencia a hugeList se mantuvo viva mediante el contexto de NDC Hastable, lo que provocó una pérdida de memoria.
Tal vez utilizando código nativo externo a través de JNI?
Con Java puro, es casi imposible.
Pero eso se trata de un tipo "estándar" de pérdida de memoria, cuando ya no puede acceder a la memoria, pero aún es propiedad de la aplicación. En su lugar, puede mantener las referencias a los objetos no utilizados o abrir secuencias sin cerrarlas después.
Aquí hay uno simple / siniestro a través de http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29 .
public class StringLeaker
{
private final String muchSmallerString;
public StringLeaker()
{
// Imagine the whole Declaration of Independence here
String veryLongString = "We hold these truths to be self-evident...";
// The substring here maintains a reference to the internal char[]
// representation of the original string.
this.muchSmallerString = veryLongString.substring(0, 1);
}
}
Debido a que la subcadena se refiere a la representación interna de la cadena original, mucho más larga, la original permanece en la memoria. Por lo tanto, siempre que tenga un StringLeaker en juego, también tendrá toda la cadena original en la memoria, aunque piense que solo está sosteniendo una cadena de un solo carácter.
La forma de evitar almacenar una referencia no deseada a la cadena original es hacer algo como esto:
...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...
Para mayor maldad, también puede ser .intern()
la subcadena:
...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...
Si lo hace, mantendrá la cadena larga original y la subcadena derivada en la memoria incluso después de que se haya descartado la instancia de StringLeaker.
Como muchas personas han sugerido, las Fugas de recursos son bastante fáciles de causar, como los ejemplos de JDBC. Las fugas reales de memoria son un poco más difíciles, especialmente si no confía en los bits rotos de la JVM para que lo haga por usted ...
Las ideas de crear objetos que tienen una huella muy grande y luego no poder acceder a ellos tampoco son fugas reales de memoria. Si nada puede acceder a él, entonces se recogerá la basura, y si algo puede acceder a él, entonces no es una fuga ...
Sin embargo, una forma en que solía funcionar, y no sé si aún funciona, es tener una cadena circular de tres profundidades. Como en el Objeto A hay 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 CG era lo suficientemente inteligente como para saber que una cadena profunda dos, como en A <--> B - se puede recolectar de manera segura si A y B no son accesibles por otra cosa, pero no pueden manejar la cadena de tres vías ...
Cree un mapa estático y siga agregando referencias difíciles. Esos nunca serán GC''d.
public class Leaker {
private static final Map<String, Object> CACHE = new HashMap<String, Object>();
// Keep adding until failure.
public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}
El entrevistador probablemente estaba buscando una referencia circular como el código a continuación (que, por cierto, solo pierde memoria en JVM muy antiguas que usaban el conteo de referencias, que ya no es el caso) Pero es una pregunta bastante vaga, por lo que es una excelente oportunidad para demostrar su comprensión de la gestión de memoria JVM.
class A {
B bRef;
}
class B {
A aRef;
}
public class Main {
public static void main(String args[]) {
A myA = new A();
B myB = new B();
myA.bRef = myB;
myB.aRef = myA;
myA=null;
myB=null;
/* at this point, there is no access to the myA and myB objects, */
/* even though both objects still have active references. */
} /* main */
}
Luego puede explicar que con el recuento de referencias, el código anterior perdería memoria. Pero la mayoría de las JVM modernas ya no usan el conteo de referencias, la mayoría usa un recolector de basura de barrido, que de hecho recopilará esta memoria.
A continuación, puede explicar cómo crear un Objeto que tenga un recurso nativo subyacente, como este:
public class Main {
public static void main(String args[]) {
Socket s = new Socket(InetAddress.getByName("google.com"),80);
s=null;
/* at this point, because you didn''t close the socket properly, */
/* you have a leak of a native descriptor, which uses memory. */
}
}
Luego, puede explicar que esto es técnicamente una pérdida de memoria, pero en realidad la pérdida es causada por el código nativo en la JVM que asigna recursos nativos subyacentes, que no fueron liberados por su código Java.
Al final del día, con una JVM moderna, necesita escribir algún código Java que asigne un recurso nativo fuera del alcance normal de la conciencia de la JVM.
He tenido una buena "pérdida de memoria" en relación con PermGen y el análisis de XML una vez. El analizador XML que usamos (no recuerdo cuál era) hizo un String.intern () en los nombres de las etiquetas, para hacer la 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 etiqueta, 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 que llegaban a una tasa de 10-15 millones por día. Eso hace 200 MB de basura al día, lo que nunca más se necesita, y nunca se recalca (ya que está en PermGen). Teníamos el permgen establecido en 512 MB, por lo que tardaron alrededor de dos días en llegar la excepción de memoria insuficiente (OOME) ...
Los hilos no se recogen hasta que terminan. Sirven como roots de la recolección de basura. Son uno de los pocos objetos que no se recuperarán simplemente olvidándolos o borrando referencias a ellos.
Considere: el patrón básico para terminar un hilo de trabajo es establecer alguna variable de condición vista por el hilo. El hilo puede verificar la variable periódicamente y usarla como una señal para terminar. Si la variable no está declarada volatile
, entonces el hilo no verá el cambio en la variable, por lo que no sabrá que termine. O imagínese si algunos subprocesos desean actualizar un objeto compartido, pero un interbloqueo al intentar bloquearlo.
Si solo tiene un puñado de hilos, estos errores probablemente serán obvios porque su programa dejará de funcionar correctamente. Si tiene un grupo de subprocesos que crea más subprocesos según sea necesario, es posible que los subprocesos obsoletos / atascados no se noten y se acumulen indefinidamente, provocando una pérdida de memoria. Es probable que los subprocesos utilicen otros datos en su aplicación, por lo que también evitará que se recopilen los datos a los que hacen referencia directamente.
Como ejemplo de juguete:
static void leakMe(final Object object) {
new Thread() {
public void run() {
Object o = object;
for (;;) {
try {
sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {}
}
}
}.start();
}
Llama a System.gc()
todos los que quieras, pero el objeto que se pasa leakMe
nunca morirá.
(* editado *)
Me encontré con un tipo más sutil de fuga de recursos recientemente. Abrimos recursos a través del cargador de clases getResourceAsStream y sucedió que los manejadores de flujo de entrada no estaban cerrados.
Uhm, podrías decir, que idiota.
Bueno, lo que lo hace interesante es: de esta manera, puede perder memoria de pila del proceso subyacente, en lugar de hacerlo de la pila de JVM.
Todo lo que necesita es un archivo jar con un archivo dentro del cual se hará referencia desde el código Java. Cuanto más grande es el archivo jar, más rápido se asigna la memoria.
Puede crear fácilmente un frasco de este tipo con la siguiente clase:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class BigJarCreator {
public static void main(String[] args) throws IOException {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
zos.putNextEntry(new ZipEntry("resource.txt"));
zos.write("not too much in here".getBytes());
zos.closeEntry();
zos.putNextEntry(new ZipEntry("largeFile.out"));
for (int i=0 ; i<10000000 ; i++) {
zos.write((int) (Math.round(Math.random()*100)+20));
}
zos.closeEntry();
zos.close();
}
}
Solo pegue en un archivo llamado BigJarCreator.java, compile y ejecútelo desde la línea de comando:
javac BigJarCreator.java
java -cp . BigJarCreator
Et voilà: encuentra un archivo jar en su directorio de trabajo actual con dos archivos dentro.
Vamos a crear una segunda clase:
public class MemLeak {
public static void main(String[] args) throws InterruptedException {
int ITERATIONS=100000;
for (int i=0 ; i<ITERATIONS ; i++) {
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
}
System.out.println("finished creation of streams, now waiting to be killed");
Thread.sleep(Long.MAX_VALUE);
}
}
Esta clase básicamente no hace nada, pero crea objetos InputStream sin referencia. Esos objetos se recolectarán de inmediato y, por lo tanto, no contribuyen al tamaño del montón. Es importante para nuestro ejemplo cargar un recurso existente desde un archivo jar, ¡y el tamaño sí importa aquí!
Si tiene dudas, intente compilar e iniciar la clase anterior, pero asegúrese de elegir un tamaño de pila decente (2 MB):
javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak
No encontrará un error de OOM aquí, ya que no se guardan referencias, la aplicación continuará ejecutándose sin importar el tamaño que elija ITERATIONS en el ejemplo anterior. El consumo de memoria de su proceso (visible en la parte superior (RES / RSS) o en el explorador de procesos) aumenta a menos que la aplicación llegue al comando de espera. En la configuración anterior, asignará alrededor de 150 MB en memoria.
Si desea que la aplicación sea segura, cierre la secuencia de entrada donde se creó:
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
y su proceso no superará los 35 MB, independientemente del recuento de iteraciones.
Bastante simple y sorprendente.
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 este en algún lugar. Al recolector de basura solo se llamará una vez en el objeto, por lo que, después, el objeto nunca se destruirá.
Pensé que era interesante que nadie usara los ejemplos internos de clase. Si tienes una clase interna; mantiene inherentemente una referencia a la clase contenedora. Por supuesto, técnicamente no es una pérdida de memoria porque Java finalmente lo limpiará; pero esto puede hacer que las clases permanezcan más tiempo de lo previsto.
public class Example1 {
public Example2 getNewExample2() {
return this.new Example2();
}
public class Example2 {
public Example2() {}
}
}
Ahora, si llama a Example1 y obtiene un Example2 descartando Example1, intrínsecamente tendrá un enlace a un objeto Example1.
public class Referencer {
public static Example2 GetAnExample2() {
Example1 ex = new Example1();
return ex.getNewExample2();
}
public static void main(String[] args) {
Example2 ex = Referencer.GetAnExample2();
// As long as ex is reachable; Example1 will always remain in memory.
}
}
También escuché el rumor de que si tiene una variable que existe por más tiempo que una cantidad específica de tiempo; Java asume que siempre existirá y que en realidad nunca intentará limpiarlo si no se puede encontrar en el código. Pero eso está completamente sin verificar.
Puede crear una pérdida de memoria en movimiento creando una nueva instancia de una clase en el método de finalización de esa clase. Puntos de bonificación si el finalizador crea múltiples instancias. Aquí hay un programa simple que pierde todo el montón en algún momento entre unos pocos segundos y unos minutos, según el tamaño de tu montón:
class Leakee {
public void check() {
if (depth > 2) {
Leaker.done();
}
}
private int depth;
public Leakee(int d) {
depth = d;
}
protected void finalize() {
new Leakee(depth + 1).check();
new Leakee(depth + 1).check();
}
}
public class Leaker {
private static boolean makeMore = true;
public static void done() {
makeMore = false;
}
public static void main(String[] args) throws InterruptedException {
// make a bunch of them until the garbage collector gets active
while (makeMore) {
new Leakee(0).check();
}
// sit back and watch the finalizers chew through memory
while (true) {
Thread.sleep(1000);
System.out.println("memory=" +
Runtime.getRuntime().freeMemory() + " / " +
Runtime.getRuntime().totalMemory());
}
}
}
Todo el mundo siempre se olvida de la ruta del código nativo. Aquí hay una fórmula simple para una fuga:
- Declara el método nativo.
- En método nativo, llamada
malloc
. No llamefree
. - Llama al método nativo.
Recuerde, las asignaciones de memoria en código nativo provienen de la pila JVM.
Tome cualquier aplicación web que se ejecute en cualquier contenedor de servlets (Tomcat, Jetty, Glassfish, lo que sea ...). Vuelva a implementar la aplicación 10 o 20 veces seguidas (puede ser suficiente con simplemente tocar el WAR en el directorio de despliegue automático del servidor).
A menos que alguien haya probado esto realmente, es probable que obtenga un OutOfMemoryError después de un par de redistribuciones, ya que la aplicación no se preocupó de limpiar después de la misma. Incluso puede encontrar un error en su servidor con esta prueba.
El problema es que la vida útil del contenedor es más larga que la vida útil de su aplicación. Debe asegurarse de que todas las referencias que pueda tener el contenedor a objetos o clases de su aplicación puedan ser recolectadas en la basura.
Si solo hay una referencia que sobrevive a la desinstalación de su aplicación web, el cargador de clases correspondiente y, por consiguiente, todas las clases de su aplicación web no se pueden recolectar.
Los subprocesos iniciados por su aplicación, las variables ThreadLocal y los agregadores de registros son algunos de los sospechosos habituales que causan fugas en el cargador de clases.
Un ejemplo común de esto en el código GUI es cuando se crea un widget / componente y se agrega un oyente a algún objeto estático / con ámbito de aplicación y luego no se elimina el escucha cuando se destruye el widget. No solo obtienes una pérdida de memoria, sino también un impacto en el rendimiento, ya que cuando escuchas los eventos, todos tus oyentes son llamados también.
hay muchas situaciones diferentes de memoria se perderá. Uno que encontré, que expone un mapa que no debe ser expuesto y utilizado en otro lugar.
public class ServiceFactory {
private Map<String, Service> services;
private static ServiceFactory singleton;
private ServiceFactory() {
services = new HashMap<String, Service>();
}
public static synchronized ServiceFactory getDefault() {
if (singleton == null) {
singleton = new ServiceFactory();
}
return singleton;
}
public void addService(String name, Service serv) {
services.put(name, serv);
}
public void removeService(String name) {
services.remove(name);
}
public Service getService(String name, Service serv) {
return services.get(name);
}
// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose
public Map<String, Service> getAllServices() {
return services;
}
}
// resource class is a heavy class
class Service {
}