Les initialisations d'objet en Java «Foo f = new Foo ()» sont-elles essentiellement les mêmes que l'utilisation de malloc pour un pointeur en C?

9

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?

Jules
la source
Les objets sont créés sur le tas pour les langages gérés comme c # / java. En cpp, vous pouvez également créer des objets sur la pile
bas
Pourquoi les créateurs de Java / C # ont-ils décidé de stocker exclusivement des objets sur le tas?
Jules
Je pense par souci de simplicité. Stocker des objets sur la pile et les transmettre un niveau plus profond implique de copier l'objet sur la pile, ce qui implique des constructeurs de copie. Je ne l' ai pas Google pour une réponse correcte, mais je suis sûr que vous pouvez trouver une réponse plus satisfaisante vous (ou quelqu'un d' autre développerai cette question de côté)
Bas
Les objets @Jules en java pourraient toujours être "décompressés" au moment de l'exécution (appelés scalar-replacement) dans des champs simples qui ne vivent que sur la pile; mais c'est quelque chose qui JITne fonctionne pas javac.
Eugene
«Tas» n'est qu'un nom pour un ensemble de propriétés associées aux objets / mémoire alloués. En C / C ++, vous pouvez sélectionner parmi deux ensembles de propriétés différents, appelés «pile» et «tas», en C # et Java, toutes les allocations d'objets ont le même comportement spécifié, qui porte le nom de «tas», qui ne impliquent que ces propriétés sont les mêmes que pour le «tas» C / C ++, en fait, elles ne le sont pas. Cela ne signifie pas que les implémentations ne peuvent pas avoir des stratégies différentes pour gérer les objets, cela implique que ces stratégies ne sont pas pertinentes pour la logique d'application.
Holger

Réponses:

5

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 newfait une allocation basée sur le tas comme malloc(), 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() et newsont 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 ...

int add(int lhs, int rhs) {
    int result = lhs + rhs;
    return result;
}

Ce qui précède semble assez simple. Nous définissons une fonction nomméeadd() 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 lhset rhssont de sizeof(int)4 octets chacun. La variable l' resultest également sizeof(int). Le compilateur peut dire que la add()fonction utilise 4 bytes * 3 intsou 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 la add()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 trois ints, une pour chaque lhs, rhsetresult . 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:

lhs:     ((int *)stack_pointer_register)[0]
rhs:     ((int *)stack_pointer_register)[1]
result:  ((int *)stack_pointer_register)[2]

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:

int addRandom(int count) {
    int numberOfBytesToAllocate = sizeof(int) * count;
    int *array = malloc(numberOfBytesToAllocate);
    int result = 0;

    if array != NULL {
        for (i = 0; i < count; ++i) {
            array[i] = (int) random();
            result += array[i];
        }

        free(array);
    }

    return result;
}

Notez que la addRandom()fonction ne sait pas au moment de la compilation quelle sera la valeur de l' countargument. Pour cette raison, cela n'a pas de sens d'essayer de définir arraycomme nous le ferions si nous le mettions sur la pile, comme ceci:

int array[count];

Si countc'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, et malloc()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 a malloc()é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 enfinfree()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.

par
la source
intn'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ème intde 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.
SS Anne
J'ai modifié ma réponse pour supprimer la déclaration sur les octets 8 intsur les plates-formes 64 bits. Vous avez raison, il intreste 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.
par