Le inizializzazioni di oggetti in Java "Foo f = new Foo ()" sono sostanzialmente le stesse dell'uso di malloc per un puntatore in C?


9

Sto cercando di capire il processo reale dietro le creazioni di oggetti in Java - e suppongo che altri linguaggi di programmazione.

Sarebbe sbagliato supporre che l'inizializzazione dell'oggetto in Java sia la stessa di quando usi malloc per una struttura in C?

Esempio:

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

È per questo che si dice che gli oggetti si trovino nell'heap piuttosto che nello stack? Perché sono essenzialmente solo puntatori ai dati?


Gli oggetti vengono creati sull'heap per linguaggi gestiti come c # / java. In cpp puoi anche creare oggetti nello stack
bas

Perché i creatori di Java / C # hanno deciso di archiviare esclusivamente oggetti nell'heap?
Jules il

Io penso che per ragioni di semplicità. La memorizzazione di oggetti nello stack e il loro passaggio a un livello più profondo comporta la copia dell'oggetto nello stack, che implica costruttori di copie. Non ho cercato su Google una risposta corretta, ma sono sicuro che puoi trovare tu stesso una risposta più soddisfacente (o qualcun altro elaborerà su questa domanda secondaria)
bas

Gli oggetti @Jules in Java possono essere "decomposti" in fase di esecuzione (chiamati scalar-replacement) in semplici campi che vivono solo nello stack; ma questo è qualcosa che JITnon lo fa javac.
Eugene,

"Heap" è solo un nome per un insieme di proprietà associate ad oggetti / memoria allocati. In C / C ++ puoi scegliere tra due diversi insiemi di proprietà, chiamati "stack" e "heap", in C # e Java, tutte le allocazioni di oggetti hanno lo stesso comportamento specificato, che va sotto il nome di "heap", che non implicano che queste proprietà sono le stesse dell'heap C / C ++, in realtà non lo sono. Ciò non significa che le implementazioni non possano avere strategie diverse per la gestione degli oggetti, implica che tali strategie siano irrilevanti per la logica dell'applicazione.
Holger,

Risposte:


5

In C, malloc()alloca una regione di memoria nell'heap e restituisce un puntatore ad essa. Questo è tutto ciò che ottieni. La memoria non è inizializzata e non hai alcuna garanzia che siano tutti zeri o qualsiasi altra cosa.

In Java, chiamare newfa un'allocazione basata su heap proprio come malloc(), ma hai anche un sacco di comodità aggiuntiva (o spese generali, se preferisci). Ad esempio, non è necessario specificare esplicitamente il numero di byte da allocare. Il compilatore lo capisce in base al tipo di oggetto che stai tentando di allocare. Inoltre, vengono chiamati costruttori di oggetti (a cui è possibile passare argomenti se si desidera controllare il modo in cui si verifica l'inizializzazione). Quando newritorna, hai la garanzia di avere un oggetto inizializzato.

Ma sì, alla fine della chiamata sia il risultato di malloc()e newsono semplicemente dei puntatori ad alcuni blocchi di dati basati su heap.

La seconda parte della domanda pone domande sulle differenze tra una pila e una pila. Risposte molto più complete possono essere trovate seguendo un corso (o leggendo un libro sulla progettazione di compilatori). Sarebbe utile anche un corso sui sistemi operativi. Ci sono anche numerose domande e risposte su SO riguardo alle pile e ai cumuli.

Detto questo, fornirò una panoramica generale che spero non sia troppo dettagliata e abbia l'obiettivo di spiegare le differenze a un livello abbastanza alto.

Fondamentalmente, il motivo principale per avere due sistemi di gestione della memoria, vale a dire un heap e uno stack, è per l' efficienza . Un motivo secondario è che ognuno è migliore in alcuni tipi di problemi rispetto all'altro.

Le pile sono per me un po 'più facili da capire come un concetto, quindi inizio con le pile. Consideriamo questa funzione in C ...

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

Quanto sopra sembra abbastanza semplice. Definiamo una funzione denominata add()e passiamo negli addendi sinistro e destro. La funzione li aggiunge e restituisce un risultato. Si prega di ignorare tutti i casi limite come gli overflow che potrebbero verificarsi, a questo punto non è pertinente alla discussione.

Lo add()scopo della funzione sembra piuttosto semplice, ma cosa possiamo dire del suo ciclo di vita? Soprattutto le sue esigenze di utilizzo della memoria?

Ancora più importante, il compilatore sa a priori (cioè al momento della compilazione) quanto sono grandi i tipi di dati e quanti saranno usati. Gli argomenti lhse rhssono sizeof(int), 4 byte ciascuno. Anche la variabile resultè sizeof(int). Il compilatore può dire che la add()funzione utilizza 4 bytes * 3 intso un totale di 12 byte di memoria.

