Questions d'entretien sur la gestion de la mémoire en Java (+ réponses)

Cet article fait partie d'une série: • Questions d'entretien sur les collections Java

• Questions d’entretien sur Java Type System

• Questions d’entretien sur Java Concurrency (+ réponses)

• Questions d'entretien sur la structure des classes Java et l'initialisation

• Questions d'entrevue Java 8 (+ réponses)

• Questions d'entretien sur la gestion de la mémoire dans Java (+ réponses) (article actuel) • Questions d'entretien sur Java Generics (+ réponses)

• Questions d’entretien sur le contrôle de flux Java (+ réponses)

• Questions d’entretien sur les exceptions Java (+ réponses)

• Questions d'entrevue sur les annotations Java (+ réponses)

• Principales questions d'entrevue Spring Framework

1. Introduction

Dans cet article, nous explorerons certaines questions de gestion de la mémoire qui apparaissent fréquemment lors des entretiens avec les développeurs Java. La gestion de la mémoire est un domaine que peu de développeurs connaissent.

En fait, les développeurs n'ont généralement pas à gérer ce concept directement - car la JVM s'occupe des détails les plus fins. À moins que quelque chose ne se passe vraiment mal, même les développeurs chevronnés peuvent ne pas disposer d'informations précises sur la gestion de la mémoire.

D'un autre côté, ces concepts sont en fait assez répandus dans les entretiens - alors allons-y directement.

2. Questions

Q1. Que signifie la déclaration «La mémoire est gérée en Java»?

La mémoire est la ressource clé dont une application a besoin pour fonctionner efficacement et, comme toute ressource, elle est rare. En tant que tel, son allocation et sa désallocation vers et à partir d'applications ou de différentes parties d'une application nécessitent beaucoup de soin et de considération.

Cependant, en Java, un développeur n'a pas besoin d'allouer et de désallouer explicitement la mémoire - la JVM et plus précisément le Garbage Collector - a le devoir de gérer l'allocation de mémoire afin que le développeur n'ait pas à le faire.

Ceci est contraire à ce qui se passe dans des langages comme C où un programmeur a un accès direct à la mémoire et référence littéralement les cellules mémoire dans son code, créant ainsi beaucoup de place pour les fuites de mémoire.

Q2. Qu'est-ce que la collecte des ordures et quels sont ses avantages?

Le garbage collection est le processus qui consiste à examiner la mémoire du tas, à identifier les objets utilisés et ceux qui ne le sont pas, et à supprimer les objets inutilisés.

Un objet en cours d'utilisation ou un objet référencé signifie qu'une partie de votre programme conserve toujours un pointeur vers cet objet. Un objet inutilisé, ou un objet non référencé, n'est plus référencé par aucune partie de votre programme. Ainsi, la mémoire utilisée par un objet non référencé peut être récupérée.

Le plus grand avantage du garbage collection est qu'il supprime le fardeau de l'allocation / désallocation manuelle de la mémoire afin que nous puissions nous concentrer sur la résolution du problème.

Q3. Y a-t-il des inconvénients du nettoyage de la mémoire?

Oui. Chaque fois que le garbage collector s'exécute, il a un effet sur les performances de l'application. Cela est dû au fait que tous les autres threads de l'application doivent être arrêtés pour permettre au thread du garbage collector d'effectuer efficacement son travail.

En fonction des exigences de l'application, cela peut être un réel problème inacceptable par le client. Cependant, ce problème peut être considérablement réduit ou même éliminé grâce à une optimisation et à un réglage habiles du ramasse-miettes et à l'utilisation de différents algorithmes GC.

Q4. Quelle est la signification du terme «Stop-The-World»?

Lorsque le thread du garbage collector est en cours d'exécution, les autres threads sont arrêtés, ce qui signifie que l'application est arrêtée momentanément. Ceci est analogue au nettoyage ou à la fumigation de la maison où les occupants se voient refuser l'accès jusqu'à ce que le processus soit terminé.

