Sind Objektinitialisierungen in Java "Foo f = new Foo ()" im Wesentlichen dasselbe wie die Verwendung von malloc für einen Zeiger in C?

9

Ich versuche, den tatsächlichen Prozess hinter der Objekterstellung in Java zu verstehen - und ich nehme an, andere Programmiersprachen.

Wäre es falsch anzunehmen, dass die Objektinitialisierung in Java dieselbe ist wie bei Verwendung von malloc für eine Struktur in C?

Beispiel:

Foo f = new Foo(10);
typedef struct foo Foo;
Foo *f = malloc(sizeof(Foo));

Sollen sich deshalb Objekte eher auf dem Haufen als auf dem Stapel befinden? Weil sie im Wesentlichen nur Zeiger auf Daten sind?

Jules
quelle
Auf dem Heap werden Objekte für verwaltete Sprachen wie c # / java erstellt. In cpp können Sie genauso gut Objekte auf dem Stapel erstellen
bas
Warum haben die Entwickler von Java / C # beschlossen, Objekte ausschließlich auf dem Heap zu speichern?
Jules
Ich denke der Einfachheit halber. Um Objekte auf dem Stapel zu speichern und eine Ebene tiefer zu übergeben, muss das Objekt auf dem Stapel kopiert werden, was Kopierkonstruktoren umfasst. Ich habe nicht nach einer korrekten Antwort googelt, aber ich bin sicher, dass Sie selbst eine zufriedenstellendere Antwort finden können (oder jemand anderes wird diese
bas
@ Jules-Objekte in Java können zur Laufzeit immer noch scalar-replacementin einfache Felder "zerlegt" (aufgerufen ) werden, die nur auf dem Stapel leben. aber das ist etwas, was JITnicht tut javac.
Eugene
"Heap" ist nur ein Name für eine Reihe von Eigenschaften, die zugewiesenen Objekten / Speicher zugeordnet sind. In C / C ++ können Sie aus zwei verschiedenen Eigenschaftensätzen auswählen, die als "Stapel" und "Heap" bezeichnet werden. In C # und Java haben alle Objektzuordnungen das gleiche angegebene Verhalten, das unter dem Namen "Heap" angezeigt wird, was nicht der Fall ist Dies bedeutet, dass diese Eigenschaften dieselben sind wie für den C / C ++ - „Heap“. Dies ist jedoch nicht der Fall. Dies bedeutet nicht, dass Implementierungen keine unterschiedlichen Strategien zum Verwalten der Objekte haben können, sondern impliziert, dass diese Strategien für die Anwendungslogik irrelevant sind.
Holger

Antworten:

5

Weist in C malloc()einen Speicherbereich im Heap zu und gibt einen Zeiger darauf zurück. Das ist alles was du bekommst. Der Speicher ist nicht initialisiert und Sie können nicht garantieren, dass alles Nullen oder etwas anderes ist.

In Java führt das Aufrufen neweine Heap-basierte Zuweisung aus malloc(), aber Sie erhalten auch eine Menge zusätzlichen Komfort (oder Overhead, wenn Sie dies bevorzugen). Beispielsweise müssen Sie die Anzahl der zuzuweisenden Bytes nicht explizit angeben. Der Compiler ermittelt dies für Sie anhand des Objekttyps, den Sie zuweisen möchten. Darüber hinaus werden Objektkonstruktoren aufgerufen (an die Sie Argumente übergeben können, wenn Sie steuern möchten, wie die Initialisierung erfolgt). Bei der newRückkehr erhalten Sie garantiert ein Objekt, das initialisiert wurde.

Aber ja, am Ende des Aufrufs sind sowohl das Ergebnis malloc()als newauch Zeiger auf einen Teil der Heap-basierten Daten.

Der zweite Teil Ihrer Frage fragt nach den Unterschieden zwischen einem Stapel und einem Haufen. Weitaus umfassendere Antworten finden Sie in einem Kurs über das Compiler-Design (oder in einem Buch darüber). Ein Kurs über Betriebssysteme wäre ebenfalls hilfreich. Es gibt auch zahlreiche Fragen und Antworten zu SO über die Stapel und Haufen.

Trotzdem gebe ich einen allgemeinen Überblick, von dem ich hoffe, dass er nicht zu ausführlich ist, und möchte die Unterschiede auf einem ziemlich hohen Niveau erklären.

Grundsätzlich liegt der Hauptgrund für zwei Speicherverwaltungssysteme, dh einen Heap und einen Stack, in der Effizienz . Ein sekundärer Grund ist, dass jeder bei bestimmten Arten von Problemen besser ist als der andere.

Stapel sind für mich als Konzept etwas leichter zu verstehen, daher beginne ich mit Stapeln. Betrachten wir diese Funktion in C ...

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

Das obige scheint ziemlich einfach zu sein. Wir definieren eine Funktion mit dem Namen add()und übergeben die linken und rechten Addends. Die Funktion fügt sie hinzu und gibt ein Ergebnis zurück. Bitte ignorieren Sie alle Randfälle wie Überläufe, die auftreten können. An dieser Stelle ist dies für die Diskussion nicht relevant.

Der add()Zweck der Funktion scheint ziemlich einfach zu sein, aber was können wir über ihren Lebenszyklus sagen? Besonders die Speicherauslastung benötigt?

Am wichtigsten ist, dass der Compiler a priori (dh zur Kompilierungszeit) weiß , wie groß die Datentypen sind und wie viele verwendet werden. Die Argumente lhsund rhssind jeweils sizeof(int)4 Bytes. Die Variable resultist auch sizeof(int). Der Compiler kann feststellen, dass die add()Funktion 4 bytes * 3 intsinsgesamt 12 Byte Speicher verwendet.

Wenn die add()Funktion aufgerufen wird, enthält ein Hardware-Register, das als Stapelzeiger bezeichnet wird, eine Adresse, die auf die Oberseite des Stapels zeigt. Um den Speicher zuzuweisen, den die add()Funktion ausführen muss, muss der Funktionseintragscode lediglich eine einzelne Assembler-Anweisung ausgeben, um den Stapelzeigerregisterwert um 12 zu verringern. Auf diese Weise wird Speicher auf dem Stapel für drei Personen erstellt ints, jeweils für lhs, rhs, und result. Das Erhalten des Speicherplatzes, den Sie durch Ausführen eines einzelnen Befehls benötigen, ist ein enormer Geschwindigkeitsgewinn, da einzelne Befehle in der Regel in einem Takt ausgeführt werden (1 Milliardstel Sekunde einer 1-GHz-CPU).

Aus Sicht des Compilers kann auch eine Zuordnung zu den Variablen erstellt werden, die der Indizierung eines Arrays sehr ähnlich sieht:

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

Auch dies alles ist sehr schnell.

Wenn die add()Funktion beendet wird, muss sie bereinigt werden. Dies geschieht durch Subtrahieren von 12 Bytes vom Stapelzeigerregister. Es ähnelt einem Aufruf von, verwendet free()jedoch nur einen CPU-Befehl und nur einen Tick. Es ist sehr, sehr schnell.


Betrachten Sie nun eine Heap-basierte Zuordnung. Dies kommt ins Spiel, wenn wir nicht a priori wissen, wie viel Speicher wir benötigen werden (dh wir werden erst zur Laufzeit davon erfahren).

Betrachten Sie diese Funktion:

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;
}

