Coletor de Lixo Caseiro para OpenJDK

Esta é uma tradução do artigo de Alexey Shipilev “Faça você mesmo (OpenJDK) Garbage Collector” , publicado com o consentimento do autor. Relate quaisquer erros de digitação e outros erros no PM - vamos corrigi-los.

O processo de criar algo em tempo de execução é um exercício divertido. Pelo menos a criação da primeira versão! Criar um subsistema de tempo de execução confiável, de alto desempenho e com proteção contra falhas, cujo comportamento possa ser convenientemente observado e depurado, é uma tarefa muito, muito difícil.


Criar um simples coletor de lixo é enganosamente simples e agora quero fazer isso neste artigo. Roman Kennke, no FOSDEM 2019, fez uma palestra e demonstração intitulada "Writing a GC in 20 Minutes", usando uma versão anterior deste patch. Apesar do fato de o código implementado lá demonstrar muito e ser amplamente comentado, é necessário uma boa descrição de alto nível do que está acontecendo - foi assim que este artigo apareceu.


Um entendimento básico do trabalho dos coletores de lixo ajudará bastante a entender o que está escrito aqui. O artigo usará detalhes e idéias em uma implementação específica do HotSpot, mas não haverá curso introdutório sobre o design do GC aqui. Pegue o Manual do GC e leia os primeiros capítulos sobre os princípios básicos do GC, e ainda mais rápido iniciará o artigo da Wikipedia .



Conteúdo




1. Em que consiste o GC


Agora que muitos GCs diferentes foram escritos, é muito simples criar seus próprios elementos - muitos elementos já escritos podem ser (re) usados ​​para mudar algumas das preocupações com os detalhes da implementação para código comprovado e testado.



1.1 Epsilon gc


O OpenJDK 11 apresenta um novo JEP 318: "Epsilon: Um Coletor de Lixo No-Op (Experimental)" . Sua tarefa é fornecer uma implementação mínima para o caso em que a liberação de memória não é necessária ou mesmo proibida. O JEP discute com mais detalhes por que pode ser útil.


Do ponto de vista da implementação, “coletor de lixo” é um nome ruim, seria mais correto usar o termo “gerenciador de memória automático” , responsável por alocar e liberar memória. O Epsilon GC implementa apenas "alocação" e não lida com "liberação". Portanto, você pode usar o Epsilon GC e começar a implementar os algoritmos de "liberação" do zero.



1.1.1 Alocação de memória


A parte mais desenvolvida do Epsilon GC é responsável pela alocação de memória . Serve solicitações externas para alocar memória de tamanho arbitrário e criar TLAB (Thread-Local Allocation Buffer) do tamanho desejado. A implementação em si está tentando não estender muito o TLAB, pois não haverá memória livre e ninguém retornará os bytes perdidos.



1.1.2 Barreiras


Alguns coletores de lixo requerem interação com o aplicativo para manter os invariantes do GC, forçando o tempo de execução e o aplicativo a criar as chamadas barreiras ao tentar acessar o heap. Isso é verdade para todos os colecionadores multithread, assim como para muitos colecionadores com gerações e parando o mundo.


O Epsilon não requer barreiras, mas o tempo de execução e o compilador ainda querem saber que as barreiras não fazem nada. Lidar com isso todas as vezes em qualquer lugar pode ser cansativo. Felizmente, começando com o OpenJDK 11, existe um novo JEP-304: "Interface de Coleta de Lixo" , que torna muito, muito mais fácil inserir barreiras. Em particular, a barreira definida no Epsilon está vazia e todo o trabalho trivial - salvar, carregar, CAS, cópia em matriz - pode ser delegado às implementações de barreiras triviais de uma superclasse existente. Se você estiver criando um GC que também não precise de barreiras, basta reutilizar o código do Epsilon.



1.1.3 Monitorando a conexão


A última parte tediosa da implementação do GC são os ganchos de vários mecanismos de monitoramento dentro da JVM: compartimentos MX, comandos de diagnóstico etc. devem funcionar. Epsilon já fez tudo isso por você.



1.2 Rantime e GC



1.2.1 Elementos raiz


