• Preguntas de la entrevista del sistema de tipo Java
• Preguntas de la entrevista de simultaneidad de Java (+ respuestas)
• Preguntas de la entrevista de inicialización y estructura de clases de Java
• Preguntas de la entrevista de Java 8 (+ respuestas)
• Preguntas de la entrevista sobre gestión de la memoria en Java (+ respuestas) (artículo actual) • Preguntas de la entrevista sobre Java genéricos (+ respuestas)
• Preguntas de la entrevista de control de flujo de Java (+ respuestas)
• Preguntas de la entrevista sobre excepciones de Java (+ respuestas)
• Preguntas de la entrevista de anotaciones de Java (+ respuestas)
• Preguntas principales de la entrevista del Spring Framework
1. Introducción
En este artículo, exploraremos algunas preguntas sobre administración de memoria que surgen con frecuencia durante las entrevistas con desarrolladores de Java. La gestión de la memoria es un área con la que no están familiarizados muchos desarrolladores.
De hecho, los desarrolladores generalmente no tienen que lidiar con este concepto directamente, ya que la JVM se encarga de los detalles esenciales. A menos que algo vaya muy mal, incluso los desarrolladores experimentados pueden no tener al alcance de la mano información precisa sobre la gestión de la memoria.
Por otro lado, estos conceptos son bastante frecuentes en las entrevistas, así que entremos de inmediato.
2. Preguntas
Q1. ¿Qué significa la afirmación "La memoria se gestiona en Java"?
La memoria es el recurso clave que una aplicación requiere para funcionar de manera eficaz y, como cualquier recurso, es escasa. Como tal, su asignación y desasignación hacia y desde aplicaciones o diferentes partes de una aplicación requieren mucho cuidado y consideración.
Sin embargo, en Java, un desarrollador no necesita asignar y desasignar memoria explícitamente - la JVM y más específicamente el Garbage Collector - tiene el deber de manejar la asignación de memoria para que el desarrollador no tenga que hacerlo.
Esto es contrario a lo que sucede en lenguajes como C, donde un programador tiene acceso directo a la memoria y literalmente hace referencia a celdas de memoria en su código, creando mucho espacio para pérdidas de memoria.
Q2. ¿Qué es la recolección de basura y cuáles son sus ventajas?
La recolección de basura es el proceso de mirar la memoria del montón, identificar qué objetos están en uso y cuáles no, y eliminar los objetos no usados.
Un objeto en uso, o un objeto referenciado, significa que alguna parte de su programa aún mantiene un puntero a ese objeto. Un objeto no utilizado, o un objeto sin referencia, ya no es referenciado por ninguna parte de su programa. Por lo tanto, la memoria utilizada por un objeto sin referencia se puede recuperar.
La mayor ventaja de la recolección de basura es que nos quita la carga de la asignación / desasignación manual de memoria para que podamos concentrarnos en resolver el problema en cuestión.
Q3. ¿Existe alguna desventaja de la recolección de basura?
Si. Siempre que se ejecuta el recolector de basura, tiene un efecto en el rendimiento de la aplicación. Esto se debe a que todos los demás subprocesos de la aplicación deben detenerse para permitir que el subproceso del recolector de basura haga su trabajo de manera efectiva.
Dependiendo de los requisitos de la aplicación, esto puede ser un problema real inaceptable por el cliente. Sin embargo, este problema puede reducirse en gran medida o incluso eliminarse mediante una hábil optimización y ajuste del recolector de basura y el uso de diferentes algoritmos de GC.
Q4. ¿Cuál es el significado del término "Stop-The-World"?
Cuando el subproceso del recolector de basura se está ejecutando, otros subprocesos se detienen, lo que significa que la aplicación se detiene momentáneamente. Esto es análogo a la limpieza o fumigación de viviendas, donde se niega el acceso a los ocupantes hasta que se completa el proceso.
Dependiendo de las necesidades de una aplicación, la recolección de basura “stop the world” puede causar una congelación inaceptable. Por eso es importante ajustar el recolector de basura y optimizar la JVM para que la congelación encontrada sea al menos aceptable.
Q5. ¿Qué son Stack and Heap? ¿Qué se almacena en cada una de estas estructuras de memoria y cómo están interrelacionadas?
La pila es una parte de la memoria que contiene información sobre llamadas a métodos anidados hasta la posición actual en el programa. También contiene todas las variables locales y referencias a objetos en el montón definido en los métodos que se están ejecutando actualmente.
Esta estructura permite que el tiempo de ejecución regrese del método sabiendo la dirección de donde fue llamado, y también borre todas las variables locales después de salir del método. Cada hilo tiene su propia pila.
El montón es una gran cantidad de memoria destinada a la asignación de objetos. Cuando crea un objeto con la nueva palabra clave, se asigna en el montón. Sin embargo, la referencia a este objeto vive en la pila.
Q6. ¿Qué es la recolección de basura generacional y qué la convierte en un enfoque popular de recolección de basura?
La recolección de basura generacional se puede definir vagamente como la estrategia utilizada por el recolector de basura donde el montón se divide en varias secciones llamadas generaciones, cada una de las cuales contendrá objetos de acuerdo con su "edad" en el montón.
Siempre que se esté ejecutando el recolector de basura, el primer paso del proceso se denomina marcado. Aquí es donde el recolector de basura identifica qué piezas de memoria están en uso y cuáles no. Este puede ser un proceso que requiere mucho tiempo si se deben analizar todos los objetos de un sistema.
A medida que se asignan más y más objetos, la lista de objetos crece y crece, lo que lleva a un tiempo de recolección de basura cada vez más largo. Sin embargo, el análisis empírico de aplicaciones ha demostrado que la mayoría de los objetos tienen una vida corta.
Con la recolección de basura generacional, los objetos se agrupan según su "edad" en términos de cuántos ciclos de recolección de basura han sobrevivido. De esta manera, la mayor parte del trabajo se repartió entre varios ciclos de cobranza menores y mayores.
Hoy, casi todos los recolectores de basura son generacionales. Esta estrategia es tan popular porque, con el tiempo, ha demostrado ser la solución óptima.
Q7. Describir en detalle cómo funciona la recolección de basura generacional
Para comprender correctamente cómo funciona la recolección de basura generacional, es importante recordar primero cómo está estructurado el montón de Java para facilitar la recolección de basura generacional.
El montón se divide en espacios o generaciones más pequeños. Estos espacios son Generación Joven, Generación Antigua o Titular y Generación Permanente.
La generación joven alberga la mayoría de los objetos recién creados . Un estudio empírico de la mayoría de las aplicaciones muestra que la mayoría de los objetos tienen una vida corta rápidamente y, por lo tanto, pronto se vuelven elegibles para la colección. Por lo tanto, los nuevos objetos comienzan su viaje aquí y solo son "promovidos" al espacio de la vieja generación después de haber alcanzado una cierta "edad".
El término "edad" en la recolección de basura generacional se refiere al número de ciclos de recolección que el objeto ha sobrevivido .
El espacio de la generación joven se divide a su vez en tres espacios: un espacio Edén y dos espacios de supervivientes como Superviviente 1 (s1) y Superviviente 2 (s2).
La vieja generación alberga objetos que han vivido en la memoria más de una determinada "edad" . Los objetos que sobrevivieron a la recolección de basura de la generación joven son promocionados a este espacio. Generalmente es más grande que la generación joven. Como es más grande, la recolección de basura es más cara y ocurre con menos frecuencia que en la generación joven.
La generación permanente o más comúnmente llamada PermGen, contiene metadatos requeridos por la JVM para describir las clases y métodos usados en la aplicación. También contiene el grupo de cuerdas para almacenar cuerdas internas. Lo llena la JVM en tiempo de ejecución en función de las clases que utiliza la aplicación. Además, las clases y métodos de la biblioteca de la plataforma se pueden almacenar aquí.
Primero, todos los objetos nuevos se asignan al espacio Edén . Ambos espacios de supervivientes comienzan vacíos. Cuando el espacio de Eden se llena, se activa una recolección de basura menor. Los objetos referenciados se mueven al primer espacio de superviviente. Los objetos no referenciados se eliminan.
Durante el siguiente GC menor, lo mismo sucede con el espacio Edén. Los objetos no referenciados se eliminan y los objetos referenciados se mueven a un espacio de superviviente. Sin embargo, en este caso, se mueven al segundo espacio de superviviente (S2).
Además, la edad de los objetos del último GC menor en el primer espacio de superviviente (S1) aumenta y se mueve a S2. Una vez que todos los objetos supervivientes se han movido a S2, tanto el espacio S1 como el Edén se despejan. En este punto, S2 contiene objetos con diferentes edades.
En el siguiente GC menor, se repite el mismo proceso. Sin embargo, esta vez los espacios de supervivientes cambian. Los objetos referenciados se mueven a S1 desde Eden y S2. Los objetos supervivientes están envejecidos. Eden y S2 se borran.
Después de cada ciclo de recolección de basura menor, se verifica la antigüedad de cada objeto. Aquellos que han alcanzado cierta edad arbitraria, por ejemplo, 8 años, son promovidos de la generación joven a la generación mayor o titular. Para todos los ciclos de GC menores posteriores, los objetos continuarán promoviéndose al espacio de la generación anterior.
Esto prácticamente agota el proceso de recolección de basura en la generación joven. Eventualmente, se llevará a cabo una importante recolección de basura en la generación anterior que limpia y compacta ese espacio. Para cada GC principal, hay varios GC menores.
Q8. ¿Cuándo se convierte un objeto en elegible para la recolección de basura? Describe cómo Gc recolecta un objeto elegible.
Un objeto se vuelve elegible para la recolección de basura o GC si no es accesible desde ningún subproceso en vivo o por referencias estáticas.
El caso más sencillo de que un objeto sea elegible para la recolección de basura es si todas sus referencias son nulas. Las dependencias cíclicas sin ninguna referencia externa activa también son elegibles para GC. Entonces, si el objeto A hace referencia al objeto B y el objeto B hace referencia al Objeto A y no tienen ninguna otra referencia en vivo, entonces ambos Objetos A y B serán elegibles para la recolección de basura.
Otro caso obvio es cuando un objeto padre se establece en nulo. Cuando un objeto de cocina hace referencia internamente a un objeto de frigorífico y un objeto de fregadero, y el objeto de cocina se establece en nulo, tanto el frigorífico como el fregadero serán elegibles para la recolección de basura junto con su cocina principal.
Q9. ¿Cómo se activa la recolección de basura desde el código Java?
Usted, como programador de Java, no puede forzar la recolección de basura en Java ; solo se activará si JVM cree que necesita una recolección de basura basada en el tamaño del montón de Java.
Antes de eliminar un objeto de la memoria, el hilo de recolección de basura invoca el método finalize () de ese objeto y brinda la oportunidad de realizar cualquier tipo de limpieza necesaria. También puede invocar este método de un código de objeto, sin embargo, no hay garantía de que la recolección de basura ocurra cuando llame a este método.
Además, existen métodos como System.gc () y Runtime.gc () que se utilizan para enviar solicitudes de recolección de basura a JVM, pero no se garantiza que suceda.
Q10. ¿Qué sucede cuando no hay suficiente espacio de almacenamiento para almacenar nuevos objetos?
Si no hay espacio de memoria para crear un nuevo objeto en Heap, Java Virtual Machine arroja OutOfMemoryError o más específicamente java.lang.OutOfMemoryError espacio de pila.
Q11. ¿Es posible "resucitar" un objeto que se volvió elegible para la recolección de basura?
Cuando un objeto se vuelve elegible para la recolección de basura, el GC debe ejecutar el método finalize en él. Se garantiza que el método de finalización se ejecutará solo una vez, por lo que el GC marca el objeto como finalizado y le da un descanso hasta el siguiente ciclo.
En el método de finalización , técnicamente puede "resucitar" un objeto, por ejemplo, asignándolo a un campo estático . El objeto volvería a estar vivo y no sería apto para la recolección de basura, por lo que el GC no lo recolectaría durante el siguiente ciclo.
Sin embargo, el objeto se marcará como finalizado, por lo que cuando vuelva a ser elegible, no se llamará al método finalize. En esencia, puede activar este truco de "resurrección" solo una vez durante la vida útil del objeto. Tenga en cuenta que este truco feo debe usarse solo si realmente sabe lo que está haciendo; sin embargo, comprender este truco le da una idea de cómo funciona el GC.
Q12. Describa las referencias fuertes, débiles, suaves y fantasmas y su papel en la recolección de basura.
Por mucho que la memoria se gestione en Java, es posible que un ingeniero deba realizar la mayor optimización posible para minimizar la latencia y maximizar el rendimiento en aplicaciones críticas. Por mucho que sea imposible controlar explícitamente cuándo se activa la recolección de basura en la JVM, es posible influir en cómo ocurre con respecto a los objetos que hemos creado.
Java nos proporciona objetos de referencia para controlar la relación entre los objetos que creamos y el recolector de basura.
De forma predeterminada, cada objeto que creamos en un programa Java está fuertemente referenciado por una variable:
StringBuilder sb = new StringBuilder();
En el fragmento anterior, la nueva palabra clave crea un nuevo objeto StringBuilder y lo almacena en el montón. La variable sb luego almacena una fuerte referencia a este objeto. Lo que esto significa para el recolector de basura es que el objeto StringBuilder en particular no es elegible para la recolección en absoluto debido a una fuerte referencia que tiene sb . La historia solo cambia cuando anulamos un sb así:
sb = null;
Después de llamar a la línea anterior, el objeto será elegible para la recolección.
Podemos cambiar esta relación entre el objeto y el recolector de basura envolviéndolo explícitamente dentro de otro objeto de referencia que se encuentra dentro del paquete java.lang.ref .
Se puede crear una referencia suave al objeto anterior como este:
StringBuilder sb = new StringBuilder(); SoftReference sbRef = new SoftReference(sb); sb = null;
En el fragmento anterior, hemos creado dos referencias al objeto StringBuilder . La primera línea crea una referencia fuerte sb y la segunda línea crea una referencia suave sbRef . La tercera línea debería hacer que el objeto sea elegible para la recolección, pero el recolector de basura pospondrá la recolección debido a sbRef .
La historia solo cambiará cuando la memoria se vuelva apretada y la JVM esté a punto de arrojar un error OutOfMemory . En otras palabras, los objetos con solo referencias suaves se recopilan como último recurso para recuperar la memoria.
Se puede crear una referencia débil de manera similar usando la clase WeakReference . Cuando sb se establece en nulo y el objeto StringBuilder solo tiene una referencia débil, el recolector de basura de la JVM no tendrá ningún compromiso e inmediatamente recolectará el objeto en el siguiente ciclo.
Una referencia fantasma es similar a una referencia débil y un objeto con solo referencias fantasmas se recopilará sin esperar. Sin embargo, las referencias fantasmas se ponen en cola tan pronto como se recopilan sus objetos. Podemos sondear la cola de referencia para saber exactamente cuándo se recopiló el objeto.
Q13. Supongamos que tenemos una referencia circular (dos objetos que se referencian entre sí). ¿Podría este par de objetos ser elegible para la recolección de basura y por qué?
Sí, un par de objetos con una referencia circular pueden ser elegibles para la recolección de basura. Esto se debe a cómo el recolector de basura de Java maneja las referencias circulares. Considera que los objetos están en vivo no cuando tienen alguna referencia a ellos, sino cuando se puede acceder a ellos navegando por el gráfico de objetos a partir de alguna raíz de recolección de basura (una variable local de un hilo vivo o un campo estático). Si un par de objetos con una referencia circular no es accesible desde ninguna raíz, se considera elegible para la recolección de basura.
Q14. ¿Cómo se representan las cadenas en la memoria?
Una instancia de String en Java es un objeto con dos campos: un campo de valor char [] y un campo hash int . El campo de valor es una matriz de caracteres que representan la cadena en sí, y el campo hash contiene el código hash de una cadena que se inicializa con cero, se calcula durante la primera llamada a hashCode () y se almacena en caché desde entonces. Como caso curioso, si un hashCode de una cadena tiene un valor cero, debe recalcularse cada vez que se llama al hashCode () .
Lo importante es que una instancia de String es inmutable: no se puede obtener ni modificar la matriz char [] subyacente . Otra característica de las cadenas es que las cadenas de constantes estáticas se cargan y almacenan en caché en un grupo de cadenas. Si tiene varios objetos String idénticos en su código fuente, todos están representados por una sola instancia en tiempo de ejecución.
Q15. ¿Qué es un Stringbuilder y cuáles son sus casos de uso? ¿Cuál es la diferencia entre agregar una cadena a un generador de cadenas y concatenar dos cadenas con un operador +? ¿En qué se diferencia Stringbuilder de Stringbuffer?
StringBuilder permite manipular secuencias de caracteres agregando, eliminando e insertando caracteres y cadenas. Esta es una estructura de datos mutable, a diferencia de la clase String que es inmutable.
Al concatenar dos instancias de String , se crea un nuevo objeto y se copian las cadenas. Esto podría traer una enorme sobrecarga del recolector de basura si necesitamos crear o modificar una cadena en un bucle. StringBuilder permite manejar manipulaciones de cadenas de manera mucho más eficiente.
StringBuffer es diferente de StringBuilder en que es seguro para subprocesos. Si necesita manipular una cadena en un solo hilo, use StringBuilder en su lugar.
3. Conclusión
En este artículo, hemos cubierto algunas de las preguntas más comunes que aparecen con frecuencia en las entrevistas de ingenieros de Java. Las preguntas sobre la administración de la memoria se hacen principalmente para los candidatos a Desarrollador Java Senior, ya que el entrevistador espera que haya creado aplicaciones no triviales que, muchas veces, están plagadas de problemas de memoria.
Esto no debe tratarse como una lista exhaustiva de preguntas, sino más bien como una plataforma de lanzamiento para futuras investigaciones. En Baeldung, le deseamos éxito en las próximas entrevistas.
Siguiente » Preguntas de la entrevista de Java Generics (+ Respuestas) « Preguntas anteriores de la entrevista de Java 8 (+ Respuestas)