Beachten Sie, dass die addRandom()Funktion zur Kompilierungszeit nicht weiß, wie hoch der Wert des countArguments sein wird. Aus diesem Grund ist es nicht sinnvoll zu versuchen, so zu definieren, arraywie wir es tun würden, wenn wir es auf den Stapel legen würden:

int array[count];

Wenn countes riesig ist, kann es dazu führen, dass unser Stack zu groß wird und andere Programmsegmente überschreibt. Wenn dieser Stapelüberlauf auftritt , stürzt Ihr Programm ab (oder schlimmer).

In Fällen, in denen wir nicht wissen, wie viel Speicher wir bis zur Laufzeit benötigen, verwenden wir malloc(). Dann können wir einfach nach der Anzahl der benötigten Bytes fragen, wenn wir sie benötigen, und malloc()prüfen, ob sie so viele Bytes verkaufen können. Wenn es geht, großartig, bekommen wir es zurück, wenn nicht, bekommen wir einen NULL-Zeiger, der uns sagt, dass der Aufruf malloc()fehlgeschlagen ist. Insbesondere stürzt das Programm jedoch nicht ab! Natürlich können Sie als Programmierer entscheiden, dass Ihr Programm nicht ausgeführt werden darf, wenn die Ressourcenzuweisung fehlschlägt, aber die vom Programmierer initiierte Beendigung unterscheidet sich von einem falschen Absturz.