Selon les besoins d'une application, le garbage collection «stop the world» peut provoquer un gel inacceptable. C'est pourquoi il est important de procéder au réglage du garbage collector et à l'optimisation de la JVM afin que le gel rencontré soit au moins acceptable.

Q5. Que sont la pile et le tas? Qu'est-ce qui est stocké dans chacune de ces structures de mémoire et comment sont-elles interdépendantes?

La pile est une partie de la mémoire qui contient des informations sur les appels de méthode imbriqués jusqu'à la position actuelle dans le programme. Il contient également toutes les variables locales et les références aux objets du tas définis dans les méthodes en cours d'exécution.

Cette structure permet au runtime de revenir de la méthode en connaissant l'adresse à laquelle il a été appelé, et également d'effacer toutes les variables locales après avoir quitté la méthode. Chaque thread a sa propre pile.

Le tas est une grande quantité de mémoire destinée à l'allocation d'objets. Lorsque vous créez un objet avec le nouveau mot-clé, il est alloué sur le tas. Cependant, la référence à cet objet vit sur la pile.

Q6. Qu'est-ce que le ramassage des ordures générationnel et qu'est-ce qui en fait une approche de ramassage des ordures populaire?

Le garbage collection générationnel peut être défini de manière approximative comme la stratégie utilisée par le garbage collector où le tas est divisé en un certain nombre de sections appelées générations, dont chacune contiendra des objets en fonction de leur «âge» sur le tas.

Chaque fois que le garbage collector est en cours d'exécution, la première étape du processus est appelée marquage. C'est là que le garbage collector identifie les éléments de mémoire utilisés et ceux qui ne le sont pas. Cela peut être un processus très long si tous les objets d'un système doivent être analysés.

Au fur et à mesure que de plus en plus d'objets sont alloués, la liste des objets augmente et augmente, ce qui entraîne un temps de nettoyage de la mémoire de plus en plus long. Cependant, l'analyse empirique des applications a montré que la plupart des objets sont de courte durée.

Avec le garbage collection générationnel, les objets sont regroupés en fonction de leur «âge» en fonction du nombre de cycles de garbage collection auxquels ils ont survécu. De cette façon, l'essentiel du travail s'est réparti sur divers cycles de collecte mineurs et majeurs.

Aujourd'hui, presque tous les éboueurs sont générationnels. Cette stratégie est si populaire car, au fil du temps, elle s'est avérée être la solution optimale.

Q7. Décrire en détail le fonctionnement de la récupération de place générationnelle

Pour bien comprendre le fonctionnement du garbage collection générationnel, il est important de se rappeler d' abord comment le tas Java est structuré pour faciliter le garbage collection générationnel.

Le tas est divisé en espaces ou générations plus petits. Ces espaces sont la jeune génération, la génération ancienne ou permanente et la génération permanente.

La jeune génération héberge la plupart des objets nouvellement créés . Une étude empirique de la plupart des applications montre que la majorité des objets sont rapidement de courte durée et deviennent donc rapidement éligibles à la collection. Par conséquent, les nouveaux objets commencent leur voyage ici et ne sont «promus» dans l'espace de l'ancienne génération qu'après avoir atteint un certain «âge».

Le terme «âge» dans le garbage collection générationnel fait référence au nombre de cycles de collecte auxquels l'objet a survécu .

L'espace des jeunes générations est divisé en trois espaces: un espace Eden et deux espaces survivants tels que Survivor 1 (s1) et Survivor 2 (s2).

L' ancienne génération héberge des objets qui ont vécu en mémoire plus longtemps qu'un certain «âge» . Les objets qui ont survécu au ramassage des ordures de la jeune génération sont promus dans cet espace. Il est généralement plus grand que la jeune génération. Comme il est de plus grande taille, le ramassage des ordures est plus coûteux et se produit moins fréquemment que dans la jeune génération.