O coletor de lixo, no caso geral, precisa saber o que exatamente no tempo de execução Java tem referências de heap. Esses elementos raiz, chamados raízes de GC , podem ser slots em pilhas de fluxo e variáveis ​​locais (incluindo aquelas encontradas no código compilado por JIT!), Classes nativas e carregadores de classes, referências em JNI e assim por diante. As tentativas de identificar esses elementos podem ser muito complexas e tediosas. Mas no Hotspot, todos eles são rastreados usando os subsistemas de VM apropriados, para que você possa simplesmente aprender como as implementações de GC existentes funcionam com eles. Mais adiante no texto, veremos isso.



1.2.2 Rastreamento de objetos


O coletor de lixo deve ignorar os links de saída nos objetos Java. Essa operação é encontrada em todos os lugares, portanto, as partes comuns do tempo de execução fornecem soluções prontas; você não precisa escrever nada. Abaixo, haverá uma seção com uma implementação específica e você poderá encontrar, por exemplo, as chamadas obj→oop_iterate .



1.2.3 Deslocamentos


O coletor de lixo em movimento precisa anotar os novos endereços dos objetos movidos em algum lugar. Existem vários lugares onde você pode gravar esses dados de encaminhamento .


  1. Você pode reutilizar a “palavra marcador” no próprio objeto (Serial, Paralelo, etc.). Depois que o mundo para, todos os acessos ao objeto são controlados e é garantido que nenhum encadeamento Java possa ver os dados temporários que decidimos inserir na palavra do marcador. Você pode reutilizá-lo para armazenar dados de encaminhamento.
  2. Você pode manter uma tabela de movimento nativa separada ( ZGC , C4 e outras). Isso isola completamente o GC do tempo de execução e do restante do aplicativo, pois apenas o GC sabe da existência dessa tabela. Montadores competitivos geralmente usam exatamente esse esquema - eles não querem sofrer com um monte de problemas desnecessários.
  3. Você pode adicionar outra palavra ao objeto ( Shenandoah e outros). Essa combinação das duas abordagens anteriores não apenas permite que o tempo de execução e o aplicativo funcionem com cabeçalhos existentes sem problemas, mas também salva os dados de encaminhamento.


1.2.4 Dados do marcador


O coletor de lixo precisa gravar dados de marcação em algum lugar. E, novamente, existem várias maneiras de salvá-los:


  1. Você pode reutilizar a palavra do marcador no próprio objeto (Serial, Paralelo etc.). Novamente, no modo de parada mundial, você pode usar os bits na palavra do marcador para codificar o fato de um rótulo. Além disso, se você precisar percorrer todos os objetos vivos, percorreremos o heap, objeto após objeto - isso é possível devido ao fato de que o heap é analisável .
  2. Você pode manter uma estrutura separada para armazenar dados de marcação (G1, Shenandoah, etc.). Isso geralmente é feito usando um bitmap separado , que mapeia todos os N bytes do heap para 1 bit do cartão. Geralmente, os objetos Java são alinhados por 8 bytes ; portanto, o cartão mapeia a cada 64 bits do heap para 1 bit do cartão, ocupando 1/64 do tamanho do heap na memória nativa. Essas despesas gerais compensam bem ao varrer o heap quanto à presença de objetos vivos, especialmente os esparsos: ignorar o mapa geralmente é muito mais rápido do que ignorar o heap que está sendo classificado objeto por objeto.
  3. Codifique os rótulos nos próprios links (ZGC, C4 e outros). Isso requer coordenação com o aplicativo, então você precisa cortar todos esses rótulos dos links ou executar outros truques para manter a correção. Em outras palavras, precisamos de barreiras ou de algum trabalho adicional do GC.


2. Plano geral


Provavelmente, o mais fácil de implementar sobre o Epsilon é o Mark-Compact, no estilo LISP2. A idéia básica deste GC é descrita na Wikipedia e no Manual do GC (capítulo 3.2). Um esboço do algoritmo estará na seção com a implementação abaixo, mas eu recomendo fortemente a leitura de um pouco da Wikipedia ou do Manual do GC para entender o que vamos fazer.