Jetzt müssen wir zurückkommen, um die Effizienz zu untersuchen. Der Stapelzuweiser ist superschnell - ein Befehl zum Zuweisen, ein Befehl zum Freigeben und wird vom Compiler ausgeführt. Denken Sie jedoch daran, dass der Stapel für Dinge wie lokale Variablen bekannter Größe gedacht ist, sodass er eher klein ist.

Der Heap-Allokator hingegen ist um mehrere Größenordnungen langsamer. Es muss in Tabellen nachgeschlagen werden, um festzustellen, ob genügend freier Speicher vorhanden ist, um die vom Benutzer gewünschte Speichermenge zu verkaufen. Diese Tabellen müssen nach dem Verkauf des Speichers aktualisiert werden, um sicherzustellen, dass niemand diesen Block verwenden kann (für diese Buchhaltung muss der Allokator möglicherweise zusätzlich zu dem, was er verkaufen möchte, Speicher für sich selbst reservieren). Der Allokator muss Sperrstrategien anwenden, um sicherzustellen, dass der Speicher threadsicher verkauft wird. Und wenn die Erinnerung endlich istfree()d, was zu unterschiedlichen Zeiten und normalerweise in keiner vorhersehbaren Reihenfolge geschieht, muss der Allokator zusammenhängende Blöcke finden und sie wieder zusammenfügen, um die Haufenfragmentierung zu reparieren. Wenn das so klingt, als würde es mehr als eine einzige CPU-Anweisung erfordern, um all das zu erreichen, haben Sie Recht! Es ist sehr kompliziert und es dauert eine Weile.

Aber Haufen sind groß. Viel größer als Stapel. Wir können viel Speicher von ihnen erhalten und sie sind großartig, wenn wir zur Kompilierungszeit nicht wissen, wie viel Speicher wir benötigen. Wir tauschen also die Geschwindigkeit gegen ein verwaltetes Speichersystem aus, das uns höflich ablehnt, anstatt abzustürzen, wenn wir versuchen, etwas zu Großes zuzuweisen.

Ich hoffe, das hilft bei der Beantwortung einiger Ihrer Fragen. Bitte lassen Sie mich wissen, wenn Sie eine Erläuterung zu einem der oben genannten Punkte wünschen.

Par
quelle
intbeträgt auf einer 64-Bit-Plattform nicht 8 Byte. Es ist immer noch 4. Gleichzeitig optimiert der Compiler sehr wahrscheinlich den dritten intTeil des Stapels in das Rückgaberegister. Tatsächlich befinden sich die beiden Argumente wahrscheinlich auch in Registern auf jeder 64-Bit-Plattform.
SS Anne
Ich habe meine Antwort bearbeitet, um die Aussage über 8-Byte intauf 64-Bit-Plattformen zu entfernen . Sie sind richtig, dass intin Java 4 Bytes bleiben. Ich habe den Rest meiner Antwort jedoch hinterlassen, weil ich glaube, dass der Einstieg in die Compiler-Optimierung den Wagen vor das Pferd stellt. Ja, auch in diesen Punkten haben Sie Recht, aber in der Frage wird um Klärung von Stapeln und Haufen gebeten. RVO, Argumentation über Register, Code-Elision usw. überlasten die Grundkonzepte und behindern das Verständnis der Grundlagen.
Par