La génération permanente ou plus communément appelée PermGen contient les métadonnées requises par la JVM pour décrire les classes et les méthodes utilisées dans l'application. Il contient également le pool de chaînes pour stocker les chaînes internes. Il est rempli par la JVM au moment de l'exécution en fonction des classes utilisées par l'application. De plus, les classes et méthodes de la bibliothèque de plate-forme peuvent être stockées ici.

Tout d'abord, tous les nouveaux objets sont attribués à l'espace Eden . Les deux cases de survivant commencent vides. Lorsque l'espace Eden se remplit, un garbage collection mineur est déclenché. Les objets référencés sont déplacés vers le premier espace de survivant. Les objets non référencés sont supprimés.

Lors du prochain GC mineur, la même chose arrive à l'espace Eden. Les objets non référencés sont supprimés et les objets référencés sont déplacés vers un espace survivant. Cependant, dans ce cas, ils sont déplacés vers la deuxième case de survivant (S2).

De plus, les objets du dernier GC mineur dans la première case de survivant (S1) voient leur âge incrémenté et sont déplacés vers S2. Une fois que tous les objets survivants ont été déplacés vers S2, les espaces S1 et Eden sont effacés. À ce stade, S2 contient des objets d'âges différents.

Au prochain GC mineur, le même processus est répété. Cependant, cette fois, les espaces des survivants changent. Les objets référencés sont déplacés vers S1 depuis Eden et S2. Les objets survivants sont vieillis. Eden et S2 sont effacés.

Après chaque cycle de garbage collection mineur, l'âge de chaque objet est vérifié. Ceux qui ont atteint un certain âge arbitraire, par exemple 8 ans, sont promus de la jeune génération à la génération ancienne ou titulaire. Pour tous les cycles GC mineurs ultérieurs, les objets continueront à être promus vers l'espace de l'ancienne génération.

Cela épuise à peu près le processus de collecte des ordures dans la jeune génération. Finalement, un important garbage collection sera effectué sur l'ancienne génération qui nettoie et compacte cet espace. Pour chaque GC majeur, il existe plusieurs GC mineurs.

Q8. Quand un objet devient-il éligible pour le nettoyage de la mémoire? Décrivez comment le GC collecte un objet éligible?

Un objet devient éligible pour le garbage collection ou GC s'il n'est pas accessible à partir de threads en direct ou par des références statiques.

Le cas le plus simple d'un objet devenant éligible pour le garbage collection est si toutes ses références sont nulles. Les dépendances cycliques sans aucune référence externe en direct sont également éligibles pour GC. Ainsi, si l'objet A fait référence à l'objet B et que l'objet B fait référence à l'objet A et qu'ils n'ont aucune autre référence en direct, les objets A et B seront éligibles pour le nettoyage de la mémoire.

Un autre cas évident est lorsqu'un objet parent est défini sur null. Lorsqu'un objet de cuisine fait référence en interne à un objet de réfrigérateur et à un objet d'évier et que l'objet de cuisine est défini sur null, le réfrigérateur et l'évier deviendront éligibles à la collecte des ordures aux côtés de leur parent, la cuisine.

Q9. Comment déclencher le nettoyage de la mémoire à partir du code Java?

En tant que programmeur Java, vous ne pouvez pas forcer le garbage collection en Java ; il ne se déclenchera que si JVM pense qu'il a besoin d'un garbage collection basé sur la taille du tas Java.

Avant de supprimer un objet de la mémoire, le thread de garbage collection appelle la méthode finalize () de cet objet et donne l'opportunité d'effectuer tout type de nettoyage requis. Vous pouvez également appeler cette méthode d'un code objet, cependant, il n'y a aucune garantie que le garbage collection se produira lorsque vous appelez cette méthode.

