2012-02-24 11 views
15

Estoy tratando de determinar cuánta memoria de pila consume cada método cuando se ejecuta. Para hacer la tarea, he ideado este sencillo programa que se acaba de forzar un StackOverflowError,Inferir el uso de la memoria de pila de un método en Java

public class Main { 
    private static int i = 0; 

    public static void main(String[] args) { 
     try { 
      m(); 
     } catch (StackOverflowError e) { 
      System.err.println(i); 
     } 
    } 

    private static void m() { 
     ++i; 
     m(); 
    } 
} 

imprimir un entero decirme cuántas veces m() se llamaba. Me he fijado manualmente tamaño de la pila de la JVM (-Xss parámetro VM) a valores variables (128k, 256k, 384 K), obteniendo los siguientes valores:

stack i  delta 
    128  1102 
    256  2723 1621 
    384  4367 1644 

delta se calculó por mí, y es el valor entre el último línea i y la actual. Como se esperaba, es fijo. Y ahí radica el problema. Como sé que el incremento de la memoria del tamaño de la pila fue de 128k, eso produce algo así como un uso de memoria de 80 bytes por llamada (lo que parece exagerado).

Mirando hacia arriba m() en BytecodeViewer de, obtenemos profundidad máxima de una pila de 2. Sabemos que este es un método estático y que no hay this paso de parámetros, y que m() tiene argumentos. También debemos tener en cuenta el puntero de la dirección de retorno. Entonces debería haber algo así como 3 * 8 = 24 bytes usados ​​por llamada a método (supongo que 8 bytes por variable, que por supuesto puede estar totalmente apagado. ¿Lo es?). Incluso si es un poco más que eso, digamos 48bytes, todavía estamos lejos del valor de 80bytes.

Pensé que podría tener algo que ver con la alineación de memoria, pero la verdad es que en ese caso tendríamos un valor de aproximadamente 64 o 128 bytes, diría yo.

Estoy ejecutando una JVM de 64 bits en un sistema operativo Windows 7 de 64 bits.

He hecho varias suposiciones, algunas de las cuales pueden ser totalmente inactivas. Siendo ese el caso, soy todo oídos.

Antes de que alguien comienza a preguntar por qué estoy haciendo esto I must be frank..

Respuesta

2

Esta pregunta puede estar muy por encima de mi cabeza, tal vez estés hablando de esto en un nivel más profundo, pero voy a arrojar mi respuesta por ahí de todos modos.

En primer lugar, ¿a qué se refiere por return address pointer? Cuando se termina un método, el método de retorno se saca del marco de la pila. Por lo tanto, no se almacena ninguna dirección de retorno dentro del método de ejecución Marco.

El método Frame almacena las variables locales. Como es estático y no tiene parámetros, estos deben estar vacíos como dices, y los tamaños de la pila de operaciones y los locales se fijan en tiempo de compilación, con cada unidad en cada uno de ellos con una anchura de 32 bits. Pero además de esto, el método también debe tener una referencia al conjunto constante de la clase a la que pertenece.

En adicional, la especificación de JVM especifica que los marcos de método may be extended with additional implementation-specific information, such as debugging information. pueden explicar los bytes restantes, según el compilador.

Todo procedente de la JVM Specification on Frames.

ACTUALIZACIÓN

fregar la fuente OpenJDK revela esto, que parece ser el struct que se pasa a los fotogramas de invocación de método. Da una buena idea sobre qué esperar dentro:

/* Invoke types */ 

#define INVOKE_CONSTRUCTOR 1 
#define INVOKE_STATIC  2 
#define INVOKE_INSTANCE 3 

typedef struct InvokeRequest { 
    jboolean pending;  /* Is an invoke requested? */ 
    jboolean started;  /* Is an invoke happening? */ 
    jboolean available; /* Is the thread in an invokable state? */ 
    jboolean detached;  /* Has the requesting debugger detached? */ 
    jint id; 
    /* Input */ 
    jbyte invokeType; 
    jbyte options; 
    jclass clazz; 
    jmethodID method; 
    jobject instance; /* for INVOKE_INSTANCE only */ 
    jvalue *arguments; 
    jint argumentCount; 
    char *methodSignature; 
    /* Output */ 
    jvalue returnValue; /* if no exception, for all but INVOKE_CONSTRUCTOR */ 
    jobject exception; /* NULL if no exception was thrown */ 
} InvokeRequest; 

Source

+0

Esa fue una información perspicaz, señor. Sin embargo, ¿podría teorizar sobre por qué cada llamada al método parece tener 80 bytes? –

+0

¿Puedo decirle qué información tiene mi propia implementación JVM dentro de la estructura Frame? – Jivings

+0

@devouredelysium Actualicé mi respuesta con la fuente OpenJDK. – Jivings

4

Es necesario incluir en la pila del puntero de instrucción (8 bytes) y puede haber otra información de contexto que se guarda incluso si usted no cree debería ser. La alineación podría ser de 16 bytes, 8 bytes como el montón. p.ej. podría reservar 8 bytes para el valor de retorno, incluso si no hay uno.

Java no es tan adecuado para el uso intensivo de la recursión como lo son muchos idiomas. p.ej. no hace la optimización de la llamada de la cola, que en este caso haría que tu programa se ejecutara para siempre. ;)

+0

Sí, me olvidé de establecer explícitamente que los 24bytes incluyen las 2 variables, además de la dirección de retorno. –

+3

"puede haber otra información de contexto que se guarda incluso si no cree que debería ser." ¡Eso es lo que quiero saber! ¡Estoy dando galletas y alcohol a cualquiera que esté disponible para arrojar algo de luz sobre el tema! –

+1

En llamadas JNI, se incluyen jenv (entorno) y jclass (la clase). La mejor manera de resolverlo es leer el código OpenJDK. –

Cuestiones relacionadas