J'essaie de comprendre le processus réel derrière les créations d'objets en Java - et je suppose que d'autres langages de programmation.
Serait-il faux de supposer que l'initialisation d'objet en Java est la même que lorsque vous utilisez malloc pour une structure en C?
Exemple:
Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));
Est-ce pour cela que les objets sont censés se trouver sur le tas plutôt que sur la pile? Parce qu'ils ne sont essentiellement que des pointeurs vers des données?
scalar-replacement
) dans des champs simples qui ne vivent que sur la pile; mais c'est quelque chose quiJIT
ne fonctionne pasjavac
.Réponses:
En C,
malloc()
alloue une région de mémoire dans le tas et lui renvoie un pointeur. C'est tout ce que vous obtenez. La mémoire n'est pas initialisée et vous n'avez aucune garantie que ce sont tous des zéros ou autre chose.En Java, appeler
new
fait une allocation basée sur le tas commemalloc()
, mais vous bénéficiez également d'une tonne de commodité supplémentaire (ou de frais généraux, si vous préférez). Par exemple, vous n'avez pas besoin de spécifier explicitement le nombre d'octets à allouer. Le compilateur le calcule pour vous en fonction du type d'objet que vous essayez d'allouer. De plus, les constructeurs d'objets sont appelés (auxquels vous pouvez transmettre des arguments si vous souhaitez contrôler la façon dont l'initialisation se produit). Quandnew
retour, vous êtes assuré d'avoir un objet qui est initialisé.Mais oui, à la fin de l'appel, le résultat de
malloc()
etnew
sont simplement des pointeurs vers un morceau de données basées sur le tas.La deuxième partie de votre question porte sur les différences entre une pile et un tas. Des réponses beaucoup plus complètes peuvent être trouvées en suivant un cours (ou en lisant un livre sur) la conception du compilateur. Un cours sur les systèmes d'exploitation serait également utile. Il y a aussi de nombreuses questions et réponses sur SO sur les piles et les tas.
Cela dit, je vais donner un aperçu général, je l'espère, n'est pas trop verbeux et vise à expliquer les différences à un niveau assez élevé.
Fondamentalement, la principale raison d'avoir deux systèmes de gestion de mémoire, à savoir un tas et une pile, est pour l' efficacité . Une raison secondaire est que chacun est meilleur à certains types de problèmes que l'autre.
Les piles sont un peu plus faciles à comprendre en tant que concept, donc je commence par les piles. Considérons cette fonction en C ...
Ce qui précède semble assez simple. Nous définissons une fonction nommée
add()
et passons dans les addends gauche et droit. La fonction les ajoute et renvoie un résultat. Veuillez ignorer tous les cas marginaux tels que les débordements qui pourraient se produire, à ce stade, cela ne concerne pas la discussion.le
add()
but de la fonction semble assez simple, mais que pouvons-nous dire de son cycle de vie? Surtout ses besoins d'utilisation de la mémoire?Plus important encore, le compilateur sait a priori (c'est-à-dire au moment de la compilation) quelle est la taille des types de données et combien seront utilisés. Les arguments
lhs
etrhs
sont desizeof(int)
4 octets chacun. La variable l'result
est égalementsizeof(int)
. Le compilateur peut dire que laadd()
fonction utilise4 bytes * 3 ints
ou un total de 12 octets de mémoire.Lorsque la
add()
fonction est appelée, un registre matériel appelé pointeur de pile contient une adresse qui pointe vers le haut de la pile. Afin d'allouer la mémoire que laadd()
fonction doit exécuter, tout ce que le code d'entrée de fonction doit faire est d'émettre une seule instruction de langage d'assemblage pour décrémenter la valeur du registre du pointeur de pile de 12. Ce faisant, il crée un stockage sur la pile pour troisints
, une pour chaquelhs
,rhs
etresult
. Obtenir l'espace mémoire dont vous avez besoin en exécutant une seule instruction est un gain énorme en termes de vitesse car les instructions simples ont tendance à s'exécuter en une seule fois (1 milliardième de seconde un CPU à 1 GHz).De plus, du point de vue du compilateur, il peut créer une carte des variables qui ressemble énormément à l'indexation d'un tableau:
Encore une fois, tout cela est très rapide.
Lorsque la
add()
fonction se termine, elle doit être nettoyée. Pour ce faire, il soustrait 12 octets du registre de pointeur de pile. C'est similaire à un appel àfree()
mais il n'utilise qu'une seule instruction CPU et il ne prend qu'un tick. C'est très, très rapide.Considérons maintenant une allocation basée sur le tas. Cela entre en jeu lorsque nous ne savons pas a priori de combien de mémoire nous aurons besoin (c'est-à-dire que nous ne l'apprendrons qu'au moment de l'exécution).
Considérez cette fonction:
Notez que la
addRandom()
fonction ne sait pas au moment de la compilation quelle sera la valeur de l'count
argument. Pour cette raison, cela n'a pas de sens d'essayer de définirarray
comme nous le ferions si nous le mettions sur la pile, comme ceci:Si
count
c'est énorme, cela pourrait entraîner une trop grande taille de notre pile et écraser d'autres segments de programme. Lorsque cette pile déborde se produit, votre programme plante (ou pire).Donc, dans les cas où nous ne savons pas de combien de mémoire nous aurons besoin jusqu'à l'exécution, nous utilisons
malloc()
. Ensuite, nous pouvons simplement demander le nombre d'octets dont nous avons besoin quand nous en avons besoin, etmalloc()
vérifierons s'il peut vendre autant d'octets. Si c'est le cas, très bien, nous le récupérons, sinon, nous obtenons un pointeur NULL qui nous indique que l'appel amalloc()
échoué. Cependant, le programme ne plante pas! Bien sûr, en tant que programmeur, vous pouvez décider que votre programme n'est pas autorisé à s'exécuter si l'allocation des ressources échoue, mais la terminaison déclenchée par le programmeur est différente d'un crash parasite.Il nous faut donc maintenant revenir sur l'efficacité. L'allocateur de pile est super rapide - une instruction à allouer, une instruction à désallouer, et c'est fait par le compilateur, mais rappelez-vous que la pile est destinée à des choses comme des variables locales d'une taille connue, donc elle a tendance à être assez petite.
L'allocateur de tas, d'autre part, est plus lent de plusieurs ordres de grandeur. Il doit effectuer une recherche dans les tableaux pour voir s'il dispose de suffisamment de mémoire libre pour pouvoir vendre la quantité de mémoire que l'utilisateur souhaite. Il doit mettre à jour ces tables après avoir distribué la mémoire pour s'assurer que personne d'autre ne peut utiliser ce bloc (cette comptabilité peut nécessiter que l'allocateur se réserve de la mémoire pour lui-même en plus de ce qu'il prévoit de vendre). L'allocateur doit utiliser des stratégies de verrouillage pour s'assurer qu'il distribue la mémoire de manière thread-safe. Et quand la mémoire est enfin
free()
d, qui se produit à des moments différents et dans aucun ordre prévisible, l'allocateur doit trouver des blocs contigus et les recoudre pour réparer la fragmentation du tas. Si vous avez l'impression que cela va prendre plus d'une instruction CPU pour accomplir tout cela, vous avez raison! C'est très compliqué et cela prend du temps.Mais les tas sont gros. Beaucoup plus grand que les piles. Nous pouvons obtenir beaucoup de mémoire d'eux et ils sont parfaits lorsque nous ne savons pas au moment de la compilation de la quantité de mémoire dont nous aurons besoin. Nous échangeons donc la vitesse pour un système de mémoire géré qui nous décline poliment au lieu de planter lorsque nous essayons d'allouer quelque chose de trop grand.
J'espère que cela aidera à répondre à certaines de vos questions. Veuillez me faire savoir si vous souhaitez des éclaircissements sur l'un des points ci-dessus.
la source
int
n'est pas de 8 octets sur une plate-forme 64 bits. C'est toujours 4. Parallèlement à cela, le compilateur est très susceptible d'optimiser le troisièmeint
de la pile dans le registre de retour. En fait, les deux arguments sont également susceptibles d'être dans des registres sur n'importe quelle plate-forme 64 bits.int
sur les plates-formes 64 bits. Vous avez raison, ilint
reste 4 octets en Java. J'ai cependant laissé le reste de ma réponse parce que je pense qu'entrer dans l'optimisation du compilateur met le chariot avant le cheval. Oui, vous avez également raison sur ces points, mais la question demande des éclaircissements sur les piles par rapport aux tas. Le RVO, l'argument passant par les registres, l'élision du code, etc. surchargent les concepts de base et entravent la compréhension des fondamentaux.