Sistemas e coleta de lixo em tempo real
O desenvolvimento de aplicativos em tempo real (RT) distingue-se do desenvolvimento de aplicativo de propósito geral, impondo restrições de tempo em partes do comportamento de tempo de execução. Essas restrições são geralmente colocadas em seções do aplicativo, como um manipulador de interrupção, em que o código que responde à interrupção deve concluir seu trabalho em um determinado período de tempo. Quando sistemas RT rígidos, como monitores cardíacos ou sistemas de defesa, perdem esses prazos finais, isso é considerado como uma falha catastrófica de todo o sistema. Em sistemas RT leves, prazos finais perdidos podem ter efeitos adversos -- como uma GUI não exibir todos os resultados de um fluxo que está monitorando -- mas não constituem uma falha de sistema.
Em aplicativos Java, a Java Virtual Machine (JVM) é responsável por otimizar o comportamento de tempo de execução, gerenciar o heap de objeto e fazer a interface com o sistema operacional e o hardware. Apesar de essa camada de gerenciamento entre a linguagem e a plataforma facilitar o desenvolvimento de software, ela apresenta uma determinada quantia de sobrecarga nos programas. Uma dessas áreas é GC, que geralmente causa pausas não deterministas no aplicativo. A frequência e a duração das pausas são imprevisíveis, tornando a linguagem Java tradicionalmente inadequada para desenvolvimento de aplicativo RT. Algumas soluções existentes baseadas em Real-time Specification for Java (RTSJ) deixam os desenvolvedores evitarem os aspectos não deterministas da tecnologia Java, mas requerem que eles alterem seu modelo de programação existente.
Metronome é um coletor de lixo determinista que oferece tempos de pausa baixos limitados e utilização de aplicativo especificado para aplicativos Java padrão. Os tempos de pausas limitados reduzidos resultam de uma abordagem incremental à coleta e decisões de engenharia cuidadosas que incluem mudanças fundamentais na VM. A utilização é a porcentagem de tempo em uma janela de tempo específica que o aplicativo tem permissão para executar, com o restante sendo devotado à GC. O Metronome permite que os usuários especifiquem o nível de utilização que um aplicativo recebe. Combinado à RTSJ, o Metronome permite que os desenvolvedores desenvolvam software determinista com baixos tempos de pausas e livres de pausas quando janelas de sincronização são criticamente pequenas. Este artigo explica as limitações de GC tradicional para aplicativos RT, detalha a abordagem do Metronome e apresenta ferramentas e orientação para desenvolver aplicativos RT rígidos com o Metronome.
As implementações de GC tradicional usam uma abordagem parar o mundo (STW) para recuperar a memória heap. Um aplicativo é executado até o heap ser exaurido de memória livre, quando a GC para too o código do aplicativo, executa uma coleta de lixo e, em seguida, permite que o aplicativo continue.
A Figura 1 ilustra pausas STW tradicionais para atividade de GC que são geralmente imprevisíveis em frequência e duração. A GC tradicional é não determinista porque a quantia de esforço necessária para recuperar memória depende da quantia total e do tamanho de objetos que o aplicativo usa, as interconexões entre esses objetos e o nível de esforço necessário para liberar memória de heap suficiente para satisfazer alocações futuras.
Figura 1. Pausas de GC tradicional
Por que GC tradicional é não determinista
É possível entender por que os tempos de GC são ilimitados e imprevisíveis examinando os componentes básico de uma GC. Uma pausa de GC geralmente consiste em duas fases distintas: as fases marcar e limpar . Apesar de muitas implementações e abordagens poderem combinar ou modificar os significados dessas fases ou aprimorar a GC por meio de outros meios (como compactação para reduzir fragmentação dentro do heap) ou fazer com que determinadas fases operem de forma simultânea com o aplicativo em execução, esses dois conceitos são as linhas de base técnicas para a GC tradicional.
A fase marcar é responsável por rastrear todos os objetos visíveis para o aplicativo e marcar eles como ativos para evitar que tenham seu armazenamento recuperado. Esse rastreio começa com o conjunto raiz, que consiste em estruturas internas, como pilhas de encadeamento e referências globais aos objetos. Ele atravessa então a cadeia de referências até todos os objetos atingíveis (direta ou indiretamente) do conjunto raiz serem marcados. Objetos não marcados no final da fase marcar não podem ser alcançados pelo aplicativo (inativo), pois não há nenhum caminho do conjunto raiz por qualquer série de referências para localizá-los. A duração da fase marcar é imprevisível, pois o número de objetos ativos em um aplicativo em qualquer momento específico e o custo de atravessar todas as referências para localizar todos os objetos no sistema não podem ser previstos. Um Oracle em um sistema que se comporta de forma consistente poderia prever requisitos de tempo com base em características de sincronização anteriores, mas a precisão dessas previsões seria uma fonte adicional e não determinismo.
A fase limpar é responsável por examinar o heap após a marcação ser concluída e recuperar o armazenamento de objetos inativos de volta ao armazenamento livre para heap, tornando esse armazenamento disponível para alocação. Como na fase marcar, o custo da limpeza d objetos inativos de volta para o conjunto de memórias livres não pode ser completamente previsto. Apesar de o número e tamanho de objetos ativos no sistema poderem ser derivados da fase marcar, a posição no heap e a adequação para o conjunto de memórias livre dos mesmos podem requerer um nível imprevisível de esforço para analisar.
Adequação de GC tradicional para aplicativos RT
Os aplicativos RT devem poder responder aos estímulos do mundo real dentro de intervalos de tempo deterministas. Uma GC tradicional não pode atender esse requisito, pois o aplicativo deve parar a GC para recuperar qualquer memória não utilizada. O tempo que leva para a recuperação é ilimitado e está sujeito a variações. Além do mais, o tempo que a GC interromperá o aplicativo é tradicionalmente imprevisível. O tempo durante o qual o aplicativo é parado é referido como tempo de pausa porque o progresso do aplicativo é pausado para a GC recuperar o espaço livre. Tempos de pausa baixos são um requisito para aplicativo RT, pois eles geralmente representam o limite de sincronização superior para responsividade do aplicativo.
A abordagem do Metronome é dividir o tempo que consome ciclos de GC em uma série de incrementos chamados quanta. Para realizar isso, cada fase é designada para realizar seu trabalho total em uma série de etapas discretas, permitindo que o coletor:
- Aproprie-se do aplicativo por períodos deterministas muito curtos.
- Tenha progresso de avanço na coleta.
- Deixe o aplicativo continuar.
Essa sequência contrasta com o modelo tradicional em que o aplicativo é parado em pontos imprevisíveis, a GC é executada até a conclusão por algum período de tempo ilimitado e a GC é colocada então em modo quiesce para permitir que o aplicativo continue.
Apesar de dividir o ciclo de GC STW em pausas curtas limitadas ajudar a reduzir o impacto da GC, isso não é suficiente para aplicativos RT. Para que aplicativos RT atendam seus prazos finais, uma parte suficiente de qualquer período de tempo específico deve ser devotada ao aplicativo; caso contrário, os requisitos são violados e o aplicativo falha. Por exemplo, considere um cenário em que as pausas de GC sejam limitadas a 1 milissegundo: Se o aplicativo tiver permissão para ser executado por somente 0,1 milissegundos entre cada pausa de GC de 1 milissegundo, então pouco progresso será feito e até mesmo sistemas RT marginalmente complexos provavelmente falharão devido à falta de tempo para progredir. Na verdade, os tempos de pausas curtas que são suficientemente próximos não são diferentes de uma GC STW integral.
A Figura 2 ilustra um cenário em que a GC é executada na maior parte do tempo, mas ainda preserva tempos de pausas de 1 milissegundo:
Figura 2. Tempos de pausas curtas, mas pouco tempo de aplicativo
É necessária uma medida diferente, que forneça, além de tempos de pausas limitados, um nível de determinismo para as porcentagens de tempo alocadas ao aplicativo e à GC. Definimos a utilização do aplicativo como a porcentagem de tempo alocada a um aplicativo em uma determinada janela de tempo se arrastando continuamente sobre a execução completa do aplicativo. O Metronome garante que uma porcentagem do tempo de processamento é dedicada ao aplicativo. O uso do tempo restante fica a critério da GC: pode ser alocado ao aplicativo ou pode ser usado pela GC. Tempos de pausas curtas permitem garantias de utilização com granularidade mais fina do que um coletor tradicional. À medida que o intervalo de tempo usado para medir a utilização se aproxima de zero, a utilização esperada de um aplicativo é 0% ou 100%, pois a medida está abaixo do tamanho de quantum da GC. A garantia para a utilização é feita estritamente em medidas do tamanho da janela deslizante. O Metronome usa quanta de 500 microssegundos de duração sobre uma janela de 10 milissegundos e tem uma meta de utilização padrão de 70%.
A Figura 3 ilustra um ciclo de GC dividido em diversas fatias e tempo de 500 microssegundos, preservando 70% da utilização sobre uma janela de 10 milissegundos:
Figura 3. Utilização da janela deslizante
Na Figura 3, cada fatia de tempo representa um quantum que executa a GC ou o aplicativo. As barras abaixo das fatias de tempo representam a janela deslizante. Para qualquer janela deslizante, há no máximo 6 quanta de GC e pelo menos 14 quanta de aplicativos. Cada quantum de GC é seguido por pelo menos quantum de aplicativo, mesmo se a utilização alvo fosse preservada com quanta de GC sequenciais. Isso assegura que os tempos de pausas de aplicativo são limitados à duração de 1 quantum. No entanto, se a utilização do destino for especificada como estando abaixo de 50%, algumas instâncias de quanta de GC sequenciais ocorrerão para permitir que a GC acompanhe a alocação.
As Figuras 4 e 5 ilustram um cenário de utilização de aplicativo típico. Na Figura 4, a região na qual a utilização cai para 70% representa a região de um ciclo de GC contínuo. Observe que quando a GC está inativa, a utilização do aplicativo é 100%.
Figura 4. Utilização geral
A Figura 5 mostra somente uma fração do ciclo de GC da Figura 4:
Figura 5. Utilização do ciclo de GC
A Seção A da Figura 5 é um gráfico de escada no qual as partes decrescentes correspondem a quanta de GC e as partes planas correspondem a quanta dos aplicativos. A escada demonstra a GC respeitando os tempos de pausa baixos intercalando com o aplicativo, produzindo uma descida como em degraus em direção à utilização alvo. A Seção B consiste em atividade de aplicativo somente para preservar metas de utilização em todas as janelas deslizantes. É comum ver um padrão de utilização que mostra a atividade da GC somente no início do padrão. Isso ocorre porque a GC é executada sempre que permitida (preservando os tempos de pausa e a utilização) e isso geralmente significa que exaure seu tempo alocado no início do padrão e permite que o aplicativo recupere para o restante da janela de tempo. A Seção C ilustra a atividade da GC quando a utilização está próxima da utilização alvo. As partes crescentes representam quanta de aplicativo e as partes decrescentes são quanta de GC. A natureza dentada desta seção é novamente devido à intercalação de GC e aplicativo para preservar tempos de pausa baixos. A Seção D representa a parte após a qual o ciclo de GC foi concluído. A natureza crescente desta seção ilustra o fato e a GC não estar mais em execução e o aplicativo recuperará 100% de utilização.
A utilização alvo pode ser especificada pelo usuário no Metronome; é possível localizar informações adicionais na seção Ajustando o Metronome deste artigo.
Executando um aplicativo com o Metronome
O Metronome foi projetado para fornecer comportamento RT a aplicativos existentes. Nenhuma modificação de código do usuário deve ser necessária. O tamanho de heap e a utilização alvo desejados devem ser ajustados para o aplicativo para que a utilização alvo mantenha o rendimento do aplicativo desejado enquanto permite que a GC acompanhe a alocação. Os usuários executam seus aplicativos na carga mais pesada que desejam sustentar para assegurar que as características RT sejam preservadas e o rendimento do aplicativo seja suficiente. A seção Ajustando o Metronome deste artigo explica o que pode ser feito se o rendimento ou utilização for insuficiente. Em determinadas situações, as garantias de tempo de pausa curto do Metronome são insuficientes para as características RT de um aplicativo. Para esses casos, é possível usar RTSJ para evitar os tempos de pausa incorridos de GC.
Real-time Specification for Java
RTSJ é uma "especificação para inclusões na plataforma Java para permitir que programas Java sejam usados para aplicativos em tempo real". O Metronome deve estar ciente de determinados aspectos de RTSJ -- especialmente, RealtimeThreads (encadeamentos RT), NoHeapRealtimeThreads (NHRTs) e memória imortal. Os encadeamentos RT são encadeamentos Java que, entre outras características, são executados em uma prioridade mais alta do que encadeamentos Java regulares. NHRTs são encadeamentos RT que não podem conter referências a objetos de heap. Ou seja, objetos que podem ser acessados por NHRT não podem fazer referência a objetos sujeitos a GC. Em troca por esse compromisso, a GC não impedirá o planejamento de NHRTs, mesmo durante um ciclo de GC. Isso significa que NHRTs não incorrerão em quaisquer tempos de pausa. A memória imortal fornece um espaço de memória que não está sujeito a GC; isso significa que NHRTs podem fazer referência a objetos imortais. Esses são apenas alguns aspectos de RTSJ; consulte Recursos para obter um link para a especificação completa.
Problemas técnicos envolvidos em GC determinista
O Metronome usa diversas abordagens dentro da máquina virtual J9 para obter tempos de pausa deterministas enquanto a segurança de GC é garantida. Elas incluem arraylets, planejamento baseado em tempo do coletor de lixo, processamento de estruturas raízes para rastreio de objetos ativos, coordenação entre a máquina virtual J9 e GC para assegurar que todos os objetos ativos sejam localizados e o mecanismo usado para suspender a máquina virtual J9 para um quantum de GC.
Apesar de o Metronome obter tempos de pausa determinista por meio da divisão do processo de coleta em unidades de trabalho incrementais, a alocação pode causar soluços na GC em algumas situações. Uma área está na alocação de objetos grandes. Para a maioria das implementações de coletores, o subsistema de alocação mantém um conjunto de memória de heap livre, consumido pelo aplicativo por meio de alocação de objetos e reabastecido pelo coletor por meio de limpeza. Após a primeira coleção, memória de heap livre é principalmente o resultado de objetos que já estiveram ativos, mas agora estão inativos. Como não há nenhum padrão previsível sobre como ou quando esses objetos focam inativos, a memória livre resultante no heap é uma coleção de partes fragmentadas de tamanhos variados, mesmo se ocorrer a aglutinação de objetos inativos adjacentes. Além do mais, cada ciclo de coleta pode retornar um padrão diferente de partes livres. Como resultado, a alocação de um objeto grande o suficiente pode falhar se nenhuma parte de memória livre for grande o suficiente para satisfazer a solicitação. Geralmente, esses objetos grandes são arrays; objetos padrão geralmente não são maiores do que algumas dúzias de campos, frequentemente resultando em menos e 2 K em tamanho para a maioria da JVMs.
Para aliviar o problema de fragmentação, alguns coletores implementam uma fase de compactação, ou desfragmentação, em seu ciclo de coleta. Após a conclusão da limpeza, se uma solicitação de alocação não puder ser atendida, o sistema tenta mover objetos ativos existentes em torno do heap em um esforço para unir duas ou mais partes livres em uma única parte maior. Essa fase às vezes é implementada como um recurso on demand, embarcado na malha do coletor (coletores de semiespaço sendo um exemplo) ou de maneira incremental. Cada um desses sistemas tem suas compensações, mas, em geral, a fase de compactação é uma dispendiosa em termos de tempo e esforço.
A versão atual do Metronome no WebSphere Real Time não implementa um sistema de compactação. Para evitar que a fragmentação seja um problema, o Metronome usa arraylets, o que divide a representação linear padrão em diversas partes distintas que podem ser alocadas de forma independente umas das outras.
A Figura 6 mostra que objetos de array aparecem como uma espinha -- que é o objeto central e a única entidade que pode ser referida por outros objetos no heap -- e uma séria de folhas de arraylet, que contêm o conteúdo real do array:
Figura 6. Arraylets
As folhas do arraylet não são referidas por outros objetos de heap e podem ser espalhadas pelo heap em qualquer posição e ordem. As folhas são de um tamanho fixo para permitir o cálculo simples da posição do elemento, que é uma inclusão indireta. Como a Figura 6 ilustra, a sobrecarga de uso e memória devido à fragmentação interna na espinha foi otimizada, incluindo quaisquer dados de trilha para uma folha na espinha.
Observe que esse formato pode significar que uma espinha de array pode crescer até tamanhos ilimitados, mas isso ainda não foi considerado um problema no sistema existente.
Para planejar pausas deterministas para GC, o Metronome usa dois encadeamentos diferentes para obter planejamento consistente e tempos de pausa curtos ininterruptos:
- O encadeamento de alarme. Para planejar um quantum de GC de forma determinista, o Metronome dedica o encadeamento de alarme para agir como o mecanismo de pulsação. O encadeamento de alarme é um encadeamento de prioridade muito alta (mais alta do que de qualquer outro encadeamento da JVM no sistema) que é ativado na mesma taxa que o período de tempo de quantum de GC (500 microssegundos no Metronome) e é responsável por determinar se um quantum de GC deve ou não ser planejado. Nesse caso, o encadeamento de alarme deve suspender a JVM em execução e ativar o encadeamento de GC. O encadeamento de alarme está ativo para um período de tempo muito curto (geralmente sob 10 microssegundos) e deve passar despercebido pelo aplicativo.
- O encadeamento de GC. O encadeamento de GC executa o trabalho real durante um quantum de GC. O encadeamento de GC deve primeiro concluir a suspensão da JVM que o encadeamento de alarme iniciou. Pode então executar o trabalho de GC para o restante do quantum, planejando-se de volta à inatividade e retomando a JVM quando o horário de encerramento se aproximar. O encadeamento de GC também pode se apropriar da inatividade se não puder concluir seu próximo item de trabalho antes do horário de encerramento do quantum. Em relação a RTSJ, a prioridade desse encadeamento é mais alta do que a de todos os encadeamentos RT, exceto NHRTs.
Mecanismo de suspensão cooperativo
Apesar de o Metronome usar uma série de pequenas pausas incrementais para concluir um ciclo de GC, ele ainda deve suspender a JVM para cada quantum de maneira STW. Para cada uma dessas pausas STW, o Metronome usa o mecanismo de suspensão cooperativo na máquina virtual J9. Esse mecanismo não depende de nenhum recurso de encadeamento nativo especial para suspender encadeamentos. Em vez disso, usa um sistema de mensagens de estilo assíncrono para notificar encadeamentos Java que devem liberar seu acesso a estruturas internas da JVM, incluindo o heap, e devem ficar inativos até serem sinalizados para continuarem o processamento. Encadeamentos Java dentro da máquina virtual J9 verificam periodicamente se uma solicitação suspensa foi emitida e, em caso afirmativo, procedem da seguinte forma:
- Liberam quaisquer estruturas internas da JVM retidas.
- Armazenam quaisquer referências de objetos retidas em locais bem descritos.
- Sinalizam o mecanismo de suspensão central da JVM que atingiu um ponto seguro.
- Ficam inativos e esperam uma continuação correspondente.
Na continuação, os encadeamentos releem ponteiros de objetos e readquirem as estruturas relacionadas à JVM anteriormente retidas. O ato de liberar estruturas da JVM permite que o encadeamento de GC processe essas estruturas de maneira segura; leitura e gravação de estruturas parcialmente atualizadas pode causar comportamento inesperado e paralisações. Ao armazenar e, em seguida, recarregar ponteiros de objetos, os encadeamentos concedem à GC a oportunidade de atualizar ponteiros de objetos durante um quantum de GC, que é necessário se o objeto for movido como parte de qualquer operação semelhante a compactação.
Como o mecanismo de suspensão coopera com encadeamentos Java, é importante que as verificações periódicas de cada encadeamento sejam espaçadas umas das outras com os intervalos mais curtos possíveis. Essa é a responsabilidade da JVM e do compilador Just-in-time (JIT). Apesar de verificar solicitações de suspensão apresentar uma sobrecarga, permite que estruturas como pilhas sejam bem definidas em termos das necessidades da GC, deixando-a determinar de forma precisa se os valores em pilhas são ponteiros para objetos ou não.
Esse mecanismo de suspensão é usado somente para encadeamentos que participam atualmente de atividades relacionadas à JVM; encadeamentos não Java, ou encadeamentos Java que estão no código de Java Native Interface (JNI) e de não estão usando a API de JNI, não estão sujeitos a serem suspensos. Se esses encadeamentos participarem de quaisquer atividades da JVM, como conectar à JVM ou chamar a API de JNI, eles serão suspensos de forma cooperativa até o quantum de GC ser concluído. Isso é importante porque permite que encadeamentos associados ao processo Java continuem a ser planejados. E, apesar de as prioridades de encadeamentos serem respeitadas, perturbar o sistema de qualquer maneira evidente nesses outros encadeamentos pode afetar o determinismo da GC.
Coletores STW integrais têm o benefício de poderem rastrear por referências de objetos e estruturas internas da JVM sem que o aplicativo perturbe os links no gráfico de objeto. Ao dividir o ciclo de GC em uma série de pequenas fases STW e intercalar sua execução com a do aplicativo, o Metronome apresenta um problema em potencial para controlar os objetos ativos em um sistema. Comportamento inesperado ou paralisações podem ocorrer porque o aplicativo, após processar um objeto, pode modificar as referências do objeto de forma que objetos não processados sejam ocultados do coletor. A Figura 7 ilustra o problema de objeto oculto:
Figura 7. Problema de objeto oculto
Suponhamos que um gráfico de objeto exista no heap, conforme descrito da Figura 7, seção I. O coletor Metronome está ativo e planejado para executar trabalho de rastreio nesse quantum. Em seu período de tempo alocado, ele consegue rastrear pelo objeto raiz, assim como pelo objeto ao qual faz referência, antes de ficar sem tempo e precisar planejar a JVM novamente na seção II. Durante a execução do aplicativo, as referências entre os objetos são alteradas de forma que o objeto A agora aponta pata um objeto não processado, que não é mais referido por nenhum outro local na seção III. A GC é então planejada novamente para outro quantum e continua a processar, sem esse ponteiro de objeto oculto . O resultado é que durante a fase limpar da GC que retorna objetos não marcados à lista livre, um objeto ativo será reclamado, resultando em um ponteiro pendente , causando comportamento incorreto ou até mesmo paralisações na JVM ou na GC.
Para evitar esse tipo de erro, a JVM e o Metronome devem cooperar no rastreamento de mudanças para as estruturas de heap e da JVM, de forma que a GC mantenha todos os objetos relevantes ativos. Isso é obtido por meio de uma barreira de gravação, que controla todas as gravações em objetos e registra a criação e divisão de referências entre objetos, de forma que o coletor possa controlar objetos ativos ocultos em potencial. O tipo de barreira que o Metronome usa é chamado de barreira de captura instantânea no início (SATB). Conceitualmente, registra o estado de heap no início de um ciclo de coleção e preserva todos os objetos ativos nesse ponto, assim como todos os alocados durante o ciclo atual. A solução concreta envolve uma barreira do tipo Yuasa (consulte Recursos), na qual o valor sobrescrito em qualquer armazenamento de campo é registrado e tratado como se tivesse uma referência raiz associada a ele. Preservar um valor de slot original antes de sobrescrever permite que o objeto ativo configurado seja preservado e processado.
Esse tipo de processamento de barreira também é necessário para estruturas internas da JVM, incluindo a lista JNI Global Reference. Como o aplicativo pode incluir e remover objetos dessa lista, uma barreira é aplicada para controlar objetos removidos, para evitar um problema de objeto oculto (semelhante a uma sobrescrição de campo), e objetos incluídos, para eliminar a necessidade de verificar a estrutura novamente.
Verificação e processamento da raiz
Para iniciar o rastreio de objetos ativos, os coletores de lixo começam a partir de um conjunto de objetos iniciais obtidos de raízes. Raízes são estruturas dentro da JVM que representam referências rígidas a objetos que o aplicativo cria explicitamente (por exemplo, JNI Global References) ou implicitamente (por exemplo, pilhas). As estruturas raízes são verificadas como parte da função inicial da fase marcar no coletor.
A maioria das raízes é maleável até certo grau durante a execução em termos de suas referências de objetos. Por essa razão, as mudanças ao conjunto de referências devem ser controladas, conforme discutido em Barreiras de gravação. No entanto, determinadas estruturas, como a pilha, não suportam pushes e pops sem penalidades significativas incorridas ao desempenho. Devido a isso, determinadas limitações e mudanças à verificação de pilhas são feitas para o Metronome para que mantenha a barreira estilo Yuasa:
-
Verificação atômica de pilhas. Pilhas de encadeamento individual devem ser verificadas de forma atômica ou dentro de um único quantum. A razão para isso é que durante a execução, um encadeamento pode efetuar pop de um número qualquer de referências dessa pilha -- referências que poderiam ter sido armazenadas em outro local durante a execução. Uma pausa no meio da verificação de uma pilha pode causar a perda de controle ou a ausências de armazenamentos durante duas verificações parciais, criando um ponteiro pendente dentro do heap. Desenvolvedores de aplicativos devem estar cientes de que pilhas são verificadas de forma atômica e devem evitar o uso de pilhas muito profundas em seus aplicativos RT.
- Barreira difusa. Apesar de uma pilha precisar ser verificada de forma atômica, seria difícil manter o determinismo se todas as pilhas fossem verificadas durante um único quantum. A GC e a JVM podem intercalar a execução durante a verificação de pilhas Java. Isso poderia resultar na movimentação de objetos de um encadeamento para outro por meio de uma série de carregamentos e armazenamentos. Para evitar a perda de referências a objetos, os encadeamentos que ainda não foram verificados durante uma GC fazem com que a barreira controle o valor sobrescrito e o valor que está sendo armazenado. O rastreamento do objeto armazenado, caso esteja armazenado em um objeto já processado e com pop efetuado para fora da pilha, preserva o alcance por meio da barreira de gravação.
É importante entender a correlação entre o tamanho de heap e a utilização do aplicativo. Apesar de utilização de meta alta ser desejável para o rendimento ideal do aplicativo, a GC deve poder acompanhar a taxa de alocação do aplicativo. Se as taxas de utilização e de alocação alvo forem altas, o aplicativo pode ficar sem memória, forçando a GC a ser executada continuamente e baixando a utilização para 0% na maioria dos casos. Essa degradação apresenta longos tempos de pausa, frequentemente inaceitáveis para aplicativos RT. Se esse cenário for encontrado, deve ser feita uma opção para reduzir a utilização alvo para conceder mais tempo para a GC, aumentar o tamanho de heap para conceder mais alocações ou uma combinação de ambos. Algumas situações podem não ter a memória necessária para sustentar uma determinada meta de utilização, portanto, reduzir a utilização alvo com prejuízo ao desempenho é a única opção.
A Figura 8 ilustra uma compensação típica entre o tamanho de heap e a utilização do aplicativo. Uma porcentagem de utilização mais alta requer um heap maior, pois a GC não pode ser executada tantas vezes quanto uma utilização mais baixa permitiria.
Figura 8. Tamanho de heap versus utilização
O relacionamento entre a utilização e o tamanho de heap depende muito do aplicativo e, para se obter um balanço apropriado, é necessária experimentação iterativa com os parâmetros do aplicativo e da VM.
A Verbose GC é uma ferramenta que registra e emite saída da atividade da GC em um arquivo ou na tela. É possível usá-la para determinar se os parâmetros (tamanho de heap, utilização alvo, tamanho da janela e tempo de quantum) suportam o aplicativo em execução. A Listagem 1 mostra um exemplo de saída detalhada:
Listagem 1. Amostra da Verbose GC
<?xml version="1.0" ?>
<verbosegc version="200702_15-Metronome">
<gc type="synchgc" id="1" timestamp="Tue Mar 13 15:17:18 2007" intervalms="0.000">
<details reason="system garbage collect" />
<duration timems="30.023" />
<heap freebytesbefore="535265280" />
<heap freebytesafter="535838720" />
<immortal freebytesbefore="15591288" />
<immortal freebytesafter="15591288" />
<synchronousgcpriority value="11" />
</gc>
<gc type="trigger start" id="1" timestamp="Tue Mar 13 15:17:45 2007" intervalms="0.000" />
<gc type="heartbeat" id="1" timestamp="Tue Mar 13 15:17:46 2007" intervalms="1003.413">
<summary quantumcount="477">
<quantum minms="0.078" meanms="0.503" maxms="1.909" />
<heap minfree="262144000" meanfree="265312260" maxfree="268386304" />
<immortal minfree="14570208" meanfree="14570208" maxfree="14570208" />
<gcthreadpriority max="11" min="11" />
</summary>
</gc>
<gc type="heartbeat" id="2" timestamp="Tue Mar 13 15:17:47 2007" intervalms="677.316">
<summary quantumcount="363">
<quantum minms="0.024" meanms="0.474" maxms="1.473" />
<heap minfree="261767168" meanfree="325154155" maxfree="433242112" />
<immortal minfree="14570208" meanfree="14530069" maxfree="14570208" />
<gcthreadpriority max="11" min="11" />
</summary>
</gc>
<gc type="trigger end" id="1" timestamp="Tue Mar 13 15:17:47 2007" intervalms="1682.816"/>
</verbosegc>
|
Cada evento da Verbose GC está contido entre tags <gc></gc> . Vários tipos de eventos estão disponíveis, mas os mais comuns estão incluídos na Listagem 1. Um tipo synchgc representa uma GC síncrona, que é um ciclo da GC que foi executado ininterruptamente do início ao fim; ou seja, não ocorreu intercalação com o aplicativo. Isso pode ocorrer por duas razões:
-
System.gc()foi chamado pelo aplicativo. - O heap ficou cheio e o aplicativo não alocou memória.
A razão para a GC síncrona, contida na tag <details> , consiste em system garbage collect no primeiro caso e out of memory no segundo. O primeiro caso não é conforme com a sustentabilidade do aplicativo com os parâmetros especificados. No entanto, chamar System.gc() do aplicativo de usuário faz com que a utilização do aplicativo caia para 0% em muitos casos e cause longos tempos de pausa; portanto, deve ser evitado. Mas se uma GC síncrona ocorrer devido ao segundo caso -- um erro de falta de memória -- isso significa que a GC não conseguiu acompanhar a alocação do aplicativo. Portanto, você deve considerar o aumento de heap ou a redução da utilização alvo do aplicativo para evitar a ocorrência de GCs síncronas.
Tipos de eventos de GC trigger correspondem aos pontos de início e término do ciclo da GC. Eles são úteis para delimitar lotes de eventos heartbeat da GC. Os tipos de eventos heartbeat da GC reúnem as informações de diversos quanta de GC em um evento detalhado resumido. Observe que isso não tem qualquer relação com a pulsação do encadeamento de alarme. O atributo quantumcount corresponde à quantia de quanta de GC reunida na GC de heartbeat . A tag <quantum> representa as informações de sincronização sobre os quantas de GC reunidos na GC de heartbeat . As tags <heap> e <immortal> contêm informações sobre a memória livre ao final dos quantas reunidos na GC de heartbeat . A tag <gcthreadpriority> contém informações sobre a prioridade do encadeamento da GC quando os quantas foram iniciados.
Os valores de tempo de quantum correspondem aos tempos de pausa observados pelo aplicativo. O tempo médio de quantum deve estar próximo de 500 microssegundos e os tempos máximos de quantum devem ser monitorados para assegurar que estejam entre os tempos de pausa aceitáveis para o aplicativo RT. Tempos de pausa longos podem ocorrer quando a GC for apropriada por outros processos do sistema, evitando que conclua seus quantas e permitindo que o aplicativo continue, ou quando determinadas estruturas raízes no sistema são abusadas e crescem a tamanhos não gerenciáveis (consulte Problemas a considerar ao usar o Metronome).
Memória imortal é um recurso necessário para RTSJ que não está sujeita à GC. Por essa razão, é normal ver a memória imortal livre no log detalhado da GC cair sem jamais se recuperar. Ela é usada para objetos como constantes de cadeias de caracteres e classes. É necessário estar cliente do comportamento de seu programa e ajustar o tamanho da memória imortal de forma apropriada.
Você deve monitorar o uso de heap para assegurar que a tendência geral permaneça estável. Uma tendência para baixo no espaço livre de heap indica a existência de uma fuga em potencial causada pelo aplicativo. Diversas condições podem causar fugas, inclusive hashtables de expansão contínua, retenção indefinidamente de grandes objetos de recursos e a falta de limpeza de referências de JNI globais.
As Figuras 9 e 10 ilustram tendências estáveis e para baixo no espaço de heap livre. Observe que os mínimos e máximos locais são normais e esperados, porque o espaço livre aumenta somente durante um ciclo de GC e diminui de forma correspondente quando o aplicativo está ativo e em alocação.
Figura 9. Heap livre estável
Figura 10. Heap livre decrescente
Na tag <gc> , o atributo interval corresponde ao tempo decorrido desde a saída do último evento de GC detalhada do mesmo tipo. No caso do tipo de evento heartbeat , pode representar o tempo desde o evento trigger start se for a primeira pulsação para o ciclo de GC atual.
Tuning Fork é uma ferramenta separada para ajustar o Metronome para se adequar melhor ao aplicativo de usuário. O Tuning Fork permite que o usuário inspecione muitos detalhes da GC após o fato por meio de um log de rastreio ou durante o tempo de execução por meio de um soquete. O Metronome foi desenvolvido com o Tuning Fork em mente e registra muitos eventos em log que podem ser inspecionados a partir do aplicativo Tuning Fork. Por exemplo, exibe a utilização do aplicativo ao longo do tempo e inspeciona o tempo que leva para várias fases da GC.
A Figura 11 mostra o gráfico de resumo do desempenho da GC gerado pelo Tuning Fork, incluindo utilização alvo, uso de memória de heap e utilização do aplicativo:
Figura 11. Resumo de desempenho do Tuning Fork
Problemas a considerar ao usar o Metronome
O Metronome busca fornecer pausas deterministas curtas para a GC, mas surgem algumas situações no código do aplicativo e na plataforma subjacente que podem perturbar esses resultados, às vezes levando a valores de tempo de pausa fora do padrão. Mudanças no comportamento da GC do que seria esperado com um coletor JDK padrão também podem ocorrer.
RTSJ determina que a GC não processe memória imortal. Como classes vivem na memória imortal, elas são estão sujeitas à GC e, portanto, não podem ser descarregadas. Os aplicativos que esperam usar um grande número de classes precisam ajustar o espaço imortal de forma apropriada e os aplicativos que requerem o descarregamento de classes precisam fazer ajustes a seu modelo de programação dentro do WebSphere Real Time.
O trabalho da GC no Metronome é baseado em tempo e qualquer mudança no relógio do hardware pode causar problemas de difícil diagnóstico. Um exemplo é sincronizar a hora do sistema a um servidor de Network Time Protocol (NTP) e, em seguida, sincronizar o relógio do hardware ao tempo do sistema. Isso pode parecer como um salto repentino no tempo para a GC e pode causar uma falha na manutenção da meta de utilização ou, possivelmente, causar erros de falta de memória.
Executar diversas JVMs em uma única máquina pode apresentar interferência entre as JVMs, distorcendo os números de utilização. O encadeamento de alarme, sendo um encadeamento RT de alta prioridade, apropria-se de qualquer outro encadeamento de baixa prioridade e o encadeamento da GC também é executado a uma prioridade RT. Se encadeamentos suficientes da GC e de alarme estiverem ativos em qualquer momento, uma JVM sem um ciclo de GC ativo pode ter seus encadeamentos de aplicativo apropriados por encadeamentos da GC e de alarme de outra JVM enquanto o tempo é na verdade taxado ao aplicativo, pois a GC para essa VM está inativa.
Aprender
-
Série Java em Tempo Real : Leia as outras partes desta série.
- "A real-time garbage collector with low overhead and consistent utilization" (David F. Bacon, Perry Cheng e V.T. Rajan, Proceedings of the 30th Annual ACM SIGPLAN/SIGACT Symposium on Principles of Programming Languages, 2003): Esse trabalho apresenta um coletor de desfragmentação dinâmica que supera as limitações da aplicação da GC a sistemas RT rígidos.
-
JSR 1: Real-time Specification for Java: Você encontrará RTSJ no site Java Community Process.
- "IBM WebSphere Real Time V1.0 delivers predictable response times using Java standards": Leia o anúncio de produto para o WebSphere Real Time.
-
Metronome: Saiba sobre o Metronome, a tecnologia de GC incorporada ao WebSphere Real Time.
- "Real-time garbage collection on general-purpose machines" (T. Yuasa, Journal of Systems and Software, março de 1990): Informações adicionais sobre barreiras Yuasa.
- "High-level
Real-time
Programming in Java": Leia sobre o protótipo de pesquisa Staccato.
-
Zona de tecnologia Java do developerWorks: Centenas de artigos sobre cada aspecto da programação Java.
Obter produtos e tecnologias
-
WebSphere Real Time: O WebSphere Real Time permite que aplicativos dependentes de tempos de resposta precisos aproveitem a tecnologia Java padrão sem sacrificarem determinismo.
-
Real-time Java technology: Visite o site de pesquisa IBM alphaWorks dos autores para encontrar tecnologias de ponta para RT Java.
Discutir
- Confira blogs do developerWorks e participe da comunidade comunidade do developerWorks.

Ben Biron trabalha na equipe J9 Virtual Machine desde maio de 2006, quando recebeu seu título de Bacharel em Engenharia em Sistemas de Computadores na Universidade de Carleton. Seu foco principal é em Metronome e em coleta de lixo em tempo real. Fora do trabalho, Ben gosta de voleibol, hóquei, golfe e desenvolvimento de software livre.

Desde que recebeu seu BCS na Universidade de Carleton em 1997, Ryan Sciampacone está envolvido em todos os aspectos de desenvolvimento de máquina virtual, incluindo implementação de VM principal, camada de API JNI e compilação Ahead-of-time. Desde 2002, ele é o líder técnico e arquiteto chefe de coleta de lixo para a máquina virtual J9. Ele é responsável pelo conjunto coletor escalável disponível na implementação de JSE, assim como pelo coletor Metronome e os coletores de configuração ME. Quando não está usando seu chapéu técnico, Ryan gosta de jogar hóquei, praticar ioga e ciclismo.