Mémoire de pile et tas en Java


99

Si je comprends bien, en Java, la mémoire de pile contient les primitives et les invocations de méthodes, tandis que la mémoire de tas est utilisée pour stocker des objets.

Supposons que j'ai une classe

class A {
       int a ;
       String b;
       //getters and setters
}
  1. Où la primitive aen classe Asera-t-elle stockée?

  2. Pourquoi la mémoire de tas existe-t-elle? Pourquoi ne pouvons-nous pas tout stocker sur la pile?

  3. Lorsque l'objet est nettoyé, la pile associée à l'objet faisant l'objet de la destruction est-elle détruite?



@ S.Lott, sauf que celui-ci concerne Java, pas C.
Péter Török

@ Péter Török: d'accord. Bien que l'exemple de code soit Java, aucune balise n'indique qu'il s'agit uniquement de Java. Et le principe général devrait s'appliquer aussi bien à Java qu'à C. De plus, il y a beaucoup de réponses à cette question sur Stack Overflow.
S.Lott

9
@SteveHaigh: sur ce site, tout le monde s'inquiète trop de savoir si quelque chose doit appartenir à cet endroit ... Je me demande quel état d'esprit partage vraiment ce site avec toute la fourberie de savoir si les questions appartiennent à cet endroit ou non.
Sam Goldberg

Réponses:


106

La différence fondamentale entre pile et tas est le cycle de vie des valeurs.

Les valeurs de pile n'existent que dans l'étendue de la fonction dans laquelle elles ont été créées. Une fois renvoyées, elles sont supprimées.
Les valeurs de tas existent cependant sur le tas. Ils sont créés à un moment donné et détruits à un autre (soit par GC, soit manuellement, en fonction de la langue / du temps d'exécution).

Maintenant, Java ne stocke que les primitives sur la pile. Cela réduit la taille de la pile et contribue à la taille réduite des trames de pile individuelles, permettant ainsi davantage d'appels imbriqués.
Les objets sont créés sur le tas et seules les références (qui sont à leur tour des primitives) sont transmises sur la pile.

Ainsi, si vous créez un objet, il est placé sur le tas, avec toutes les variables qui lui appartiennent, afin qu'il puisse persister après le retour de l'appel de la fonction.


2
"et seulement les références (qui sont à leur tour des primitives)" Pourquoi dit-on que les références sont des primitives? Pouvez-vous clarifier s'il vous plaît?
Geek

5
@ Geek: Parce que la définition commune des types de données primitifs s'applique: "un type de données fourni par un langage de programmation en tant que bloc de construction de base" . Vous remarquerez peut-être aussi que les références sont énumérées parmi les exemples canoniques plus loin dans l'article .
back2dos

4
@ Geek: En termes de données, vous pouvez visualiser n'importe quel type de données primitif, y compris les références, sous forme de nombres. Même chars sont des nombres et peuvent être utilisés indifféremment. Les références ne sont également que des chiffres faisant référence à une adresse mémoire, longue de 32 ou 64 bits (bien qu'elles ne puissent pas être utilisées telles quelles - à moins que vous ne jouiez avec sun.misc.Unsafe).
Sune Rasmussen

3
La terminologie de cette réponse est fausse. Selon les spécifications du langage Java, les références ne sont PAS des primitives. Cependant, l'essentiel de ce que dit la réponse est correct. (Alors que vous pouvez faire valoir que les références sont « dans un sens » primitif est le par la JLS définit la terminologie pour Java, et il dit que les types primitifs sont. boolean, byte, short, char, int, long, floatEt double.)
Stephen C

4
Comme je l’ai découvert dans cet article , Java peut stocker des objets sur la pile (ou même dans des registres pour de petits objets de courte durée). La JVM peut faire pas mal de choses sous les couvertures. Ce n'est pas tout à fait correct de dire "Java ne stocke plus que les primitives sur la pile".

49

Où sont stockés les champs primitifs?

Les champs primitifs sont stockés en tant que partie de l'objet instancié quelque part . La meilleure façon de voir où cela se trouve est le tas. Cependant , ce n'est pas toujours le cas. Comme décrit dans la théorie et la pratique Java: Légendes de la performance urbaine, revisitées :

Les machines virtuelles Java peuvent utiliser une technique appelée analyse d'échappement, qui leur permet de savoir que certains objets restent confinés dans un seul thread pendant toute leur durée de vie, et que cette durée de vie est limitée par la durée de vie d'un cadre de pile donné. De tels objets peuvent être alloués en toute sécurité sur la pile au lieu du tas. Mieux encore, pour les petits objets, la machine virtuelle Java peut optimiser complètement l’allocation et hisser simplement les champs de l’objet dans des registres.