Quando add()viene chiamata la funzione, un registro hardware chiamato puntatore dello stack avrà un indirizzo che punta in cima allo stack. Per allocare la memoria che la add()funzione deve eseguire, tutto il codice di immissione della funzione deve essere emettere una singola istruzione di linguaggio assembly per ridurre il valore del registro del puntatore dello stack di 12. In tal modo, crea memoria per tre ints, uno ciascuno per lhs, rhse result. Ottenere lo spazio di memoria necessario eseguendo una singola istruzione è una vittoria enorme in termini di velocità perché le singole istruzioni tendono ad essere eseguite in un tick di clock (1 miliardesimo di secondo una CPU da 1 GHz).

Inoltre, dal punto di vista del compilatore, può creare una mappa per le variabili che assomiglia moltissimo come indicizzare un array:

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

Ancora una volta, tutto questo è molto veloce.

Quando la add()funzione esce, deve ripulire. Lo fa sottraendo 12 byte dal registro del puntatore dello stack. È simile a una chiamata free()ma utilizza solo un'istruzione CPU e richiede solo un segno di spunta. È molto, molto veloce.


Ora considera un'allocazione basata su heap. Questo entra in gioco quando non sappiamo a priori di quanta memoria avremo bisogno (cioè ne impareremo solo in fase di esecuzione).

Considera questa funzione:

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

Si noti che la addRandom()funzione non sa al momento della compilazione quale sarà il valore countdell'argomento. Per questo motivo, non ha senso cercare di definire arraycome faremmo se lo mettessimo in pila, in questo modo:

int array[count];

Se countè enorme, il nostro stack potrebbe diventare troppo grande e sovrascrivere altri segmenti di programma. Quando si verifica questo overflow dello stack, il programma si arresta in modo anomalo (o peggio).

Quindi, nei casi in cui non sappiamo quanta memoria avremo bisogno fino al runtime, usiamo malloc(). Quindi possiamo semplicemente chiedere il numero di byte di cui abbiamo bisogno quando ne abbiamo bisogno e malloc()andremo a controllare se può vendere quel numero di byte. Se è possibile, ottimo, lo recuperiamo, in caso contrario, otteniamo un puntatore NULL che ci dice che la chiamata è malloc()fallita. In particolare, tuttavia, il programma non si arresta in modo anomalo! Ovviamente tu come programmatore puoi decidere che il tuo programma non può essere eseguito se l'allocazione delle risorse fallisce, ma la terminazione avviata dal programmatore è diversa da un incidente spurio.

Quindi ora dobbiamo tornare a guardare l'efficienza. L'allocatore dello stack è super veloce: un'istruzione da allocare, un'istruzione da deallocare, ed è fatta dal compilatore, ma ricorda che lo stack è pensato per cose come variabili locali di dimensioni note, quindi tende ad essere abbastanza piccolo.

L'allocatore di heap invece è più lento di diversi ordini di grandezza. Deve fare una ricerca nelle tabelle per vedere se ha abbastanza memoria libera per poter vendere la quantità di memoria che l'utente desidera. Deve aggiornare quelle tabelle dopo aver venduto la memoria per assicurarsi che nessun altro possa utilizzare quel blocco (questa contabilità potrebbe richiedere all'allocatore di riservare memoria per sé in aggiunta a ciò che prevede di vendere). L'allocatore deve utilizzare strategie di blocco per assicurarsi che distribuisca la memoria in modo thread-safe. E quando la memoria è finalmentefree()d, che si verifica in momenti diversi e in genere in un ordine non prevedibile, l'allocatore deve trovare blocchi contigui e ricucirli insieme per riparare la frammentazione dell'heap. Se sembra che ci vorrà più di una singola istruzione CPU per ottenere tutto ciò, hai ragione! È molto complicato e richiede un po 'di tempo.

Ma i cumuli sono grandi. Molto più grande delle pile. Possiamo ottenere molta memoria da loro e sono fantastici quando non sappiamo al momento della compilazione di quanta memoria avremo bisogno. Quindi compromettiamo la velocità per un sistema di memoria gestito che ci rifiuta educatamente invece di schiantarci quando proviamo ad allocare qualcosa di troppo grande.

Spero che ciò aiuti a rispondere ad alcune delle tue domande. Per favore fatemi sapere se desiderate chiarimenti su uno dei punti precedenti.


intnon è di 8 byte su una piattaforma a 64 bit. È ancora 4. Insieme a questo, è molto probabile che il compilatore ottimizzi il terzo intfuori dallo stack nel registro di ritorno. In effetti, è probabile che i due argomenti siano anche nei registri di qualsiasi piattaforma a 64 bit.
SS Anne,

Ho modificato la mia risposta per rimuovere la dichiarazione sugli ints da 8 byte su piattaforme a 64 bit. Hai ragione che intrimane 4 byte in Java. Ho comunque lasciato il resto della mia risposta perché credo che l'ottimizzazione del compilatore metta il carrello davanti al cavallo. Sì, hai ragione anche su questi punti, ma la domanda richiede chiarimenti su pile contro cumuli. L'RVO, gli argomenti che passano attraverso i registri, l'elezione del codice, ecc. Sovraccaricano i concetti di base e ostacolano la comprensione dei fondamenti.
par
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.