¿Las inicializaciones de objetos en Java "Foo f = new Foo ()" son esencialmente lo mismo que usar malloc para un puntero en C?

9

Estoy tratando de entender el proceso real detrás de las creaciones de objetos en Java, y supongo que otros lenguajes de programación.

¿Sería un error suponer que la inicialización de objetos en Java es la misma que cuando usa malloc para una estructura en C?

Ejemplo:

Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));

¿Es por eso que se dice que los objetos están en el montón en lugar de en la pila? ¿Porque son esencialmente solo punteros a los datos?

Jules
fuente
Los objetos se crean en el montón para lenguajes administrados como c # / java. En CPP se pueden crear objetos en la pila igual de bien
bas
¿Por qué los creadores de Java / C # decidieron almacenar objetos exclusivamente en el montón?
Julio
Yo creo que en aras de la simplicidad. Almacenar objetos en la pila y pasarlos un nivel más profundo implica copiar el objeto en la pila, lo que implica construir constructores. No Google para una respuesta correcta, pero estoy seguro de que usted puede encontrar una respuesta más satisfactoria a sí mismo (o alguien más lo hará más detalles sobre esta cuestión lado)
bas
Los objetos de @Jules en Java aún podrían "descomponerse" en tiempo de ejecución (llamados scalar-replacement) en campos simples que viven solo en la pila; pero eso es algo que JIThace, no javac.
Eugene
"Montón" es solo un nombre para un conjunto de propiedades asociadas con objetos / memoria asignados. En C / C ++ puede seleccionar entre dos conjuntos diferentes de propiedades, llamadas "pila" y "montón", en C # y Java, todas las asignaciones de objetos tienen el mismo comportamiento especificado, que se denomina "montón", que no implica que estas propiedades son las mismas que para el "montón" C / C ++, de hecho, no lo son. Esto no significa que las implementaciones no puedan tener diferentes estrategias para administrar los objetos, implica que esas estrategias son irrelevantes para la lógica de la aplicación.
Holger

Respuestas:

5

En C, malloc()asigna una región de memoria en el montón y le devuelve un puntero. Eso es todo lo que obtienes. La memoria no está inicializada y no tiene garantía de que sea todo ceros o cualquier otra cosa.

En Java, las llamadas newhacen una asignación basada en el montón como malloc(), pero también obtienes un montón de conveniencia adicional (o gastos generales, si lo prefieres). Por ejemplo, no tiene que especificar explícitamente el número de bytes que se asignarán. El compilador lo resuelve según el tipo de objeto que está tratando de asignar. Además, se llaman constructores de objetos (a los que puede pasar argumentos si desea controlar cómo se produce la inicialización). Cuando newregrese, tiene la garantía de tener un objeto inicializado.

Pero sí, al final de la llamada, tanto el resultado malloc()como newsimplemente son punteros a una porción de datos basados ​​en el montón.

La segunda parte de su pregunta se refiere a las diferencias entre una pila y un montón. Se pueden encontrar respuestas mucho más completas tomando un curso sobre (o leyendo un libro sobre) el diseño del compilador. Un curso sobre sistemas operativos también sería útil. También hay numerosas preguntas y respuestas en SO sobre las pilas y los montones.

Dicho esto, daré una descripción general que espero que no sea demasiado detallada y tenga como objetivo explicar las diferencias a un nivel bastante alto.

Fundamentalmente, la razón principal para tener dos sistemas de administración de memoria, es decir, un montón y una pila, es la eficiencia . Una razón secundaria es que cada uno es mejor en ciertos tipos de problemas que el otro.

Para mí, las pilas son algo más fáciles de entender como concepto, así que empiezo con las pilas. Consideremos esta función en C ...

int add(int lhs, int rhs) {
    int result = lhs + rhs;
    return result;
}

Lo anterior parece bastante sencillo. Definimos una función llamada add()y pasamos en los sumandos izquierdo y derecho. La función los agrega y devuelve un resultado. Ignore todas las cosas del caso de borde, como desbordamientos que puedan ocurrir, en este punto no es pertinente para la discusión.

El add()propósito de la función parece bastante sencillo, pero ¿qué podemos decir sobre su ciclo de vida? ¿Especialmente sus necesidades de utilización de memoria?

Lo más importante es que el compilador sabe a priori (es decir, en tiempo de compilación) qué tan grandes son los tipos de datos y cuántos se utilizarán. Los argumentos lhsy rhsson de sizeof(int)4 bytes cada uno. La variable resultes también sizeof(int). El compilador puede decir que la add()función usa 4 bytes * 3 intso un total de 12 bytes de memoria.

Cuando add()se llama a la función, un registro de hardware llamado puntero de la pila tendrá una dirección que apunte a la parte superior de la pila. Para asignar la memoria add()que necesita ejecutar la función, todo el código de entrada de función debe emitir una sola instrucción de lenguaje ensamblador para disminuir el valor del registro del puntero de la pila en 12. Al hacerlo, crea almacenamiento en la pila para tres ints, uno para cada uno lhs, rhsy result. Obtener el espacio de memoria que necesita ejecutando una sola instrucción es una ganancia enorme en términos de velocidad porque las instrucciones individuales tienden a ejecutarse en un tic de reloj (mil millonésimas de segundo una CPU de 1 GHz).