Ainsi, au-delà de dire "l'objet est créé et le champ y est aussi", on ne peut pas dire si quelque chose se trouve sur le tas ou sur la pile. Notez que pour les petits objets de courte durée, il est possible que "l'objet" n'existe pas en mémoire en tant que tel et que ses champs soient placés directement dans des registres.

Le papier se termine par:

Les JVM sont étonnamment douées pour comprendre des choses que nous supposions auparavant que seul le développeur pouvait connaître. En laissant la JVM choisir entre l'allocation de pile et l'allocation de segment de mémoire au cas par cas, nous pouvons obtenir les avantages en termes de performances de l'allocation de pile sans que le programmeur ne s'embarrasse à l'idée d'allouer sur la pile ou sur le tas.

Donc, si vous avez un code qui ressemble à:

void foo(int arg) {
    Bar qux = new Bar(arg);
    ...
}

où le ...ne permet pas quxde quitter cette portée, qux peut être alloué sur la pile à la place. C’est en fait une victoire pour la machine virtuelle, car cela signifie qu’elle n’a pas besoin d’être ramassée à la poubelle - elle disparaîtra quand elle quittera la portée.

Plus d'informations sur l' analyse d'évasion sur Wikipedia. Pour ceux qui souhaitent approfondir leurs connaissances, Escape Analysis for Java d’IBM. Pour ceux qui viennent d'un monde C #, vous pouvez trouver de bonnes lectures: La pile est un détail d'implémentation et La vérité sur les types de valeur par Eric Lippert (utiles pour les types Java, de nombreux concepts et aspects étant identiques ou similaires) . Pourquoi les livres .Net parlent-ils d'allocation de mémoire pile / pile? va aussi dans cela.

Sur les pourquoi de la pile et du tas

Sur le tas

Alors, pourquoi avoir la pile ou le tas du tout? Pour les choses qui laissent de la place, la pile peut être chère. Considérons le code:

void foo(String arg) {
    bar(arg);
    ...
}

void bar(String arg) {
    qux(arg);
    ...
}

void qux(String arg) {
    ...
}

Les paramètres font également partie de la pile. Dans le cas où vous n'avez pas de tas, vous transmettez l'ensemble des valeurs de la pile. C'est bien pour de "foo"petites chaînes ... mais que se passerait-il si quelqu'un mettait un fichier XML énorme dans cette chaîne? Chaque appel copierait toute la chaîne sur la pile, ce qui serait une perte de temps.

Au lieu de cela, il est préférable de placer les objets qui ont une vie en dehors de la portée immédiate (passés à une autre portée, coincés dans une structure que quelqu'un d'autre maintient, etc.) dans un autre domaine appelé le tas.

Sur la pile

Vous n'avez pas besoin de la pile. On pourrait, hypothétiquement, écrire un langage qui n’utilise pas de pile (de profondeur arbitraire). Un vieux BASIC que j’ai appris dans ma jeunesse l’a fait, on ne pouvait faire que 8 niveaux d’ gosubappels et toutes les variables étaient globales - il n’y avait pas de pile.

L'avantage de la pile est que, lorsque vous avez une variable qui existe avec une étendue, lorsque vous quittez cette étendue, ce cadre de pile est éclaté. Cela simplifie vraiment ce qui existe et ce qui ne l’est pas. Le programme passe à une autre procédure, un nouveau cadre de pile; le programme revient à la procédure et vous revenez à celui qui voit votre étendue actuelle; le programme quitte la procédure et tous les éléments de la pile sont désalloués.

Cela rend vraiment la vie facile pour la personne qui écrit le runtime pour que le code utilise une pile et un tas. Ils consistent simplement en de nombreux concepts et façons de travailler sur le code, ce qui permet à la personne qui l’écrit dans le langage d’être libérée d’y penser explicitement.

La nature de la pile signifie également qu'elle ne peut pas être fragmentée. La fragmentation de la mémoire est un réel problème avec le tas. Vous allouez quelques objets, puis ramassez un objet central, puis vous essayez de trouver de la place pour le prochain objet de grande taille à allouer. C'est le bordel. Pouvoir mettre les choses sur la pile signifie plutôt que vous n'avez pas à vous en occuper.

Quand quelque chose est ramassé

Quand quelque chose est ramassé, il est parti. Mais ce n'est que des ordures collectées car elles ont déjà été oubliées - il n'y a plus de références à l'objet dans le programme auxquelles on peut accéder à partir de l'état actuel du programme.

Je ferai remarquer qu'il s'agit d'une très grande simplification de la collecte des ordures. Il existe de nombreux éboueurs (même en Java - vous pouvez modifier ce dernier en utilisant divers indicateurs ( docs ). Ceux-ci se comportent différemment et les nuances de la façon dont chacune fait les choses sont un peu trop profondes pour cette réponse. Vous voudrez peut-être lire Java Garbage Collection Basics pour avoir une meilleure idée de comment cela fonctionne.

