Eden Space
Quindi la mia domanda è: tutto ciò può essere vero, e se sì, perché l'allocazione dell'heap di java è molto più veloce.
Ho studiato un po 'su come funziona Java GC poiché è molto interessante per me. Cerco sempre di espandere la mia raccolta di strategie di allocazione della memoria in C e C ++ (interessato a provare a implementare qualcosa di simile in C), ed è un modo molto, molto veloce per allocare molti oggetti in modo rapido da un prospettiva pratica ma principalmente dovuta al multithreading.
Il modo in cui funziona l'allocazione del GC Java consiste nell'utilizzare una strategia di allocazione estremamente economica per allocare inizialmente gli oggetti nello spazio "Eden". Da quello che posso dire, sta usando un allocatore di pool sequenziale.
È molto più veloce solo in termini di algoritmo e di riduzione degli errori di pagina obbligatori rispetto a quelli generici malloc
in C o predefiniti, operator new
in C ++.
Ma gli allocatori sequenziali hanno un evidente punto debole: possono allocare blocchi di dimensioni variabili, ma non possono liberare alcun singolo blocco. Si allocano semplicemente in modo sequenziale con imbottitura per l'allineamento e possono solo eliminare tutta la memoria allocata in una sola volta. Sono utili in genere in C e C ++ per la costruzione di strutture di dati che richiedono solo inserimenti e nessuna rimozione di elementi, come un albero di ricerca che deve essere creato solo una volta all'avvio di un programma e quindi viene ripetutamente cercato o sono state aggiunte solo nuove chiavi ( nessuna chiave rimossa).
Possono anche essere utilizzati anche per le strutture di dati che consentono la rimozione di elementi, ma tali elementi non verranno effettivamente liberati dalla memoria poiché non è possibile allocarli singolarmente. Una struttura del genere che utilizza un allocatore sequenziale consumerebbe solo sempre più memoria, a meno che non avesse un passaggio differito in cui i dati venivano copiati in una nuova copia compatta usando un allocatore sequenziale separato (e questa è talvolta una tecnica molto efficace se un allocatore fisso ha vinto per qualche motivo, basta semplicemente allocare in sequenza una nuova copia della struttura dei dati e scaricare tutta la memoria di quella vecchia).
Collezione
Come nell'esempio di struttura di dati / pool sequenziale sopra, sarebbe un grosso problema se Java GC fosse allocato solo in questo modo, anche se è super veloce per un'allocazione a raffica di molti singoli blocchi. Non sarebbe in grado di liberare nulla fino allo spegnimento del software, a quel punto potrebbe liberare (eliminare) tutti i pool di memoria contemporaneamente.
Quindi, invece, dopo un singolo ciclo GC, viene effettuato un passaggio attraverso oggetti esistenti nello spazio "Eden" (allocati in sequenza), e quelli a cui si fa ancora riferimento vengono allocati utilizzando un allocatore più generico in grado di liberare singoli blocchi. Quelli a cui non si fa più riferimento verranno semplicemente dislocati nel processo di spurgo. Quindi in pratica si tratta di "copiare oggetti dallo spazio Eden se sono ancora citati e quindi eliminare".
Questo sarebbe normalmente piuttosto costoso, quindi viene eseguito in un thread in background separato per evitare di bloccare in modo significativo il thread che originariamente allocava tutta la memoria.
Una volta che la memoria viene copiata dallo spazio Eden e allocata utilizzando questo schema più costoso che può liberare singoli blocchi dopo un ciclo GC iniziale, gli oggetti si spostano in una regione di memoria più persistente. Quei singoli pezzi vengono quindi liberati nei successivi cicli GC se cessano di essere referenziati.
Velocità
Quindi, in parole povere, il motivo per cui il GC Java potrebbe benissimo sovraperformare C o C ++ nell'allocazione diretta dell'heap è perché sta usando la strategia di allocazione più economica e totalmente degenerata nel thread che richiede di allocare memoria. Quindi consente di risparmiare il lavoro più costoso che normalmente dovremmo fare quando utilizziamo un allocatore più generale come quello semplice malloc
per un altro thread.
Quindi concettualmente il GC in realtà deve fare complessivamente più lavoro, ma lo sta distribuendo su più thread in modo che il costo completo non sia pagato in anticipo da un singolo thread. Permette al thread di allocare memoria per farlo super economico, e quindi rinviare la vera spesa richiesta per fare le cose correttamente in modo che i singoli oggetti possano effettivamente essere liberati su un altro thread. In C o C ++ quando malloc
o chiamiamo operator new
, dobbiamo pagare l'intero costo in anticipo all'interno dello stesso thread.
Questa è la differenza principale e il motivo per cui Java potrebbe molto meglio di C o C ++ usando solo ingenui chiamate malloc
o operator new
allocare un gruppo di pezzi di adolescenti individualmente. Ovviamente ci saranno in genere alcune operazioni atomiche e qualche potenziale blocco quando si avvia il ciclo GC, ma è probabilmente ottimizzato abbastanza.
Fondamentalmente la semplice spiegazione si riduce al pagamento di un costo più pesante in un singolo thread ( malloc
) rispetto al pagamento di un costo più economico in un singolo thread e quindi al pagamento del costo più pesante in un altro thread che può essere eseguito in parallelo ( GC
). Come inconveniente, fare in questo modo implica che siano necessarie due indirette per ottenere dal riferimento all'oggetto l'oggetto come richiesto per consentire all'allocatore di copiare / spostare la memoria senza invalidare i riferimenti agli oggetti esistenti, e inoltre si può perdere la localizzazione spaziale una volta che la memoria degli oggetti è spostato dallo spazio "Eden".
Ultimo ma non meno importante, il confronto è un po 'ingiusto perché il codice C ++ normalmente non alloca un carico di oggetti barca individualmente sull'heap. Il codice C ++ decente tende ad allocare memoria per molti elementi in blocchi contigui o nello stack. Se alloca un carico di piccoli oggetti uno alla volta nel negozio gratuito, il codice è shite.