Eden Space
Portanto, minha pergunta é: será que isso pode realmente ser verdade? Se sim, por que a alocação de heap do java é muito mais rápida?
Eu estudei um pouco sobre como o Java GC funciona, pois é muito interessante para mim. Estou sempre tentando expandir minha coleção de estratégias de alocação de memória em C e C ++ (interessado em tentar implementar algo semelhante em C), e é uma maneira muito, muito rápida de alocar muitos objetos de maneira rápida a partir de um perspectiva prática, mas principalmente devido ao multithreading.
A maneira como a alocação do Java GC funciona é usar uma estratégia de alocação extremamente barata para alocar objetos inicialmente ao espaço "Eden". Pelo que sei, é usando um alocador de pool seqüencial.
Isso é muito mais rápido apenas em termos de algoritmo e redução de falhas de página obrigatórias do que de propósito geral malloc
em C ou padrão, jogando operator new
em C ++.
Mas os alocadores seqüenciais têm uma fraqueza evidente: eles podem alocar pedaços de tamanho variável, mas não podem liberar nenhum pedaço individual. Eles apenas alocam de maneira sequencial direta com preenchimento para alinhamento e podem apenas limpar toda a memória que alocaram de uma só vez. Eles são úteis normalmente em C e C ++ para a construção de estruturas de dados que precisam apenas de inserções e não remoções de elementos, como uma árvore de pesquisa que só precisa ser criada uma vez quando o programa é iniciado e depois é pesquisada repetidamente ou apenas novas chaves foram adicionadas ( nenhuma chave removida).
Eles também podem ser usados mesmo para estruturas de dados que permitem a remoção de elementos, mas esses elementos não serão realmente liberados da memória, pois não podemos desalocá-los individualmente. Essa estrutura usando um alocador seqüencial consumiria cada vez mais memória, a menos que houvesse alguma passagem diferida na qual os dados fossem copiados para uma cópia compacta e nova usando um alocador sequencial separado (e essa é uma técnica muito eficaz se um alocador fixo ganhar por algum motivo - apenas aloque sequencialmente uma nova cópia da estrutura de dados e despeje toda a memória da antiga).
Coleção
Como no exemplo acima da estrutura de dados / pool sequencial, seria um grande problema se o Java GC apenas alocasse dessa maneira, mesmo que seja super rápido para uma alocação intermitente de muitos blocos individuais. Não seria possível liberar nada até que o software fosse desligado; nesse momento, ele poderia liberar (limpar) todos os conjuntos de memórias de uma só vez.
Portanto, depois de um único ciclo de GC, é feita uma passagem pelos objetos existentes no espaço "Eden" (alocados sequencialmente), e os que ainda são referenciados são alocados usando um alocador de uso geral, capaz de liberar pedaços individuais. Os que não são mais referenciados serão simplesmente desalocados no processo de limpeza. Então, basicamente, é "copiar objetos do espaço Eden, se eles ainda são referenciados e depois limpar".
Normalmente, isso seria muito caro, por isso é feito em um encadeamento em segundo plano separado para evitar um bloqueio significativo do encadeamento que originalmente alocava toda a memória.
Depois que a memória é copiada do espaço Eden e alocada usando esse esquema mais caro que pode liberar blocos individuais após um ciclo inicial do GC, os objetos são movidos para uma região de memória mais persistente. Esses blocos individuais são liberados em ciclos subsequentes de GC se deixarem de ser referenciados.
Rapidez
Portanto, de maneira grosseira, a razão pela qual o Java GC pode muito bem superar o C ou C ++ na alocação direta de heap é porque ele está usando a estratégia de alocação totalmente mais barata e totalmente degeneralizada no encadeamento que solicita a alocação de memória. Em seguida, ele economiza o trabalho mais caro que normalmente precisaríamos ao usar um alocador mais geral, como diretamente malloc
para outro encadeamento.
Portanto, conceitualmente, o GC realmente precisa fazer mais trabalho, mas está distribuindo isso entre os threads, para que o custo total não seja pago antecipadamente por um único thread. Ele permite que o thread que aloca memória faça com que seja super barato e adie a verdadeira despesa necessária para fazer as coisas corretamente, de modo que objetos individuais possam realmente ser liberados para outro thread. Em C ou C ++, quando ligamos malloc
ou ligamos operator new
, temos que pagar o custo total antecipadamente no mesmo encadeamento.
Essa é a principal diferença, e por que o Java pode muito bem superar o C ou C ++ usando apenas chamadas ingênuas para malloc
ou operator new
alocar um monte de pequenos pedaços individualmente. É claro que normalmente haverá algumas operações atômicas e algum bloqueio potencial quando o ciclo do GC entrar em ação, mas provavelmente é otimizado um pouco.
Basicamente, a explicação simples se resume a pagar um custo mais alto em um único encadeamento ( malloc
) vs. pagar um custo mais barato em um único encadeamento e depois pagar o custo mais alto em outro que possa ser executado em paralelo ( GC
). Como desvantagem, fazer isso dessa maneira implica que você precisa de dois indiretos para obter referência de objeto a objeto, conforme necessário, para permitir que o alocador copie / mova a memória sem invalidar as referências de objeto existentes e também poderá perder a localidade espacial quando a memória do objeto for saiu do espaço "Eden".
Por último, mas não menos importante, a comparação é um pouco injusta, porque o código C ++ normalmente não aloca uma carga de objetos individualmente no heap. O código C ++ decente tende a alocar memória para muitos elementos em blocos contíguos ou na pilha. Se ele alocar uma carga cheia de objetos minúsculos, um de cada vez, no armazenamento gratuito, o código é uma merda.