Java Language
Gestión de memoria de Java
Buscar..
Observaciones
En Java, los objetos se asignan en el montón y la memoria del montón se reclama mediante la recolección automática de basura. Un programa de aplicación no puede eliminar explícitamente un objeto Java.
Los principios básicos de la recolección de basura Java se describen en el ejemplo de recolección de basura . Otros ejemplos describen la finalización, cómo activar el recolector de basura a mano y el problema de las fugas de almacenamiento.
Finalización
Un objeto Java puede declarar un método de finalize
. Este método se llama justo antes de que Java libere la memoria para el objeto. Normalmente se verá así:
public class MyClass {
//Methods for the class
@Override
protected void finalize() throws Throwable {
// Cleanup code
}
}
Sin embargo, hay algunas advertencias importantes sobre el comportamiento de la finalización de Java.
- Java no ofrece ninguna garantía sobre cuándo se llamará a un método
finalize()
. - Java ni siquiera garantiza que se llame a un método
finalize()
algún momento durante el tiempo de vida de la aplicación en ejecución. - Lo único que se garantiza es que se llamará al método antes de que se elimine el objeto ... si se elimina el objeto.
Las advertencias anteriores significan que es una mala idea confiar en el método de finalize
para realizar acciones de limpieza (u otras) que deben realizarse de manera oportuna. La dependencia excesiva de la finalización puede provocar fugas de almacenamiento, fugas de memoria y otros problemas.
En resumen, hay muy pocas situaciones en las que la finalización es realmente una buena solución.
Los finalizadores solo se ejecutan una vez.
Normalmente, un objeto se elimina después de que se ha finalizado. Sin embargo, esto no sucede todo el tiempo. Considere el siguiente ejemplo 1 :
public class CaptainJack {
public static CaptainJack notDeadYet = null;
protected void finalize() {
// Resurrection!
notDeadYet = this;
}
}
Cuando una instancia de CaptainJack
vuelve inaccesible y el recolector de basura intenta reclamarla, el método finalize()
asignará una referencia a la instancia a la variable notDeadYet
. Eso hará que la instancia sea accesible una vez más, y el recolector de basura no la eliminará.
Pregunta: ¿Es el Capitán Jack inmortal?
Respuesta: No.
El problema es que la JVM solo ejecutará un finalizador en un objeto una vez en su vida. Si asigna null
a notDeadYet
hace que una instancia resurectada no sea accesible una vez más, el recolector de basura no llamará a finalize()
en el objeto.
1 - Ver https://en.wikipedia.org/wiki/Jack_Harkness .
Activación manual de GC
Puede activar manualmente el recolector de basura llamando
System.gc();
Sin embargo, Java no garantiza que el recolector de basura se haya ejecutado cuando se devuelva la llamada. Este método simplemente "sugiere" a la JVM (Java Virtual Machine) que desea que ejecute el recolector de basura, pero no lo obliga a hacerlo.
En general, se considera una mala práctica tratar de activar manualmente la recolección de basura. La JVM se puede ejecutar con la -XX:+DisableExplicitGC
para deshabilitar las llamadas a System.gc()
. La activación de la recolección de basura mediante una llamada a System.gc()
puede interrumpir las actividades normales de gestión de basura / promoción de objetos de la implementación específica del recolector de basura en uso por la JVM.
Recolección de basura
El enfoque de C ++ - nuevo y eliminar
En un lenguaje como C ++, el programa de aplicación es responsable de administrar la memoria utilizada por la memoria asignada dinámicamente. Cuando se crea un objeto en el ++ montón usando el C new
operador, es necesario que haya un uso correspondiente de la delete
operador para disponer del objeto:
Si el programa se olvida de
delete
un objeto y simplemente se "olvida" de él, la memoria asociada se pierde en la aplicación. El término para esta situación es una pérdida de memoria , y demasiada pérdida de memoria de una aplicación puede usar más y más memoria, y eventualmente bloquearse.Por otro lado, si una aplicación intenta
delete
el mismo objeto dos veces, o usar un objeto después de que se haya eliminado, la aplicación puede fallar debido a problemas con la corrupción de la memoria.
En un programa de C ++ complicado, la implementación de la administración de memoria usando new
y delete
puede llevar mucho tiempo. De hecho, la gestión de la memoria es una fuente común de errores.
El enfoque de Java - recolección de basura
Java tiene un enfoque diferente. En lugar de un operador de delete
explícita, Java proporciona un mecanismo automático conocido como recolección de basura para recuperar la memoria utilizada por objetos que ya no son necesarios. El sistema de tiempo de ejecución de Java asume la responsabilidad de encontrar los objetos que se deben eliminar. Esta tarea es realizada por un componente llamado recolector de basura , o GC para abreviar.
En cualquier momento durante la ejecución de un programa Java, podemos dividir el conjunto de todos los objetos existentes en dos subconjuntos distintos 1 :
Los objetos accesibles son definidos por el JLS de la siguiente manera:
Un objeto accesible es cualquier objeto al que se pueda acceder en cualquier posible cálculo continuo desde cualquier hilo en vivo.
En la práctica, esto significa que hay una cadena de referencias a partir de una variable local dentro del alcance o una variable
static
por la cual algún código podría alcanzar el objeto.Los objetos inalcanzables son objetos a los que no se puede acceder como se mencionó anteriormente.
Cualquier objeto que sea inalcanzable es elegible para la recolección de basura. Esto no significa que serán recolectados en la basura. De hecho:
- Un objeto inalcanzable no se recolecta inmediatamente al volverse inalcanzable 1 .
- Un objeto inalcanzable nunca puede ser recogido basura.
La especificación del lenguaje Java le da mucha latitud a una implementación de JVM para decidir cuándo recoger objetos inalcanzables. También (en la práctica) otorga permiso para que una implementación de JVM sea conservadora en la forma en que detecta objetos inalcanzables.
Lo único que garantiza el JLS es que nunca se recogerá la basura en ningún objeto accesible .
¿Qué sucede cuando un objeto se vuelve inalcanzable?
En primer lugar, nada sucede específicamente cuando un objeto se vuelve inalcanzable. Las cosas solo suceden cuando el recolector de basura se ejecuta y detecta que el objeto es inalcanzable. Además, es común que una ejecución de GC no detecte todos los objetos inalcanzables.
Cuando el GC detecta un objeto inalcanzable, pueden ocurrir los siguientes eventos.
Si hay objetos de
Reference
que se refieren al objeto, esas referencias se borrarán antes de que se elimine el objeto.Si el objeto es finalizable , entonces será finalizado. Esto sucede antes de que se elimine el objeto.
El objeto se puede eliminar y la memoria que ocupa se puede recuperar.
Tenga en cuenta que hay una secuencia clara en la que pueden ocurrir los eventos anteriores, pero nada requiere que el recolector de basura realice la eliminación final de cualquier objeto específico en un período de tiempo específico.
Ejemplos de objetos alcanzables e inalcanzables
Considere las siguientes clases de ejemplo:
// A node in simple "open" linked-list.
public class Node {
private static int counter = 0;
public int nodeNumber = ++counter;
public Node next;
}
public class ListTest {
public static void main(String[] args) {
test(); // M1
System.out.prinln("Done"); // M2
}
private static void test() {
Node n1 = new Node(); // T1
Node n2 = new Node(); // T2
Node n3 = new Node(); // T3
n1.next = n2; // T4
n2 = null; // T5
n3 = null; // T6
}
}
Examinemos lo que sucede cuando se llama test()
. Las declaraciones T1, T2 y T3 crean objetos de Node
, y todos los objetos son accesibles a través de las variables n1
, n2
y n3
, respectivamente. La declaración T4 asigna la referencia al objeto del segundo Node
al next
campo del primero. Cuando se hace eso, se puede Node
al 2do Node
a través de dos rutas:
n2 -> Node2
n1 -> Node1, Node1.next -> Node2
En la declaración T5, asignamos null
a n2
. Esto rompe la primera de las cadenas de accesibilidad para Node2
, pero la segunda permanece intacta, por lo que Node2
todavía es accesible.
En la declaración T6, asignamos null
a n3
. Esto rompe la única cadena de accesibilidad para Node3
, lo que hace que Node3
sea inalcanzable. Sin embargo, Node1
y Node2
aún son accesibles a través de la variable n1
.
Finalmente, cuando el método test()
regresa, sus variables locales n1
, n2
y n3
quedan fuera del alcance y, por lo tanto, nadie puede acceder a ellas. Esto rompe las cadenas de accesibilidad restantes para Node1
y Node2
, y todos los objetos de Node
son inalcanzables y son elegibles para la recolección de basura.
1 - Esta es una simplificación que ignora la finalización y las clases de Reference
. 2 - Hipotéticamente, una implementación de Java podría hacer esto, pero el costo de rendimiento de hacerlo lo hace impráctico.
Configuración de los tamaños Heap, PermGen y Stack
Cuando se inicia una máquina virtual Java, necesita saber qué tan grande es el Heap y el tamaño predeterminado para las pilas de hilos. Estos se pueden especificar usando las opciones de línea de comando en el comando java
. Para las versiones de Java anteriores a Java 8, también puede especificar el tamaño de la región PermGen del montón.
Tenga en cuenta que PermGen se eliminó en Java 8, y si intenta establecer el tamaño de PermGen, la opción se ignorará (con un mensaje de advertencia).
Si no especifica explícitamente los tamaños de pila y pila, la JVM utilizará los valores predeterminados que se calculan de una manera específica de la versión y la plataforma. Esto puede hacer que su aplicación use muy poca o demasiada memoria. Esto suele ser correcto para las pilas de hilos, pero puede ser problemático para un programa que utiliza mucha memoria.
Configuración de los tamaños de pila, PermGen y pila predeterminada:
Las siguientes opciones de JVM establecen el tamaño del montón:
-
-Xms<size>
: establece el tamaño del montón inicial -
-Xmx<size>
: establece el tamaño máximo del montón -
-XX:PermSize<size>
- establece el tamaño inicial de PermGen -
-XX:MaxPermSize<size>
: establece el tamaño máximo de PermGen -
-Xss<size>
: establece el tamaño de pila de subprocesos predeterminado
El parámetro <size>
puede ser un número de bytes, o puede tener un sufijo de k
, m
o g
. Los últimos especifican el tamaño en kilobytes, megabytes y gigabytes respectivamente.
Ejemplos:
$ java -Xms512m -Xmx1024m JavaApp
$ java -XX:PermSize=64m -XX:MaxPermSize=128m JavaApp
$ java -Xss512k JavaApp
Encontrar los tamaños por defecto:
La -XX:+printFlagsFinal
se puede usar para imprimir los valores de todas las banderas antes de iniciar la JVM. Esto se puede usar para imprimir los valores predeterminados para las configuraciones de tamaño de pila y pila de la siguiente manera:
Para Linux, Unix, Solaris y Mac OSX.
$ java -XX: + PrintFlagsFinal -version | grep -iE 'HeapSize | PermSize | ThreadStackSize'
Para ventanas:
java -XX: + PrintFlagsFinal -version | findstr / i "HeapSize PermSize ThreadStackSize"
La salida de los comandos anteriores se asemejará a lo siguiente:
uintx InitialHeapSize := 20655360 {product}
uintx MaxHeapSize := 331350016 {product}
uintx PermSize = 21757952 {pd product}
uintx MaxPermSize = 85983232 {pd product}
intx ThreadStackSize = 1024 {pd product}
Los tamaños se dan en bytes.
Fugas de memoria en Java
En el ejemplo de recolección de basura , implicamos que Java resuelve el problema de las fugas de memoria. Esto no es realmente cierto. Un programa Java puede perder memoria, aunque las causas de las fugas son bastante diferentes.
Los objetos alcanzables pueden fugarse
Considere la siguiente implementación de pila ingenua.
public class NaiveStack {
private Object[] stack = new Object[100];
private int top = 0;
public void push(Object obj) {
if (top >= stack.length) {
throw new StackException("stack overflow");
}
stack[top++] = obj;
}
public Object pop() {
if (top <= 0) {
throw new StackException("stack underflow");
}
return stack[--top];
}
public boolean isEmpty() {
return top == 0;
}
}
Cuando push
un objeto y luego lo haces pop
, todavía habrá una referencia al objeto en la matriz de la stack
.
La lógica de la implementación de la pila significa que esa referencia no se puede devolver a un cliente de la API. Si se ha extraído un objeto, podemos probar que no se puede acceder a él en ningún posible cálculo continuo desde cualquier secuencia activa . El problema es que una JVM de generación actual no puede probar esto. Las JVM de generación actual no tienen en cuenta la lógica del programa para determinar si se puede acceder a las referencias. (Para empezar, no es práctico).
Pero dejando de lado el problema de lo que realmente significa accesibilidad , claramente tenemos una situación aquí donde la implementación de NaiveStack
está " NaiveStack
" a objetos que deberían ser reclamados. Eso es una pérdida de memoria.
En este caso, la solución es sencilla:
public Object pop() {
if (top <= 0) {
throw new StackException("stack underflow");
}
Object popped = stack[--top];
stack[top] = null; // Overwrite popped reference with null.
return popped;
}
Los cachés pueden ser fugas de memoria.
Una estrategia común para mejorar el rendimiento del servicio es almacenar en caché los resultados. La idea es que mantenga un registro de las solicitudes comunes y sus resultados en una estructura de datos en memoria conocida como caché. Luego, cada vez que se realiza una solicitud, busca la solicitud en el caché. Si la búsqueda tiene éxito, devuelve los resultados guardados correspondientes.
Esta estrategia puede ser muy efectiva si se implementa adecuadamente. Sin embargo, si se implementa incorrectamente, un caché puede ser una pérdida de memoria. Considere el siguiente ejemplo:
public class RequestHandler {
private Map<Task, Result> cache = new HashMap<>();
public Result doRequest(Task task) {
Result result = cache.get(task);
if (result == null) {
result == doRequestProcessing(task);
cache.put(task, result);
}
return result;
}
}
El problema con este código es que, si bien cualquier llamada a doRequest
puede agregar una nueva entrada al caché, no hay nada para eliminarlos. Si el servicio obtiene continuamente diferentes tareas, la memoria caché consumirá toda la memoria disponible. Esta es una forma de pérdida de memoria.
Un método para resolver esto es utilizar un caché con un tamaño máximo y eliminar las entradas antiguas cuando el caché excede el máximo. (Lanzar la entrada menos utilizada recientemente es una buena estrategia). Otro método consiste en crear la memoria caché utilizando WeakHashMap
para que JVM pueda expulsar las entradas de la memoria caché si el montón comienza a llenarse demasiado.