O algoritmo em questão é o GC de deslocamento : os objetos em movimento movem-se em uma matriz até o início do heap. Tem seus prós e contras:


  • Ele mantém a ordem das alocações de memória. Isso é muito bom para controlar o layout na memória, se for importante para você (controlar malucos, é a sua vez!). A desvantagem é que você não obterá a localidade automática do link dessa maneira.
  • Sua complexidade é O (N) do número de objetos. No entanto, a linearidade tem um preço: é necessário que o GC ignore um monte de 4 vezes para cada ciclo de construção.
  • Não requer memória livre na pilha! Não há necessidade de reservar memória no heap para evacuar objetos ativos, para que você possa trabalhar com um heap que excede em 99. (9)%. Se adotarmos outras idéias de colecionadores simples, por exemplo, um limpador com um semi-espaço (limpador de semi-espaço), teremos que reescrever ligeiramente a apresentação da pilha e reservar um pouco de espaço para evacuação, mas isso está além do escopo deste exercício.
  • Se você trabalhar um pouco sobre o problema, poderá obter zero consumo de memória e tempo durante os períodos em que o GC estiver inativo. Inicia em uma memória em um estado arbitrário e para, compactando-a significativamente. Isso se encaixa muito bem com o funcionamento do Epsilon: ele continua destacando logo após o último objeto. Isso também é um sinal de menos: alguns objetos mortos no início da pilha levam a um grande número de movimentos.
  • Ele simplesmente não requer novas barreiras, você pode reutilizar o EpsilonBarrierSet como está.

Por uma questão de simplicidade, a implementação do GC usará um ponto final do mundo (stop-the-world, STW), não terá gerações ou multithreading. Nesse caso, faz sentido usar um bitmap para armazenar marcas e reutilizar a palavra do marcador para armazenar dados de movimento.



3. Implementação do núcleo do GC


Ler e entender toda a implementação pode ser muito complicado para uma pessoa ignorante. Nesta seção, entenderemos passo a passo.



3.1 Prólogo