De plus, il existe des méthodes comme System.gc () et Runtime.gc () qui sont utilisées pour envoyer une demande de récupération de place à JVM, mais il n'est pas garanti que la récupération de place se produira.

Q10. Que se passe-t-il lorsqu'il n'y a pas assez d'espace de tas pour accueillir le stockage de nouveaux objets?

S'il n'y a pas d'espace mémoire pour créer un nouvel objet dans le tas, la machine virtuelle Java lance OutOfMemoryError ou plus spécifiquement l' espace de tas java.lang.OutOfMemoryError .

Q11. Est-il possible de «ressusciter» un objet qui est devenu éligible pour le garbage collection?

Lorsqu'un objet devient éligible pour le garbage collection, le GC doit y exécuter la méthode finalize . La méthode de finalisation est garantie de ne s'exécuter qu'une seule fois, ainsi le GC marque l'objet comme finalisé et lui donne un repos jusqu'au cycle suivant.

Dans la méthode finalize , vous pouvez techniquement «ressusciter» un objet, par exemple, en l'affectant à un champ statique . L'objet redeviendrait vivant et non éligible pour le garbage collection, de sorte que le GC ne le collecterait pas au cours du cycle suivant.

L'objet, cependant, serait marqué comme finalisé, donc lorsqu'il redeviendrait éligible, la méthode finaliser ne serait pas appelée. En substance, vous ne pouvez activer cette astuce de «résurrection» qu'une seule fois pendant la durée de vie de l'objet. Sachez que cet horrible hack ne devrait être utilisé que si vous savez vraiment ce que vous faites - cependant, comprendre cette astuce donne un aperçu du fonctionnement du GC.

Q12. Décrivez les références fortes, faibles, souples et fantômes et leur rôle dans la collecte des ordures.

Tout comme la mémoire est gérée en Java, un ingénieur peut devoir effectuer autant d'optimisation que possible pour minimiser la latence et maximiser le débit, dans les applications critiques. Bien qu'il soit impossible de contrôler explicitement le moment où le garbage collection est déclenché dans la JVM, il est possible d'influencer son déroulement en ce qui concerne les objets que nous avons créés.

Java nous fournit des objets de référence pour contrôler la relation entre les objets que nous créons et le garbage collector.

Par défaut, chaque objet que nous créons dans un programme Java est fortement référencé par une variable:

StringBuilder sb = new StringBuilder();

Dans l'extrait de code ci-dessus, le nouveau mot-clé crée un nouvel objet StringBuilder et le stocke sur le tas. La variable sb stocke alors une référence forte à cet objet. Ce que cela signifie pour le garbage collector est que l' objet StringBuilder particulier n'est pas du tout éligible pour la collecte en raison d'une référence forte qui lui est détenue par sb . L'histoire ne change que lorsque nous annulons sb comme ceci:

sb = null;

Après avoir appelé la ligne ci-dessus, l'objet sera alors éligible pour la collecte.

Nous pouvons modifier cette relation entre l'objet et le ramasse-miettes en l'enveloppant explicitement dans un autre objet de référence situé dans le package java.lang.ref .

Une référence souple peut être créée à l'objet ci-dessus comme ceci:

StringBuilder sb = new StringBuilder(); SoftReference sbRef = new SoftReference(sb); sb = null;

Dans l'extrait de code ci-dessus, nous avons créé deux références à l' objet StringBuilder . La première ligne crée une référence forte sb et la seconde ligne crée une référence souple sbRef . La troisième ligne devrait rendre l'objet éligible pour la collecte, mais le garbage collector reportera sa collecte à cause de sbRef .

L'histoire ne changera que lorsque la mémoire deviendra insuffisante et que la JVM est sur le point de générer une erreur OutOfMemory . En d'autres termes, les objets avec uniquement des références logicielles sont collectés en dernier recours pour récupérer la mémoire.