Además, desde la vista del compilador, puede crear un mapa de las variables que se parece mucho a la indexación de una matriz:

lhs:     ((int *)stack_pointer_register)[0]
rhs:     ((int *)stack_pointer_register)[1]
result:  ((int *)stack_pointer_register)[2]

Nuevamente, todo esto es muy rápido.

Cuando la add()función sale, tiene que limpiarse. Lo hace restando 12 bytes del registro del puntero de la pila. Es similar a una llamada a, free()pero solo usa una instrucción de CPU y solo toma un tic. Es muy, muy rápido.


Ahora considere una asignación basada en el montón. Esto entra en juego cuando no sabemos a priori cuánta memoria necesitaremos (es decir, solo lo aprenderemos en tiempo de ejecución).

Considere esta función:

int addRandom(int count) {
    int numberOfBytesToAllocate = sizeof(int) * count;
    int *array = malloc(numberOfBytesToAllocate);
    int result = 0;

    if array != NULL {
        for (i = 0; i < count; ++i) {
            array[i] = (int) random();
            result += array[i];
        }

        free(array);
    }

    return result;
}

Observe que la addRandom()función no sabe en tiempo de compilación cuál countserá el valor del argumento. Debido a esto, no tiene sentido tratar de definir arraycomo lo haríamos si lo pusiéramos en la pila, así:

int array[count];

Si countes enorme, podría hacer que nuestra pila crezca demasiado y sobrescriba otros segmentos del programa. Cuando ocurre este desbordamiento de pila, su programa se bloquea (o peor).

Entonces, en casos donde no sabemos cuánta memoria necesitaremos hasta el tiempo de ejecución, usamos malloc(). Luego, podemos pedir la cantidad de bytes que necesitamos cuando la necesitemos, e malloc()iremos a verificar si puede vender esa cantidad de bytes. Si puede, genial, lo recuperamos, si no, obtenemos un puntero NULL que nos dice que la llamada malloc()falló. Notablemente, sin embargo, ¡el programa no se bloquea! Por supuesto, usted como programador puede decidir que su programa no puede ejecutarse si falla la asignación de recursos, pero la terminación iniciada por el programador es diferente a un bloqueo espurio.

Así que ahora tenemos que volver para analizar la eficiencia. El asignador de pila es súper rápido: una instrucción para asignar, una instrucción para desasignar, y la realiza el compilador, pero recuerde que la pila está destinada a cosas como variables locales de un tamaño conocido, por lo que tiende a ser bastante pequeña.

El asignador de almacenamiento dinámico, por otro lado, es varios órdenes de magnitud más lento. Tiene que hacer una búsqueda en tablas para ver si tiene suficiente memoria libre para poder vender la cantidad de memoria que desea el usuario. Tiene que actualizar esas tablas después de vender la memoria para asegurarse de que nadie más pueda usar ese bloque (esta contabilidad puede requerir que el asignador reserve memoria para sí mismo además de lo que planea vender). El asignador tiene que emplear estrategias de bloqueo para asegurarse de que distribuye la memoria de manera segura. Y cuando la memoria finalmente esfree()d, que ocurre en diferentes momentos y generalmente sin un orden predecible, el asignador tiene que encontrar bloques contiguos y volver a unirlos para reparar la fragmentación del montón. Si eso parece que se necesitará más de una instrucción de CPU para lograr todo eso, ¡tienes razón! Es muy complicado y lleva un tiempo.

Pero los montones son grandes. Mucho más grande que las pilas. Podemos obtener mucha memoria de ellos y son geniales cuando no sabemos en tiempo de compilación cuánta memoria necesitaremos. Por lo tanto, cambiamos la velocidad por un sistema de memoria administrado que nos rechaza cortésmente en lugar de fallar cuando intentamos asignar algo demasiado grande.

Espero que eso ayude a responder algunas de sus preguntas. Avíseme si desea una aclaración sobre cualquiera de los anteriores.

par
fuente
intno es de 8 bytes en una plataforma de 64 bits. Todavía es 4. Junto con eso, es muy probable que el compilador optimice el tercero intfuera de la pila en el registro de retorno. De hecho, es probable que los dos argumentos también estén en registros en cualquier plataforma de 64 bits.
SS Anne
He editado mi respuesta para eliminar la declaración sobre los 8 bytes inten las plataformas de 64 bits. Tienes razón que intsigue siendo de 4 bytes en Java. Sin embargo, he dejado el resto de mi respuesta porque creo que entrar en la optimización del compilador pone el carro delante del caballo. Sí, también tiene razón en estos puntos, pero la pregunta solicita aclaraciones sobre las pilas frente a los montones. RVO, la discusión que pasa a través de registros, código de elisión, etc. sobrecarga los conceptos básicos y se interpone en el camino de la comprensión de los fundamentos.
par