O coletor de lixo geralmente precisa fazer algumas coisas para se preparar para a coleta. Leia os comentários, eles devem falar por si:


 { GCTraceTime(Info, gc) time("Step 0: Prologue", NULL); //      .      //   :   ,   ,  // «»   ,      //   ,     . if (!os::commit_memory((char*)_bitmap_region.start(), _bitmap_region.byte_size(), false)) { log_warning(gc)("Could not commit native memory for marking bitmap, GC failed"); return; } //        ,  , //       TLAB-. ensure_parsability(true); //      ,    GC. CodeCache::gc_prologue(); BiasedLocking::preserve_marks(); //        . //       . DerivedPointerTable::clear(); } 

Como usamos um bitmap para rastrear a acessibilidade dos objetos, precisamos limpá-lo antes do uso. Ou no nosso caso, como pretendemos nunca solicitar recursos antes de iniciar o ciclo do GC, teremos que comprometer o bitmap na memória com antecedência. Isso fornece várias vantagens interessantes, pelo menos no Linux, onde a maior parte do bitmap aponta para a página zero, especialmente para pilhas esparsas.


Os threads devem liberar seus TLABs e solicitar ao GC novos após a conclusão da compilação.


Não confunda TLAB e java.lang.ThreadLocal . Do ponto de vista do GC, ThreadLocal são objetos comuns e não serão compilados pelo GC, a menos que seja especificamente exigido de outra forma no código Java.

Algumas partes do tempo de execução, especialmente aquelas que mantêm links para o heap Java, serão interrompidas durante a coleta de lixo, portanto, você deve avisá-las especificamente de que o GC começará a funcionar em breve. Isso permitirá que os respectivos subsistemas preparem e salvem parte de seu estado antes que o GC faça a sua jogada.



3.2 Marcação


Marcar para interromper o modo mundial se torna bastante simples quando quase tudo já foi feito por nós. A rotulagem é bastante padrão e, provavelmente, em muitas implementações, o GC é o primeiro passo.


 { GCTraceTime(Info, gc) time("Step 1: Mark", NULL); //    ,     .  //   ,  ,    //      . EpsilonMarkStack stack; EpsilonScanOopClosure cl(&stack, &_bitmap); //      . process_roots(&cl); stat_reachable_roots = stack.size(); //    ,    . //    ,   , //      . while (!stack.is_empty()) { oop obj = stack.pop(); obj->oop_iterate(&cl); stat_reachable_heap++; } //       . DerivedPointerTable::set_active(false); } 

Isso funciona exatamente da mesma forma que em qualquer outro gráfico: você inicia a travessia com o conjunto inicial de vértices alcançáveis, segue as arestas de saída e grava todos os vértices visitados. A travessia continua até que todos os picos não visitados terminem. No GC, "vértices" são objetos e "arestas" são links entre eles.


Tecnicamente, poderíamos simplesmente recursivamente examinar o gráfico de objetos, mas essa é uma má idéia para gráficos arbitrários que podem ter diâmetros muito grandes. Imagine uma lista vinculada de um bilhão de picos! Portanto, para limitar a profundidade da recursão, usamos uma pilha de marcação que registra os objetos detectados.


O conjunto inicial de objetos alcançáveis ​​é a raiz do GC. Agora, não pense no que process_roots , mais sobre isso mais tarde. Por enquanto, digamos que ele ignora todos os links acessíveis do lado da VM.


Um bitmap com marcas serve como uma ferramenta para registrar a frente de onda de marcação (muitos objetos já visitados) e, no final - como um repositório do resultado desejado, um conjunto de todos os objetos alcançáveis. O trabalho real ocorre no EpsilonScanOopClosure , é aplicado a todos os objetos interessantes e iterado em todos os links do objeto selecionado.


Olha, Java sabia como fechar (fechamento) antes de ficar na moda!

 class EpsilonScanOopClosure : public BasicOopIterateClosure { private: EpsilonMarkStack* const _stack; MarkBitMap* const _bitmap; template <class T> void do_oop_work(T* p) { // p -     ,   oop,   //      ,  : T o = RawAccess<>::oop_load(p); if (!CompressedOops::is_null(o)) { oop obj = CompressedOops::decode_not_null(o); //  . ,   .  , //        . //    +, //       . if (!_bitmap->is_marked(obj)) { _bitmap->mark((HeapWord*)obj); _stack->push(obj); } } } }; 

Após concluir esta etapa, _bitmap contém bits indicando a localização dos objetos _bitmap . Graças a isso, é possível ignorar todos os objetos vivos, por exemplo:


 //           . //   ,    ( )  ,  //       1/64  . void EpsilonHeap::walk_bitmap(ObjectClosure* cl) { HeapWord* limit = _space->top(); HeapWord* addr = _bitmap.get_next_marked_addr(_space->bottom(), limit); while (addr < limit) { oop obj = oop(addr); assert(_bitmap.is_marked(obj), "sanity"); cl->do_object(obj); addr += 1; if (addr < limit) { addr = _bitmap.get_next_marked_addr(addr, limit); } } } 


3.3 Calcular novos endereços


Este também é um passo bastante simples e implementa exatamente o que o algoritmo diz.



 //    forwarding data (,    ) //   .        . //          . PreservedMarks preserved_marks; //     GC. HeapWord* new_top; { GCTraceTime(Info, gc) time("Step 2: Calculate new locations", NULL); //    ,        //    . ,  - . EpsilonCalcNewLocationObjectClosure cl(_space->bottom(), &preserved_marks); walk_bitmap(&cl); //         . //       ,    //      ,      "" //  . new_top = cl.compact_point(); stat_preserved_marks = preserved_marks.size(); } 

A única coisa que chama sua atenção é que decidimos armazenar novos endereços na palavra de marcação dos objetos Java, e essa palavra já pode ser ocupada por algo importante, por exemplo, informações sobre bloqueios. Felizmente, essas palavras de marcação não triviais são bastante raras e podemos simplesmente armazená-las separadamente, se necessário: é para isso que o PreservedMarks usado.


O trabalho algorítmico real é realizado por EpsilonCalcNewLocationObjectClosure :


 class EpsilonCalcNewLocationObjectClosure : public ObjectClosure { private: HeapWord* _compact_point; PreservedMarks* const _preserved_marks; public: EpsilonCalcNewLocationObjectClosure(HeapWord* start, PreservedMarks* pm) : _compact_point(start), _preserved_marks(pm) {} void do_object(oop obj) { //    :    . //        (      , //    ),      , //     . if ((HeapWord*)obj != _compact_point) { markOop mark = obj->mark_raw(); if (mark->must_be_preserved(obj)) { _preserved_marks->push(obj, mark); } obj->forward_to(oop(_compact_point)); } _compact_point += obj->size(); } HeapWord* compact_point() { return _compact_point; } }; 

forward_to é a parte mais importante porque armazena o "endereço de movimentação" na palavra do marcador do objeto. Isso será necessário nas próximas etapas.



3.4 Corrigir ponteiros


Agora você precisa passar pelo heap novamente e reescrever todos os links com seus novos endereços, de acordo com o seguinte algoritmo:



 { GCTraceTime(Info, gc) time("Step 3: Adjust pointers", NULL); //     _   _,     // « ».      forwarding data, //    .      . EpsilonAdjustPointersObjectClosure cl; walk_bitmap(&cl); //     ,      VM,  //     :      . EpsilonAdjustPointersOopClosure cli; process_roots(&cli); //   ,      , //     . preserved_marks.adjust_during_full_gc(); } 

Existem dois tipos de referências a objetos deslocados: saída de objetos no próprio heap ou das raízes do GC. Você precisa atualizar as duas classes de links. Alguns rótulos salvos também armazenam referências a objetos, portanto, é necessário solicitar a atualização. PreservedMarks sabe como fazer isso porque espera "encaminhar dados" no mesmo local em que os salvamos, na palavra de marcação do objeto.


Os fechamentos são divididos em dois tipos: alguns pegam objetos e ignoram seu conteúdo, outros atualizam esses endereços. Aqui você pode fazer uma pequena otimização de desempenho: se o objeto não se mover, você poderá salvar alguns registros em um monte:


 class EpsilonAdjustPointersOopClosure : public BasicOopIterateClosure { private: template <class T> void do_oop_work(T* p) { // p -     ,   oop. //        ,  : T o = RawAccess<>::oop_load(p); if (!CompressedOops::is_null(o)) { oop obj = CompressedOops::decode_not_null(o); //         . //  ,    . if (obj->is_forwarded()) { oop fwd = obj->forwardee(); assert(fwd != NULL, "just checking"); RawAccess<>::oop_store(p, fwd); } } } }; class EpsilonAdjustPointersObjectClosure : public ObjectClosure { private: EpsilonAdjustPointersOopClosure _cl; public: void do_object(oop obj) { //    ,    : obj->oop_iterate(&_cl); } }; 

Depois de concluir esta etapa, basicamente quebramos a pilha: os links apontam para os endereços "errados" nos quais os objetos ainda não estão. Vamos consertar!



3.5 Nós movemos objetos


Hora de mover objetos para novos endereços, de acordo com o algoritmo:



EpsilonMoveObjectsObjectClosure ao redor dos montões novamente e aplique o fechamento EpsilonMoveObjectsObjectClosure a todos os objetos vivos:


 { GCTraceTime(Info, gc) time("Step 4: Move objects", NULL); //       . //          . EpsilonMoveObjectsObjectClosure cl; walk_bitmap(&cl); stat_moved = cl.moved(); //         ,   // «»      . _space->set_top(new_top); } 

Imediatamente depois disso, você pode arrastar a pilha da pilha do ponto de compactação, possibilitando alocar memória diretamente desse local, imediatamente após o término do ciclo de coleta de lixo.


Observe que na montagem de deslocamento podemos sobrescrever o conteúdo dos objetos existentes, mas como a digitalização segue na mesma direção, os objetos substituídos já são copiados para o local certo.


Os locais antigos e novos da mesma instalação podem se cruzar. Por exemplo, se você mudar um objeto de 100 bytes por 8 bytes. O procedimento de cópia deve resolver por si próprio e o conteúdo que se cruza deve ser copiado corretamente, preste atenção a Copy::aligned_*conjoint*_words .

O fechamento em si simplesmente moverá os objetos movidos para os novos endereços:


 class EpsilonMoveObjectsObjectClosure : public ObjectClosure { public: void do_object(oop obj) { //     ,  .   - , //   -  mark word, //      forwarding data. if (obj->is_forwarded()) { oop fwd = obj->forwardee(); assert(fwd != NULL, "just checking"); Copy::aligned_conjoint_words((HeapWord*)obj, (HeapWord*)fwd, obj->size()); fwd->init_mark_raw(); } } }; 


3.6 Epílogo


A coleta de lixo está concluída, a pilha é novamente quase consistente, os últimos retoques finais são deixados:


 { GCTraceTime(Info, gc) time("Step 5: Epilogue", NULL); //    . preserved_marks.restore(); //   ,    . DerivedPointerTable::update_pointers(); BiasedLocking::restore_marks(); CodeCache::gc_epilogue(); JvmtiExport::gc_epilogue(); //     . if (!os::uncommit_memory((char*)_bitmap_region.start(), _bitmap_region.byte_size())) { log_warning(gc)("Could not uncommit native memory for marking bitmap"); } //    ,  . //        . if (EpsilonUncommit) { _virtual_space.shrink_by((_space->end() - new_top) * HeapWordSize); _space->set_end((HeapWord*)_virtual_space.high()); } } 

Informamos ao restante do tempo de execução que eles devem iniciar os procedimentos pós-montagem. Restauramos as palavras especiais marcadas que salvamos anteriormente. Beijo de despedida para o nosso cartão marcador - não é mais necessário.


E, se você realmente quiser, pode reduzir a memória para alocação para um novo tamanho, retornando a memória ao sistema operacional!



4. Conecte o GC à VM



4.1 Root Traversal


Lembre-se de que você precisa ignorar links especiais e acessíveis da VM? Você pode solicitar a cada subsistema VM especial para ignorar os links ocultos de outros objetos Java. Uma lista exaustiva de tais elementos raiz no Hotspot atual se parece com isso:


 void EpsilonHeap::do_roots(OopClosure* cl) { //   ,        1 . StrongRootsScope scope(1); //         . CLDToOopClosure clds(cl, ClassLoaderData::_claim_none); MarkingCodeBlobClosure blobs(cl, CodeBlobToOopClosure::FixRelocations); //      . //        . { MutexLockerEx lock(CodeCache_lock, Mutex::_no_safepoint_check_flag); CodeCache::blobs_do(&blobs); } { MutexLockerEx lock(ClassLoaderDataGraph_lock); ClassLoaderDataGraph::cld_do(&clds); } Universe::oops_do(cl); Management::oops_do(cl); JvmtiExport::oops_do(cl); JNIHandles::oops_do(cl); WeakProcessor::oops_do(cl); ObjectSynchronizer::oops_do(cl); SystemDictionary::oops_do(cl); Threads::possibly_parallel_oops_do(false, cl, &blobs); } 

, . GC .



4.2.


GC , VM . Hotspot VM_Operation , GC VM- :


 // VM_operation,      class VM_EpsilonCollect: public VM_Operation { private: const GCCause::Cause _cause; EpsilonHeap* const _heap; static size_t _last_used; public: VM_EpsilonCollect(GCCause::Cause cause) : VM_Operation(), _cause(cause), _heap(EpsilonHeap::heap()) {}; VM_Operation::VMOp_Type type() const { return VMOp_EpsilonCollect; } const char* name() const { return "Epsilon Collection"; } virtual bool doit_prologue() { //     ,     . //         GC, //          . //   ,         //  .     , //       1%, ,  , //     . Heap_lock->lock(); size_t used = _heap->used(); size_t capacity = _heap->capacity(); size_t allocated = used > _last_used ? used - _last_used : 0; if (_cause != GCCause::_allocation_failure || allocated > capacity / 100) { return true; } else { Heap_lock->unlock(); return false; } } virtual void doit() { _heap->entry_collect(_cause); } virtual void doit_epilogue() { _last_used = _heap->used(); Heap_lock->unlock(); } }; size_t VM_EpsilonCollect::_last_used = 0; void EpsilonHeap::vmentry_collect(GCCause::Cause cause) { VM_EpsilonCollect vmop(cause); VMThread::execute(&vmop); } 

, GC — , .



4.3


, GC , , GC , . , allocate_work , GC :


 HeapWord* EpsilonHeap::allocate_or_collect_work(size_t size) { HeapWord* res = allocate_work(size); if (res == NULL && EpsilonSlidingGC) { vmentry_collect(GCCause::_allocation_failure); res = allocate_work(size); } return res; } 

!



5.


OpenJDK.


 $ hg clone https://hg.openjdk.java.net/jdk/jdk/ jdk-jdk $ cd jdk-jdk $ curl https://shipilev.net/jvm/diy-gc/webrev/jdk-jdk-epsilon.changeset | patch -p1 

OpenJDK :


 $ ./configure --with-debug-level=fastdebug $ make images 

:


 $ build/linux-x86_64-server-fastdebug/images/jdk/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC -version openjdk version "13-internal" 2019-09-17 OpenJDK Runtime Environment (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon) OpenJDK 64-Bit Server VM (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon, mixed mode, sharing 


6.


, GC ? :


  1. . . Hotspot , JVM fastdebug , GC.
  2. . , . , ( ) , .
  3. Testes. , , , . - , .

, , :


 $ CONF=linux-x86_64-server-fastdebug make images run-test TEST=gc/epsilon/ Building targets 'images run-test' in configuration 'linux-x86_64-server-fastdebug' Test selection 'gc/epsilon/', will run: * jtreg:test/hotspot/jtreg/gc/epsilon Running test 'jtreg:test/hotspot/jtreg/gc/epsilon' Passed: gc/epsilon/TestAlwaysPretouch.java Passed: gc/epsilon/TestAlignment.java Passed: gc/epsilon/TestElasticTLAB.java Passed: gc/epsilon/TestEpsilonEnabled.java Passed: gc/epsilon/TestHelloWorld.java Passed: gc/epsilon/TestLogTrace.java Passed: gc/epsilon/TestDieDefault.java Passed: gc/epsilon/TestDieWithOnError.java Passed: gc/epsilon/TestMemoryPools.java Passed: gc/epsilon/TestMaxTLAB.java Passed: gc/epsilon/TestPrintHeapSteps.java Passed: gc/epsilon/TestArraycopyCheckcast.java Passed: gc/epsilon/TestClasses.java Passed: gc/epsilon/TestUpdateCountersSteps.java Passed: gc/epsilon/TestDieWithHeapDump.java Passed: gc/epsilon/TestByteArrays.java Passed: gc/epsilon/TestManyThreads.java Passed: gc/epsilon/TestRefArrays.java Passed: gc/epsilon/TestObjects.java Passed: gc/epsilon/TestElasticTLABDecay.java Passed: gc/epsilon/TestSlidingGC.java Test results: passed: 21 TEST SUCCESS 

? fastdebug . ? - .



7.


- spring-petclinic , Apache Bench GC! , , GC .


: -Xlog:gc -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC :


:


 Heap: 20480M reserved, 20480M (100.00%) committed, 19497M (95.20%) used GC(2) Step 0: Prologue 2.085ms GC(2) Step 1: Mark 51.005ms GC(2) Step 2: Calculate new locations 71.207ms GC(2) Step 3: Adjust pointers 49.671ms GC(2) Step 4: Move objects 22.839ms GC(2) Step 5: Epilogue 1.008ms GC(2) GC Stats: 70561 (8.63%) reachable from roots, 746676 (91.37%) reachable from heap, 91055 (11.14%) moved, 2237 (0.27%) markwords preserved GC(2) Heap: 20480M reserved, 20480M (100.00%) committed, 37056K (0.18%) used GC(2) Lisp2-style Mark-Compact (Allocation Failure) 20479M->36M(20480M) 197.940ms 

200 ? GC! , . , , : ( — , ). - ( ).


, GC . , -Xlog:gc -XX:+UseSerialGC — , , :


 GC(46) Pause Young (Allocation Failure) 575M->39M(1943M) 2.603ms GC(47) Pause Young (Allocation Failure) 575M->39M(1943M) 2.606ms GC(48) Pause Young (Allocation Failure) 575M->39M(1943M) 2.747ms GC(49) Pause Young (Allocation Failure) 575M->39M(1943M) 2.578ms 

, 2 . , , GC . -Xlog:gc -XX:+UseSerialGC , , :


 GC(3) Pause Full (Allocation Failure) 16385M->34M(18432M) 1969.694ms GC(4) Pause Full (Allocation Failure) 16385M->34M(18432M) 2261.405ms GC(5) Pause Full (Allocation Failure) 16385M->34M(18432M) 2327.577ms GC(6) Pause Full (Allocation Failure) 16385M->34M(18432M) 2328.976ms 

, . .



8. ?


. , GC OpenJDK — , , .


:


  1. . , // . . , , « » , , .

    GC, java.lang.ref.Reference.referent — Java-, , , - . FinalReference , .
    ReferenceProcessor / / .
  2. VM. VM, , , . . , , , - , .


  3. . — , GC, . , , , .

    mark-compact GC Full GC fallbacks Shenandoah ( OpenJDK 8) G1 ( OpenJDK 10, JEP 307: «Parallel Full GC for G1» ).

  4. . , «» , , , - . . , .


  5. . , , , . , «» — «» «» , .


  6. - GC Handbook .




9.


? GC — , , , GC.


, - GC . , , GC (, Serial GC Parallel GC), .


Minuto de publicidade. , 5-6 2019, JPoint — Java-. — OpenJDK, GraalVM, Kotlin . .

Source: https://habr.com/ru/post/pt443250/


All Articles