Une référence faible peut être créée de la même manière en utilisant la classe WeakReference . Lorsque sb est défini sur null et que l' objet StringBuilder n'a qu'une référence faible, le garbage collector de la JVM n'aura absolument aucun compromis et collectera immédiatement l'objet au cycle suivant.

Une référence fantôme est similaire à une référence faible et un objet avec uniquement des références fantômes sera collecté sans attendre. Cependant, les références fantômes sont mises en file d'attente dès que leurs objets sont collectés. Nous pouvons interroger la file d'attente de référence pour savoir exactement quand l'objet a été collecté.

Q13. Supposons que nous ayons une référence circulaire (deux objets qui se référencent). Une telle paire d'objets pourrait-elle devenir éligible pour le nettoyage de la mémoire et pourquoi?

Oui, une paire d'objets avec une référence circulaire peut devenir éligible pour le garbage collection. Cela est dû à la façon dont le garbage collector de Java gère les références circulaires. Il considère les objets vivants non pas lorsqu'ils y font référence, mais lorsqu'ils sont accessibles en naviguant dans le graphique d'objets à partir d'une racine de garbage collection (une variable locale d'un thread en direct ou d'un champ statique). Si une paire d'objets avec une référence circulaire n'est pas accessible à partir d'une racine, elle est considérée comme éligible pour le garbage collection.

Q14. Comment les chaînes sont-elles représentées en mémoire?

Une instance String en Java est un objet avec deux champs: un champ de valeur char [] et un champ de hachage int . Le champ de valeur est un tableau de caractères représentant la chaîne elle-même, et le champ de hachage contient le hashCode d'une chaîne qui est initialisée à zéro, calculée lors du premier appel hashCode () et mise en cache depuis. Comme cas curieux, si un hashCode d'une chaîne a une valeur nulle, il doit être recalculé à chaque fois que hashCode () est appelé.

L'important est qu'une instance String est immuable: vous ne pouvez pas obtenir ou modifier le tableau char [] sous-jacent . Une autre caractéristique des chaînes est que les chaînes de constantes statiques sont chargées et mises en cache dans un pool de chaînes. Si vous avez plusieurs objets String identiques dans votre code source, ils sont tous représentés par une seule instance au moment de l'exécution.

Q15. Qu'est-ce qu'un Stringbuilder et quels sont ses cas d'utilisation? Quelle est la différence entre l'ajout d'une chaîne à un constructeur de chaînes et la concaténation de deux chaînes avec un opérateur +? Quelle est la différence entre Stringbuilder et Stringbuffer?

StringBuilder permet de manipuler des séquences de caractères en ajoutant, supprimant et insérant des caractères et des chaînes. Il s'agit d'une structure de données modifiable, par opposition à la classe String qui est immuable.

Lors de la concaténation de deux instances String , un nouvel objet est créé et les chaînes sont copiées. Cela pourrait entraîner une énorme surcharge du ramasse-miettes si nous devons créer ou modifier une chaîne dans une boucle. StringBuilder permet de gérer les manipulations de chaînes beaucoup plus efficacement.

StringBuffer est différent de StringBuilder en ce qu'il est thread-safe. Si vous devez manipuler une chaîne dans un seul thread, utilisez plutôt StringBuilder .

3. Conclusion

Dans cet article, nous avons couvert certaines des questions les plus courantes qui apparaissent fréquemment dans les entretiens avec les ingénieurs Java. Les questions sur la gestion de la mémoire sont principalement posées aux candidats Senior Java Developer, car l'intervieweur s'attend à ce que vous ayez créé des applications non triviales qui sont souvent en proie à des problèmes de mémoire.

Cela ne doit pas être traité comme une liste exhaustive de questions, mais plutôt comme une rampe de lancement pour de nouvelles recherches. Chez Baeldung, nous vous souhaitons beaucoup de succès dans vos prochaines interviews.

Suivant » Questions d'entretien Java Generics (+ réponses) « Précédent Questions d'entretien Java 8 (+ réponses)