Cela dit, si quelque chose est alloué sur la pile, ce ne sont pas des ordures collectées dans le cadre de System.gc(), mais elles sont désallouées lorsque le cadre de la pile apparaît. Si quelque chose se trouve sur le tas et est référencé à partir de quelque chose sur la pile, ce ne sera pas une ordure collectée à ce moment-là.

Pourquoi est-ce important?

Pour la plupart, sa tradition. Les manuels écrits, les classes de compilateur et la documentation sur divers bits font l’objet d’une grande importance pour le tas et la pile.

Cependant, les machines virtuelles d’aujourd’hui (JVM et similaires) ont tout mis en oeuvre pour tenter de cacher cela au programmeur. À moins que vous ne manquiez de l'un ou de l'autre et que vous ayez besoin de savoir pourquoi (plutôt que d'augmenter simplement l'espace de stockage de manière appropriée), peu importe.

L'objet est quelque part et il se trouve à l'endroit où il peut être consulté correctement et rapidement pendant la période de temps qu'il existe. Si c'est sur la pile ou le tas - ce n'est pas grave.


7
  1. Dans le tas, en tant que partie de l'objet, qui est référencé par un pointeur dans la pile. c'est à dire. a et b seront stockés l'un à côté de l'autre.
  2. Parce que si toute la mémoire était une pile de mémoire, cela ne serait plus efficace. C'est bien d'avoir une petite zone d'accès rapide où nous commençons et d'avoir les éléments de référence dans la zone de mémoire beaucoup plus grande qui reste. Cependant, ceci est excessif lorsqu'un objet est simplement une primitive qui prendrait à peu près la même quantité d'espace sur la pile que le pointeur qui le dirigerait.
  3. Oui.

1
J'ajouterais à votre remarque n ° 2 que si vous stockiez des objets sur la pile (imaginez un objet dictionnaire avec des centaines de milliers d'entrées), vous devrez alors copier l'objet, ou le renvoyer depuis une fonction. temps. En utilisant un pointeur ou une référence à un objet dans le tas, nous passons seulement la (petite) référence.
Scott Whitlock

1
Je pensais que 3 serait 'non' parce que si l'objet est en train d'être ramassé, il n'y a pas de référence dans la pile qui le pointe.
Luciano

@ Luciano - Je vois ce que tu veux dire. J'ai lu la question 3 différemment. Le "en même temps" ou "à ce moment-là" est implicite. :: haussement d'épaules ::
pdr le

3
  1. Sur le tas, à moins que Java n'alloue l'instance de classe sur la pile en tant qu'optimisation après avoir prouvé via une analyse d'échappement que cela n'affectera pas la sémantique. Il s’agit là d’un détail de la mise en œuvre. Par conséquent, à toutes fins pratiques, à l’exception de la micro-optimisation, la réponse est "sur le tas".

  2. La mémoire de pile doit être allouée et désallouée en dernier dans le premier sorti. La mémoire de tas peut être allouée et désallouée dans n'importe quel ordre.

  3. Lorsque l'objet est collecté, il n'y a plus de références pointant vers lui depuis la pile. S'il y en avait, ils garderaient l'objet en vie. Les primitives de pile ne sont pas du tout collectées car elles sont automatiquement détruites au retour de la fonction.


3

La mémoire de pile est utilisée pour stocker les variables locales et l'appel de fonction.

Alors que la mémoire de tas est utilisée pour stocker des objets en Java. Peu importe, où l'objet est créé dans le code.

Où la primitive aà class Astocker?

Dans ce cas, la primitive a est associée à un objet de classe A. Donc, il crée dans la mémoire de tas.

Pourquoi la mémoire de tas existe-t-elle? Pourquoi ne pouvons-nous pas tout stocker sur la pile?

  • Les variables créées sur la pile sortiront de la portée et seront automatiquement détruites.
  • La pile est beaucoup plus rapide à allouer par rapport aux variables du tas.
  • Les variables du segment de mémoire doivent être détruites par Garbage Collector.
  • Plus lent à allouer par rapport aux variables de la pile.
  • Vous utiliseriez la pile si vous savez exactement combien de données vous devez allouer avant la compilation et si elles ne sont pas trop volumineuses (les variables locales primitives sont stockées dans la pile).
  • Vous utiliseriez le tas si vous ne savez pas exactement combien de données vous aurez besoin au moment de l'exécution ou si vous devez allouer beaucoup de données.

Lorsque l'objet est nettoyé, la pile associée à l'objet faisant l'objet de la destruction est-elle détruite?

Garbage Collector fonctionne dans le cadre de la mémoire Heap, il détruit donc les objets qui ne possèdent pas de chaîne de référence de la racine.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.