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?
scalar-replacement
) en campos simples que viven solo en la pila; pero eso es algo queJIT
hace, nojavac
.Respuestas:
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
new
hacen una asignación basada en el montón comomalloc()
, 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). Cuandonew
regrese, tiene la garantía de tener un objeto inicializado.Pero sí, al final de la llamada, tanto el resultado
malloc()
comonew
simplemente 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 ...
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
lhs
yrhs
son desizeof(int)
4 bytes cada uno. La variableresult
es tambiénsizeof(int)
. El compilador puede decir que laadd()
función usa4 bytes * 3 ints
o 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 memoriaadd()
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 tresints
, uno para cada unolhs
,rhs
yresult
. 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:
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:
Observe que la
addRandom()
función no sabe en tiempo de compilación cuálcount
será el valor del argumento. Debido a esto, no tiene sentido tratar de definirarray
como lo haríamos si lo pusiéramos en la pila, así:Si
count
es 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, emalloc()
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 llamadamalloc()
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 es
free()
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.
fuente
int
no 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 terceroint
fuera 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.int
en las plataformas de 64 bits. Tienes razón queint
sigue 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.