Avançar para a área de conteúdo

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

A primeira vez que acessar o developerWorks, um perfil será criado para você. Informações do seu perfil (tais como: nome, país / região, e empresa) estarão disponíveis ao público, que poderá acompanhar qualquer conteúdo que você publicar. Seu perfil no developerWorks pode ser atualizado a qualquer momento.

Todas as informações enviadas são seguras.

  • Fechar [x]

Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

Todas as informações enviadas são seguras.

  • Fechar [x]

Java em Tempo Real, Parte 5: Composição e Implementação de Aplicativos Java em Tempo Real

Exemplos, sugestões e dicas

Andrew Hall, Software Engineer, IBM
Andrew Hall
Andrew Hall entrou para o Centro de Tecnologia Java da IBM em 2004, iniciando na equipe de teste do sistema onde trabalhou por dois anos. Ele ficou 18 meses na equipe de serviço Java, onde depurou dezenas de problemas de memória nativa em várias plataformas. Atualmente ele é o desenvolvedor da equipe de Confiabilidade, Disponibilidade e Capacidade de Manutenção Java. Nas horas vagas ele se dedica à leitura, fotografia e diversões.
Caroline Gough , Software Engineer, IBM Hursley Lab
Caroline Gough
Caroline Gough trabalhou como desenvolvedora em uma pequena cada de software por três anos antes de passar a fazer parte da equipe Java Technology Centre System Test no Laboratório da IBM Hursley. Ela é testadora senior com conhecimento em teste de tensão e conjunto de ferramentas RAS (confiabilidade, disponibilidade e capacidade de manutenção). Ela trabalhou no IBM WebSphere Real Time V1.0 e agora está preparando testes para releases futuros da plataforma Java.
Alan Stevens, Software Engineer, IBM Hursley Lab
Alan Stevens
Helen Masters se formou em 1995 na Universidade de Nottingham e passou a fazer parte da organização IBM Global Services em 1996 para trabalhar em desenvolvimento de software em um grande contrato de defesa. Ela foi transferida para o Laboratório da IBM Hursley em 2000, onde teve diversos cargos de liderança em sua área de conhecimento técnico: teste. Helen atualmente é responsável por liderar a equipe de esforço de teste no IBM WebSphere Real Time V1.0.
Helen Masters, Software Engineer, IBM Hursley Lab
Helen Masters
Helen Masters se formou em 1995 na Universidade de Nottingham e passou a fazer parte da organização IBM Global Services em 1996 para trabalhar em desenvolvimento de software em um grande contrato de defesa. Ela foi transferida para o Laboratório da IBM Hursley em 2000, onde teve diversos cargos de liderança em sua área de conhecimento técnico: teste. Helen atualmente é responsável por liderar a equipe de esforço de teste no IBM WebSphere Real Time V1.0.

Resumo:  Este artigo, o quinto de uma série em seis partes sobre Java™ em tempo real, mostra como escrever e implementar aplicativos Java em tempo real usando as ferramentas fornecidas com o IBM® WebSphere® Real Time. Usando aplicativos de amostra, os autores demonstram o coletor de lixo Metronome para controlar pausas de coleta de lixo, o compilador Ahead-of-time para evitar pausas de compilação de tempo de execução e NoHeapRealtimeThreads para atender os requisitos de sincronização mais rígidos.

Visualizar mais conteúdo nesta série

Data:  16/Set/2011
Nível:  Intermediário Também disponível em :   Inglês
Atividade:  2682 visualizações
Comentários:  


Os artigos anteriores desta série descreve como o IBM WebSphere Real Time soluciona problemas de não determinismo até escalas de tempo muito baixas. Esse recurso estende a faixa e os benefícios da plataforma Java para áreas anteriormente reservadas a linguagens de programação em tempo real (RT) especializadas, como Ada. Hardware e sistemas operacionais RT são frequentemente customizados e enigmáticos. O WebSphere Real Time, por sua vez, é executando em uma versão RT do Linux® compatível com o IBM BladeCenter® LS20 (consulte Recursos) e hardware semelhante. Suporta a demanda de aplicativos RT típicos:

  • Baixa latência: Resposta garantida a sinais em tempo limitado.

  • Determinismo: Nenhuma pausa ilimitada de coleta de lixo (GC).

  • Capacidade de previsão: As prioridades de encadeamento regem a ordem de execução e os tempos de execução são consistentes.

  • Sem inversões de prioridade: Encadeamentos de alta prioridade não podem ser bloqueados por encadeamentos de baixa prioridade que retêm bloqueios necessários, pois encadeamentos de média prioridade estão em execução.

  • Acesso à memória física: Aplicativos RT, como drivers de dispositivos, para chegar à base.

Este artigo mostra como escrever e implementar aplicativo Java RT usando as ferramentas fornecidas com o WebSphere Real Time. Ele faz referência aos artigos anteriores da série à medida que mostra como obter programas executando com um grau crescente de determinismo RT. (Seria útil, mas não essencial, ler os artigos anteriores.) Você verá como é possível usar uma política de GC RT, como o Metronome, para melhorar a capacidade de previsão no aplicativo de amostra do módulo de Pouso Lunar fornecido com o WebSphere Real Time. Aprenderá também como compilar Ahead-of-time (AOT) seu aplicativo para melhor determinismo em um ambiente RT. Por fim, você irá projetar e implementar um aplicativo RT usando memória que não é controlada pelo coletor de lixo e irá descobrir dicas e truques para obter o máximo de seu aplicativo Java RT.

Se quiser executar alguns dos programas descritos neste artigo -- ou, melhor ainda, escrever seu próprio aplicativo Java RT -- é necessário ter acesso a um sistema com o WebSphere Real Time instalado (consulte Recursos para obter detalhes sobre como obter essa tecnologia).

Benefícios do coletor de lixo Metronome

Metronome é o coletor de lixo do WebSphere Real Time. É possível ver seus benefícios a começar com o aplicativo de amostra fornecido com o WebSphere Real Time. Após instalar o WebSphere Real Time, é possível localizá-lo em diretório de instalação/sdk/demo/realtime/sample_application.zip.

O aplicativo de amostra simula a tecnologia de controle para um módulo de Pouso Lunar não tripulado. Para obter um pouso seguro, os propulsores do foguete do módulo de Pouso devem ser implementados de forma precisa:

  • Os propulsores verticais para reduzir a taxa de queda.
  • Os propulsores horizontais para alinhar com o local do pouso.

Para calcular a posição do módulo de Pouso, o Controlador usa o tempo que leva para retorno dos pulsos do radar. A Figura 1 ilustra a simulação:


Figura 1. O módulo de Pouso Lunar

Se ocorrer qualquer atraso no retorno do sinal -- por exemplo, devido a uma pausa de GC -- a posição do módulo de Pouso é calculada incorretamente. Como um tempo mais longo para o retorno do pulso do radar implicaria em uma distância maior, o Controlador faria então os ajustes com base em uma posição estimada incorretamente. Claramente, isso levaria a consequências desastrosas para o módulo de Pouso ou para qualquer sistema RT.

Uma maneira de mostrar como Java padrão é inadequado para executar aplicativos RT é medir com que precisão o Controlador mantém o módulo de Pouso em sua trajetória correta e qual é o seu sucesso de pousos. O gráfico na Figura 2 mostra uma simulação do Controlador usando Java VM padrão. A linha vermelha mostra a posição real do módulo de Pouso, a linha azul a posição medida pelo radar.


Figura 2. Simulação do Controlador usando a Java VM padrão

Apesar de esse voo ter terminado em um pouco bem-sucedido, o gráfico na Figura 2 mostra diversos picos grandes (a linha azul) em altura medida pelo radar. Esses correspondem a pausas de GC. Em algumas execuções, as pausas de GC causam erros grandes o suficiente em medida de posição que podem causas acidentes devido a velocidade excessiva no pouso (erro de posição vertical) ou erro no local do pouso (erro de posição horizontal). Esse comportamento de tempo de execução não determinista ilustra uma das principais razões para plataformas Java padrão não terem sido usadas para aplicativos RT.

Real-time Specification for Java (RTSJ) fornece várias soluções para o problema de pausas de GC. Reconhece a importância de gerenciamento de memória automática para os programadores de Java, mas também apresenta novas áreas de memória para evitar efeitos de GC que requerem que programadores reassumam o controle da memória. Conforme mostrado na seção sobre NoHeapRealtimeThreads, isso eleva o nível de diversas maneiras desafiadoras para escrever aplicativos Java confiáveis. Uma abordagem alternativa, adequada para muitos aplicativos RT que podem tolerar pausas muito curtas, é usar um coletor de lixo RT, como o Metronome no WebSphere Real Time.

Executar o aplicativo de Pouso Lunar com o Metronome produz um gráfico que controla a posição verdadeira do módulo de Pouso muito mais de perto, sem picos significativos na medida de altura e um pouso seguro toda vez (consulte a Figura 3):


Figura 3. Simulação do Controlador usando o WebSphere Real Time

Nessa segunda execução, o código Java do Controlador ficou inalterado; é um aplicativo J2SE normal que se beneficia de um coletor de lixo RT.

É possível incluir o parâmetro -verbose:gc na chama da amostra para mostrar detalhes adicionais das pausas de GC reduzidas, como na saída a seguir:


<gc type="heartbeat" id="2" timestamp="Tue Apr 24 04:00:58 2007" intervalms="1002.940">
  <summary quantumcount="171">
    <quantum minms="0.006" meanms="0.470" maxms="0.656" />
    <heap minfree="142311424" meanfree="171371274" maxfree="264060928" />
    <immortal minfree="15964488" meanfree="15969577" maxfree="15969820" />
  </summary>
</gc>

Essa sub-rotina de exemplo relata a atividade de GC durante um intervalo de 1 segundo de uma execução da demo. Aqui mostra que a GC foi executada 171 vezes (quantumcount) e que o tempo médio de pausa que o aplicativo recebeu dessas pausas incrementais de GC (meanms) foi 0,470 milissegundos.

Para uma visualização ainda mais detalhada da intercalação de trabalho do aplicativo e pausas de GC, é possível registrar um arquivo de rastreio do Metronome e visualizá-lo com a ferramenta de análise TuningFork (consulte Recursos), conforme mostrado na Figura 4:


Figura 4. Visualização de TuningFork da parte da demo

Quando as pausas de GC tiverem sido minimizadas, outros fatores que podem causar perturbações a um aplicativo em execução tornam-se mais aparentes. Um deles é a atividade do compilador Just-in-time (JIT). Compilar bytecodes Java para código nativo é essencial para bom desempenho, mas a ação de gerar o código nativo pode causas pausas. Uma solução para esse problema é pré-compilar bytecodes Java usando compilação AOT.


Compilação AOT para aplicativos

Tempos de execução Java geralmente usam um compilador JIT para gerar código nativo dinamicamente para os métodos mais frequentemente executados dentro de um aplicativo Java. Em um ambiente RT, alguns aplicativos, por exemplo, aqueles com prazos rígidos, podem não tolerar o não determinismo associado a atividades de compilação dinâmicas. Para outros, a sobrecarga de compilar os muitos métodos usados para iniciar um aplicativo complexo é indesejável. Desenvolvedores de aplicativos que enfrentam esses tipos de problemas podem se beneficiar do uso de compilação AOT.

Um erro comum é supor que código pré-compilado sempre melhora o desempenho de um aplicativo. Esse nem sempre é o caso, pois alternar entre código interpretado e pré-compilado pode ser dispendioso. Se partes do aplicativo forem pré-compiladas e outras não forem, o aplicativo pode ser executado mais lentamente do que se a compilação AOT não fosse usada. Por essa razão, você deve tomar cuidado ao escolher o que compilar AOT.

A compilação AOT envolve gerar código nativo para os métodos Java do aplicativo antes de o aplicativo ser executado. Isso permite que o usuário evite o não determinismo da compilação dinâmica, enquanto ainda obtém a maioria dos benefícios de desempenho associados à compilação nativa. É importante entender que geralmente executar código compilado AOT (também conhecido como pré-compilado) é um pouco mais lento do que executar com um compilador JIT dinâmico. Devido à sua natureza estática, o código pré-compilado -- diferentemente do código gerado dinamicamente por um compilador JIT -- não pode se beneficiar de otimizações adicionais de métodos frequentemente usados ao longo do tempo. O WebSphere Real Time atualmente não permite a combinação de compilação JIT dinâmica e de código pré-compilado. Em suma, a compilação AOT pode fornecer um desempenho de tempo de execução mais determinista com um impacto de tempo de execução mais lento, porque a compilação dinâmica não ocorre, enquanto mantém a conformidade Java suportando resolução dinâmica.

Leia "Java em Tempo Real, Parte 2: Comparando Técnicas de Compilação" para obter informações adicionais sobre as técnicas que o compilador JIT usa para executar otimizações, as vantagens e as desvantagens de compiladores JIT e AOT e uma comparação dos dois.

Gerando código pré-compilado

A ferramenta de compilação AOT, jxeinajar, gera código nativo de classes armazenadas em formatos de arquivo JAR ou ZIP. A ferramenta pode criar código compilado AOT para todos os métodos em cada uma das classes do arquivo JAR ou para uma seleção de métodos definida. O código compilado AOT é o equivalente ao código nativo que o compilador JIT geraria se usasse um nível de otimização fixo. O código é armazenado em um formato interno conhecido como Java eXEcutable (JXE). A ferramenta jxeinajar agrupa o arquivo JXE em um arquivo JAR, que o WebSphere Real Time pode então executar.

A opção -Xrealtime denota que você deseja executar o Java Runtime Environment RT. A ferramenta jxeinajar não funcionará se a opção -Xrealtime for omitida. Se essa opção não for especificada, você chamará o padrão IBM SDK and Runtime Environment for Linux Platforms, Java 2 Technology versão 5.0.

A compilação AOT é um processo em dois estágios. A primeira etapa, geração de código AOT (usando a ferramenta jxeinajar ), gera código nativo usando o compilador AOT. A segunda etapa é a execução desse código dentro do Java Runtime Environment (JRE).

O comando a seguir (em que aotJarPath é o diretório no qual você deseja que os arquivos pré-compilados sejam gravados) cria código compilado AOT para todos os arquivos JAR ou ZIP no diretório atual e supões que $JAVA_HOME/bin esteja em $PATH:

jxeinajar -Xrealtime -outPath aotJarPath
            

Após executar o comando, você verá a seguinte saída:

J9 Java(TM) jxeinajar 2.0
Licensed Materials - Property of IBM

(c) Copyright IBM Corp. 1991, 2006 All Rights Reserved
IBM is a registered trademark of IBM Corp.
Java and all Java-based marks and logos are trademarks or registered
trademarks of Sun Microsystems, Inc.

Searching for .jar files to convert
Found /home/rtjaxxon/demo.jar
Searching for .zip files to convert
Converting files
Converting /home/rtjaxxon/demo.jar into /home/rtjaxxon/aot///demo.jar
JVMJ2JX002I Precompiled 3098 of 3106 method(s) for target ia32-linux.
Succeeded to JXE jar file /home/rtjaxxon/demo.jar

Processing complete

Return code of 0 from jxeinajar

O arquivo JAR criado não é um JAR verdadeiro. Ele não contém arquivos de classe. Em vez disso, contém o arquivo JXE para todas as classes e arquivos de classe marcadores, que são usados para acessar o código nativo. Esses arquivos não podem ser usados com outras ferramentas de tempo de execução Java e são específicos da versão do WebSphere Real Time.

Os arquivos JAR ou ZIP individuais podem ser especificados na linha de comando para substituir comportamento padrão. Para estender a procura por arquivos de entrada para incluir subdiretórios, inclua a opção -recurse no comando.

Reconhecendo um arquivo pré-compilado

Os formatos de arquivos produzidos pela ferramenta jxeinajar contêm um arquivo JXE e o que são efetivamente ponteiros para os arquivos de classe individuais dentro do arquivo JXE. Listando o conteúdo de um arquivo JAR ou ZIP, é possível identificar rapidamente se a ferramenta jxeinajar gerou o arquivo. Se quiser inspecionar demo.jar, o comando para listar seu conteúdo é:

jar vtf demo.jar

Um arquivo JAR gerado por jxeinajar produz saída semelhante a seguinte:

0 Thu Apr 19 13:59:14 CDT 2006 META-INF/
71 Thu Apr 19 13:59:14 CDT 2006 META-INF/MANIFEST.MF
68 Thu Apr 19 13:59:14 CDT 2006 demo.class
4119 Thu Apr 19 13:59:14 CDT 2006 jxe22A6B69D-010D-1000-8001-810D22A6B69D.class

O arquivo JXE extra dentro do arquivo JAR identifica-o como um arquivo JAR que a ferramenta jxeinajar gerou. Caso contrário, a saída seria:

0 Thu Apr 19 09:00:01 CDT 2006 META-INF/
71 Thu Apr 19 09:00:01 CDT 2006 META-INF/MANIFEST.MF
846 Thu Apr 19 09:00:01 CDT 2006 demo.class

Executando código pré-compilado

Após ter compilado seu aplicativo AOT, é possível usar este comando para executá-lo:

java -Xrealtime -Xnojit -classpath aotJarPath AppName
            

Sempre verifique se quaisquer arquivos JAR de aplicativo pré-compilados estão listados primeiro no caminho de classe para assegurar que o código pré-compilado seja executado.

Lembre-se de que a compilação JIT dinâmica e a compilação AOT não podem ser combinadas quando o WebSphere Real Time é usado. Se você omitir a opção -Xnojit , qualquer código compilado AOT disponível para a Java VM não será usado. Em vez disso, o código é interpretado ou compilado dinamicamente por JIT. A opção -Xrealtime no comando ativa a Java VM RT. Se essa opção não for fornecida, então a Java VM SE enviada com o WebSphere Real Time será usada.

Quando o sinalizador -Xnojit está configurado, o WebSphere Real Time usa o interpretador para executar quaisquer métodos que não foram pré-compilados. Isso significa que se localizar versões do código do aplicativo que não foram pré-compiladas antes, nos arquivos JAR pré-compilados ou em outros arquivos JAR especificado no caminho de classe, esse código é executado somente a uma velocidade interpretada.

Compilando AOT JARs do sistema

Aconselhamos não apenas pré-compilar seu aplicativo, mas também compilar AOT arquivos JAR chaves do sistema. Qualquer aplicativo que esteja usando as APIs Java padrão é efetivamente compilado apenas parcialmente, a menos que os arquivos JAR do sistema também sejam compilados. A maioria das classes de API padrão é armazenada nos arquivos core.jar e vm.jar, portanto, recomendamos compilar AOT esses dois arquivos como um ponto de partida. Para aplicativos RT, você também deve pré-compilar realtime.jar. Além disso, a natureza de seu aplicativo determina quais arquivos do sistema adicionais podem beneficiar ainda mais o desempenho sendo pré-compilados.

O processo para compilar AOT arquivos JAR do sistema é idêntico à compilação AOT de qualquer outro arquivo JAR. No entanto, como os arquivos JAR do sistema são carregados a partir do caminho de classe de inicialização, você deve usar o comando a seguir para pré-anexar quaisquer arquivos JAR do sistema pré-compilados ao caminho de classe de boot para assegurar que sejam usados:

java -Xrealtime -Xnojit 
-Xbootclasspath/p:aotSystemJarPath/core.jar:aotSystemJarPath/vm.jar:
aotSystemJarPath/realtime.jar -classpath aotJarPath/realTimeApp.jar realTimeApp

O /p na opção -Xbootclasspath/p: pré-anexa os arquivos JAR do sistema pré-compilados ao caminho de classe de boot. O caminho de classe de boot também pode ser manipulado com as opções -Xbootclasspath: e -Xbootclasspath/a: (para configurar e anexar, respectivamente). No entanto, se você usar -Xbootclasspath: ou -Xbootclasspath/a: para colocar um arquivo JAR compilado AOT no caminho de classe de boot, as classes compiladas não serão usadas.

Confirmando que você selecionou JARs pré-compilados

Pode ser muito fácil cometer erros nos caminhos de classe, principalmente se seu aplicativo consistir em diversos arquivos JAR e você também estiver pré-compilando arquivos JAR do sistema. Erros levam a código não pré-compilado ser executado em vez de o código pré-compilado esperado. Uma combinação das opções a seguir pode ajudar a confirmar que as classes que estão sendo usadas estão pré-compiladas:

  • -verbose:relocations imprime as informações de relocação do código pré-compilado em STDERR. Uma mensagem de log é impressa toda vez que um método pré-compilado for executado. A saída dessa opção é semelhante à seguinte:

    Relocation: realTimeApp.main([Ljava/lang/String;)V <B7F42A30-B7F42B28> Time: 10 usec

  • -verbose:class grava a mensagem em STDERR para cada classe à medida que é carregada. A opção produz saída semelhante à seguinte:

    class load: java/lang/Object
    class load: java/lang/J9VMInternals
    class load: java/io/Serializable
    class load: java/lang/reflect/GenericDeclaration
    class load: java/lang/reflect/Type
    class load: java/lang/reflect/AnnotatedElement

  • -verbose:dynload fornece informações detalhadas sobre cada classe carregada pela Java VM. As informações incluem o nome de classe, seu pacote e o local do arquivo de classe. O formato dessas informações é semelhante ao seguinte:

    <Loaded java/lang/String from /myjdk/sdk/jre/lib/vm.jar>
    <Class size 17258; ROM size 21080; debug size 0>
    <Read time 27368 usec; Load time 782 usec; Translate time 927 usec>



    Infelizmente, essa opção não lista classes de arquivos JAR pré-compilados. No entanto, se você combiná-la à opção -verbose:class , será possível inferir quais classes estão pré-compiladas devido à ausência das mesmas. Qualquer classe listada na saída -verbose:class , mas não na saída -verbose:dynload deve ser carregada de um arquivo JAR pré-compilado. A opção detalhada necessária é -verbose:class,dynload.

Compilação AOT direcionada por perfil

É possível desenvolver um conjunto mais otimizado de arquivos JAR pré-compilados criando um perfil dos métodos que seu aplicativo usa frequentemente e compilar AOT somente esses métodos.

É possível criar esse perfil dinamicamente executando seu aplicativo com a opção -Xjit:verbose={precompile},vlog=optFileName (em que optFileName é o nome do arquivo que está listando os métodos que você deseja ter pré-compilados):

java -Xjit:verbose={precompile},vlog=optFileName -classpath appJarPath realTimeApp

Essa opção gera um arquivo de opções que contém uma lista das assinaturas de métodos correspondentes aos métodos que o compilador JIT compilou durante a execução do aplicativo. É possível editar esse arquivo com um editor de texto se necessário. Em seguida, é possível fornecer o arquivo à ferramenta jxeinajar para controlar quais métodos são pré-compilados. Você fornece o arquivo à ferramenta com o comando a seguir:

jxeinajar -Xrealtime -outPath aotJarPath-optFile optFileName

O Centro de Informações fornecido com o WebSphere Real Time também discute a compilação AOT com perfil (consulte Recursos para obter um link para o Centro de Informações on-line). Ele o guia pela geração de um perfil de tempo de execução para o módulo de Pouso Lunar discutido na seção anterior e como usar esse perfil para pré-compilar o aplicativo de Pouso Lunar e os arquivos JAR do sistema de forma seletiva. Como alternativa, se quiser experimentar pré-compilar um aplicativo diferente, também é possível usar o aplicativo de Fábrica de Doces discutido na próxima seção.


Usando NoHeapRealtimeThreads

O WebSphere Real Time contém uma implementação completa de RTSJ. RTSJ foi projetada antes de coletores de lixo RT, como o Metronome, estarem disponíveis e contém meios alternativos para atingir desempenho previsível de baixa latência de um tempo de execução Java.

Quando RTSJ foi escrita, os dois maiores obstáculos para execução previsível em um tempo de execução Java eram o compilador JIT e o coletor de lixo. Cada uma dessas tecnologias usa tempo de processador fora do controle do programador do aplicativo. A natureza dinâmica dos mesmos significa que ambas as tecnologias apresentam atrasos imprevisíveis em um aplicativo Java. Em alguns casos, esses atrasos podem levar vários segundos, o que pode ser inaceitável para muitos sistemas RT.

O compilador JIT pode simplesmente ser desativado ou substituído por outra tecnologia, como compilação AOT, mas a GC não pode ser tão facilmente desativada. Antes de pode ser removida, uma solução de gerenciamento de memória alternativa deve ser fornecida.

Para suportar sistemas RT que possam tolerar atrasos resultantes de um coletor de lixo padrão, RTSJ define áreas de memória imortal e com escopo definido para suplementar o heap Java padrão. RTSJ também inclui suporte para duas novas classes de encadeamento -- RealtimeThread e NoHeapRealtimeThread (NHRT) -- que permitem que os programadores de aplicativos aproveitem outros recursos RT, inclusive o uso de áreas de memória além do heap.

NHRTs são encadeamentos que não podem funcionar com qualquer objeto criado no heap Java. Isso permite que sejam executados independentemente do coletor de lixo para atingir execução previsível de baixa latência. NHRTs deve criar seus objetos usando memória com escopo definido ou imortal. Isso requer um estilo de programação muito diferente daquele usado na programação Java baseada em heap padrão.

Agora, você irá desenvolver um aplicativo simples usando NHRTs para demonstrar alguns dos desafios de programação exclusivos associados ao uso de memória não heap.

Cenário de exemplo

Vamos implementar um sistema de automação para uma fábrica de doces. Na fábrica há diversas linhas de produção que transformam ingredientes brutos em diferentes tipos de doces e, em seguida, carregam os mesmos em potes. O sistema será projetado para detectar os potes nos quais foram colocados muitos doces ou poucos doces e notificará um trabalhador da fábrica para retirar os potes preenchidos incorretamente.

Após os potes serem preenchidos, eles são pesados para verificar o número de doces que foram carregados em cada pote. Se o número de doces em um pote estiver 2 por cento fora da meta, uma mensagem deve ser enviada a uma tela de controle do trabalhador da fábrica para notificar o trabalhador sobre o problema. O trabalhador usa o ID do pote exibido no painel de controle para localizar o pote e removê-lo da fila de embalagem, em seguida, confirma no painel de controle que ele foi removido. A massa de cada pote deve ser gravada em um arquivo de log para propósitos de auditoria.

A Figura 5 mostra um diagrama do cenário de exemplo:


Figura 5. O cenário da fábrica de doces

Obviamente, este exemplo é uma invenção, mas ajuda a explorar os desafios de criar um aplicativo NHRT e, principalmente, compartilhar dados entre NHRTs e outros tipos de encadeamentos.

Interfaces externas

O sistema deve lidar com três classes de entidades externas: as máquinas de pesagem nas linhas de produção, o console do trabalhador e o log de auditoria. A linha de produção e o console do trabalhador estão contidos em interfaces Java com as quais o sistema é fornecido.

A interface para a máquina de pesagem tem um método -- weighJarGrams() --- que bloqueia até o próximo pote passar sobre as balanças e retorna a massa desse pote em gramas. A taxa pela qual os potes chegam é variável, mas para obter a melhor taxa de produção possível, pode ser tão pequena quanto um pote a cada 10 milissegundos. Se o método weighJarGrams() não for pesquisado com frequência suficiente, os potes serão ignorados.

A máquina de pesagem é um componente de uma linha de produção, que possuiu métodos para consultar o tipo de doce que está sendo produzido e o tamanho dos potes que estão sendo preenchidos.

O console do trabalhador tem dois métodos -- jarOverfilled() e jarUnderfilled() -- ambos os quais usam um ID de pote. Esses métodos bloqueiam até o trabalhador confirmar a mensagem (que pode levar vários segundos).

Vamos implementar a interface MonitoringSystem , que tem dois métodos: startMonitoring() e stopMonitoring(). O método startMonitoring() aceita os objetos ProductionLine e o objeto WorkerConsole que precisamos para comunicação como argumentos.

O log de auditoria é especificado como um arquivo simples chamado audit.log no qual cada linha é uma cadeia de caractere separada por vírgula com o formato timestamp,jar id,sweet type code, jar size code,mass of jar.

A Figura 6 é um diagrama de classes UML que mostra essas interfaces:


Figura 6. Diagrama de classes UML para as interfaces

Projetando a solução

Agora que temos uma especificação, podemos projetar nossa solução. O problema pode ser dividido em duas partes: primeiro, pesquisas as linhas de produção e verificar as massas dos potes e, segundo, gravar o log de auditoria.

Pesquisando as linhas de produção

Se considerarmos a interface WeighingMachine , o método weighJar() precisa ser pesquisado frequentemente, portanto, é sensato ter um encadeamento dedicado para cada ProductionLine para tornar o design escalável. Vamos usar um NHRT para minimizar a possibilidade de o encadeamento de pesquisas ser interrompido pelo coletor de lixo e perder medidas.

Após pesar um pote, precisamos calcular a quantos doces a massa é equivalente e comparar isso ao valor alvo. Prever a quantia de processamento adicional necessário para uma medida é difícil; se o número de doces em um pote estiver fora da tolerância, devemos nos comunicar com o WorkerConsole, o que pode levar vários segundos.

Os potes podem estar chegando a cada 10 milissegundos, portanto, está claro que não podemos fazer os cálculos no encadeamento de pesquisas. Precisamos passar as medidas a um encadeamento de cálculo separado. Como algum processamento pode levar muito tempo, precisamos de diversos encadeamentos de processamentos por linha de produção para assegurar que um encadeamento sempre esteja disponível para trabalhar na medida mais recente.

Poderíamos efetuar spawn de um novo encadeamento para cada dado produzido, mas isso gastaria muito tempo de processador para iniciar e parar encadeamentos. Para fazer um melhor uso do tempo de CPU, podemos criar um conjunto de NHRTs para processar os dados. Mantendo um conjunto de encadeamentos em execução, não temos sobrecarga de inicialização e de encerramento ao executar.

Poderíamos ter um único conjunto de encadeamentos compartilhado por todas as linhas de produção, mas qualquer estrutura de dados acessada por diversos encadeamentos requer sincronização. Ter um único conjunto de encadeamentos poderia causar contenção de bloqueio significativa. Para tornar nossa solução escalável, cada linha de produção terá seu próprio conjunto de encadeamento pequeno anexado.

Projetar conjuntos de encadeamentos envolve muitas considerações, como dimensionamento do conjunto e técnicas de gerenciamento que estão fora do escopo deste artigo. Para os propósitos deste artigo, vamos criar 10 encadeamentos de conjunto por objeto ProductionLine e expandir o conjunto se ficarmos sem encadeamentos por qualquer razão.

Gravando o log de auditoria

Diferentemente de outros componentes de nosso sistema, o componente de criação de log de auditoria não é crítico com relação ao tempo. Se (inocentemente) negligenciarmos a possibilidade de o computador travar ou ser desligado, a única consideração importante é que medidas são registradas em log em algum momento.

Com isso em mente, vamos usar um único java.lang.Thread para gravar no arquivo de log. Ele conclui seu trabalho quando os NHRTs estão esperando mais trabalho e o coletor de lixo está inativo. Essa decisão de design tem muitas implicações, pois acabamos de apresentar uma interface entre Java baseado em heap tradicional e o ambiente não heap de NHRTs. Como verá posteriormente, é necessário tomar cuidado ao lidar com essa interface.

A Figura 7 é um diagrama de alto nível da arquitetura:


Figura 7. Diagrama de alto nível da arquitetura

A regra de pai único

É possível projetar arquiteturas para compartilhar dados entre encadeamentos com base em compartilhamento de escopos. No entanto, isso é difícil e frequentemente contra intuitivo para escrever, principalmente devido à regra de pai único. A regra de pai único determina que um escopo pode ter somente um pai exclusivo.

Para entender por que a regra de pai único é necessária, considere que os encadeamentos RT podem entrar em diversas áreas de memória, uma após a outra, criando uma pilha de áreas de memória. Em algumas dessas áreas de memória, você está em uma situação em que alguma memória será coletada muito em breve e alguns objetos (aqueles na imortal) nunca serão coletados.

RTSJ precisou definir regras que impeçam que programadores de aplicativos criem uma arquitetura na qual objetos podem ser liberados em momentos inesperados e desapareçam misteriosamente. No entanto, essas regras podem ser difíceis de entender e frustrantes para programar. A regra de pai único não é a única regra de acesso à memória sobre a qual um programador de Java RT deve se preocupar, mas é a regra que torna o compartilhamento de escopo entre encadeamentos difícil.

A memória com escopo definido em um conceito de um escopo pai. Ao entrar em um escopo, o escopo ingressado mais recentemente, mas do qual ainda não saiu (o próximo escopo na pilha da área de memória para o encadeamento atual) torna-se o pai do escopo que acaba de ser ingressado. Se esse for o primeiro escopo na pilha da área de memória para um encadeamento, o pai do escopo é o escopo primordial -- um escopo lógico em vez de algum lugar no qual é possível realmente criar objetos. Se um escopo não estiver sendo usado, ele não tem nenhum pai.

O pai único torna o compartilhamento de escopos entre encadeamentos difíceis, pois você é forçado a controlar cuidadosamente a sequência na qual áreas de memória são ingressadas em diversos encadeamentos. De forma efetiva, você está tentando alinhar dados por encadeamento (a pilha da área de memória de encadeamentos) a dados compartilhados de encadeamento (o campo pai para o escopo). O exemplo mais óbvio de uma operação ilegal seria iniciar dois encadeamentos, cada um com um escopo diferente como sua área de memória inicial e fazer com que cada encadeamento tentasse entrar em um escopo compartilhado para trocar alguns dados. Isso criaria dois pais para o escopo único e não é permitido.

A pilha da área de memória para um encadeamento é na verdade uma pilha cactos; é possível regressar pela pilha sem sair das áreas de memória e, em seguida, criar uma ramificação mais abaixo. Criar e manter estruturas de pilha de área de memórias complexas é difícil e não deve ser tentado sem uma boa razão. No entanto, com uso cuidadoso da pilha de área de memória, é possível compartilhar escopos em muitas situações.

As maneiras mais fáceis de compartilhar escopos entre dois encadeamentos são:

  • Iniciar ambos os encadeamentos com o escopo compartilhado como área de memória inicial. O pai para o escopo será o escopo primordial.

  • Inicie ambos os encadeamentos na memória imortal e entre no escopo por lá. Como esse será o primeiro escopo ingressado por cada encadeamento, o pai para o escopo será o escopo primordial e você não terá violado a regra de pai único.

Agora que você obteve a sensação do que deseja do aplicativo NHRT faça, o próximo desafio é determinar em quais áreas de memória o sistema irá fazer isso.

Memória não heap em Java RT

Antes de ser possível aplicar memória com escopo definido e imortal a seu design, é necessário entender um pouco mais sobre como funcionam.

Objetos criados em memória imortal nunca são limpos e ficam ativos ao longo do tempo de vida do aplicativo. Mesmo ao concluir o uso de um objeto, ele continua a usar espaço que não pode ser reclamado. Isso obviamente sobrecarrega os programadores com a responsabilidade de controlar quaisquer objetos criados na imortal e evitar a continuidade da criação de objetos ao longo do tempo. Fugas de memória imortal são uma fonte de erros comum em aplicativos Java RT.

Objetos criados na memória com escopo definido ficam ativos ao longo do tempo de vida do escopo no qual são criados. Cada área de memória com escopo definido tem uma contagem de referência; quando um encadeamento entra em uma área de memória com escopo definido, a contagem de referência é incrementada, e, ao sair, a contagem de referência é decrescida. Quando a contagem de referência atinge zero, os objetos dentro do escopo são liberados. As áreas de memória com escopo definido têm um tamanho máximo que é especificado quando elas são criadas e deve ser sintonizado à tarefa para a qual estão sendo usadas. Designers de aplicativos RT geralmente associam escopos a tarefas específicas para que possam ser sintonizados de forma efetiva. Escopos são pouco adequados para tarefas que usam uma quantia de imprevisível de memória, pois o tamanho de um escopo é fixo e deve ser pré-declarado.

Arquitetura da memória para a demo de Fábrica de Doces

Agora que sabemos um pouco sobre a memória não heap, podemos aplicá-la ao sistema projetado anteriormente.

De uma perspectiva de memória, o sistema de auditoria é fácil. Ele é executado em um java.lang.Thread na memória heap. Independentemente de se você usa um encadeamento Java padrão ou um encadeamento RT baseado em heap, é sensato realizar manipulação de cadeia de caractere e E/S na memória gerenciada pelo coletor de lixo, pois essas operações podem usar muita memória de maneiras surpreendentes.

O restante dos encadeamentos em nosso sistema são NHRTs e -- por definição -- não podem usar o heap Java para alocar objetos. Nossa opção está restrita a alguma combinação de memória com escopo definido e imortal.

Todos os encadeamentos têm uma área de memória inicial que é usada para o tempo de vida do encadeamento. Em nosso design, nossos NHRTs são de longa execução, portanto, independentemente do que escolhermos para nossa área de memória inicial, não devemos continuar a usar nenhuma memória da mesma após a inicialização inicial, pois -- independentemente de se usamos uma com escopo definido ou imortal -- a memória nunca seria limpa e eventualmente ficaríamos sem ela.

A área de memória atual é consumida somente por alocações de objetos, portanto, uma abordagem ao gerenciamento de memória é usar somente um número fixo de objetos ou evitá-los de uma vez por todas. Ao usar valores primitivos na pilha, podemos realizar trabalho sem usar a área de memória atual. (A pilha é a seção da memória que retém argumentos de função e campos usados nos métodos. É separada do heap Java e da memória imortal ou com escopo definido, mas não pode reter objetos -- somente valores primitivos ou referências de objetos.)

No entanto, a linguagem Java e suas bibliotecas de classe incentivam o uso de objetos para realizar seus objetivos. Portanto, para este exemplo, vamos supor que as operações que nossos NHRTs precisam executar criam alguns objetos e usam alguma memória toda vez que são executadas.

Neste cenário -- no qual o sistema deve ter um perfil de memória simples para uma duração de tempo grande, mas não especificada, mas ainda assim criar objetos -- a melhor abordagem é iniciar os encadeamentos na memória imortal e designar áreas de memória com escopo definido a tarefas finitas específicas.

Enquanto um encadeamento está em execução, sempre que precisar executar uma tarefa, ele deve entrar em um escopo (cujo tamanho foi calibrado para essa tarefa), executar a tarefa e, em seguida, sair o escopo para liberar a memória consumida. Para que essa técnica seja robusta, as tarefas executadas devem ser limitadas para que seja possível prever e calibrar a quantia de memória com escopo definido necessária.

O compartilhamento de escopos entre diversos encadeamentos é possível, mas difícil devido a uma regra de pai único para escopos de memória (consulte A regra de pai único). O gerenciamento de escopos compartilhados não é fácil, pois um escopo é reclamado somente quando todos os encadeamentos tiverem saído dele. Isso significa que o escopo deve ser dimensionado para permitir que diversos encadeamentos executem a tarefa simultaneamente.

Em geral, é mais fácil desenvolver com NHRTs se mantiver um escopo para uma tarefa em um encadeamento de uma vez. Para nosso exemplo, cada encadeamento de pesquisas de linha de produção será iniciado na memória imortal e terá um escopo criado antecipadamente ingressado toda vez antes de consultar ProductionLine. Cada encadeamento de conjuntos de triagem será iniciado na imortal e fará seus cálculos usando dados primitivos na pilha. Cada encadeamento também terá um escopo para entrar se precisar usar a interface WorkerConsole (onde objetos seriam criados).

Comunicação entre encadeamentos

Nosso problema de memória final é como se comunicar entre os encadeamentos. O encadeamento de pesquisas ProductionLine precisa enviar dados ao conjunto de triagem para passar dados ao encadeamento de auditoria.

Seria possível solucionar esse problema de forma simples passando valores primitivos como argumentos para métodos. Como todos os dados estariam em uma pilha, não teríamos problemas com áreas de memória.

Para tornar nosso aplicativo de exemplo mais interessante, vamos criar a classe Measurement , cujos objetos serão usados para passar dados de medida. Mas em qual área de memória devemos criar esses objetos? Não podemos usar o heap Java, pois NHRTs não podem acessá-lo. Não podemos um escopo, pois, em nossa arquitetura, nenhum dos escopos é compartilhado entre encadeamentos.

Tendo descartado heap e escopos, sobrou a memória imortal. Sabemos que a memória imortal nunca é recuperada, portanto, não podemos ficar criando objetos Measurement no tempo livre, pois podemos ficar sem memória. A resposta é criar um número finito de objetos Measurement na imortal e reutilizá-los -- na verdade, criando um conjunto de objetos.

Agrupando objetos de medida com MeasurementManager

Vamos criar a classe MeasurementManager com os mesmos métodos estáticos para obter e retornar instâncias Measurement reutilizáveis. Como programadores de Java SE, podemos ficar tentados a usar uma classe LinkedList ou Queue existente para fornecer um armazenamento de dados para reter nossas medidas. No entanto, isso não funcionará por duas razões: A primeira razão é que a maioria das classes de coleta SE criam objetos em segundo plano para manter a estrutura de dados -- nós em uma lista vinculada, por exemplo -- e criar objetos dessa maneira causaria fuga de memória imortal. A segunda razão é mais sutil. Estamos tentando fazer a ponte de encadeamentos em execução em contexto heap e não heap e, como na maioria dos aplicativos multiencadeados, seria necessário usar bloqueio para garantir acesso exclusivo à estrutura de dados que estávamos usando. Esse compartilhamento de bloqueios entre NHRTs e os encadeamentos baseados em heap podem fazer com que o coletor de lixo se aproprie de NHRTs como um efeito colateral de proteção de inversão de prioridade. Se entrarmos na situação em que o coletor de lixo provavelmente interromperá nossos NHRTs, perdemos todos os benefícios de usar memória não heap. É suficiente dizer que não se deve compartilhar bloqueios entre NHRTs e encadeamentos baseados em heap; consulte "Java em Tempo Real, Parte 3: Encadeamento e Sincronização" para obter uma explicação detalhada do problema.

A solução para compartilhar dados entre NHRTs e encadeamentos baseados em heap fornecida por RTSJ é as classes WaitFreeQueue . São filas que têm um lado de espera livre em que um NHRT pode solicitar a leitura ou gravação de alguns dados (dependendo da classe) sem o perigo de bloqueio. O outro lado da fila usa sincronização Java tradicional e é usada por encadeamentos heap. Ao evitar o compartilhamento de bloqueios entre ambientes baseados em não heap e em heap, é possível trocar dados de forma segura.

Nosso MeasurementManager será usado por NHRTs buscando medidas e pelo encadeamento de auditoria baseado em heap para retornar medidas. Portanto, usamos uma WaitFreeReadQueue para gerenciar essa interface. O lado de espera livre de uma WaitFreeQueue é projetado para ser de encadeamento único. Uma WaitFreeReadQueue é projetada para aplicativos com diversos gravadores e um único leitor. Estamos usando um aplicativo com diversos leitores e um único gravador, portanto, devemos incluir nossa própria sincronização para assegurar que somente um NHRT solicite uma medida por vez. Isso pode soar como se tivéssemos acabado com o propósito de usar uma WaitFreeQueue incluindo sincronização adicional. Mas o monitor que controla o acesso ao método read() será compartilhado somente entre NHRTs, portanto, não ocorrerá nenhum compartilhamento perigoso de bloqueios entre contextos heap e não heap.

Essa discussão trouxe à tona outro desafio significativo no desenvolvimento de aplicativos NHRT: torna-se muito mais difícil reutilizar partes existentes de código Java em um ambiente não heap. Como viu, é necessário considerar cuidadosamente de onde cada objeto está sendo alocado e como será possível evitar fugas de memória. Um dos principais pontos fortes da linguagem Java e da programação orientada a objetos em geral -- o encapsulamento de detalhes da implementação -- torna-se um ponto fraco em um contexto não heap, pois não é mais possível prever e gerenciar seu uso de memória.

Agora que projetamos nosso modelo de memória, a Figura 8 mostra nosso diagrama do sistema atualizado com as áreas de memória marcadas:


Figura 8. Diagrama da arquitetura de alto nível com áreas de memória marcadas

Prioridades de encadeamento

Escolher prioridades de encadeamento apropriadas é muito mais importante quando se está desenvolvendo com o WebSphere Real Time do que com código Java padrão. Uma escolha indevida pode permitir que o coletor de lixo se aproprie de seus NHRTs ou faça com que partes de seu sistema tenha falta de CPU.

"Java em Tempo Real, Parte 3: Encadeamento e Sincronização" explora os detalhes de prioridades de encadeamento. Para nosso sistema de exemplo, os objetivos para configurar as prioridades são:

  • Dar aos encadeamentos de pesquisas prioridade máxima para minimizar o risco de perder uma medida.
  • Evitar que os encadeamentos de conjuntos de triagem sejam interrompidos pelo coletor de lixo.

Para fazer isso, configuramos a prioridade do encadeamento dos encadeamentos de pesquisas para 38 (a prioridade RT mais alta) e as prioridades dos encadeamentos de conjuntos de triagem para 37. Como o encadeamento de auditoria é um encadeamento Java SE normal com prioridade padrão (5), sua prioridade é muito mais baixa do que dos NHRTs.

Essa configuração significa que a prioridade do encadeamento do coletor de lixo está logo acima do encadeamento de auditoria -- muito abaixo de nossos NHRTs.

Considerações sobre autoinicialização: Iniciando o aplicativo

Até o momento, verificamos somente os aspectos de estado estável do aplicativo -- ou seja, como funcionará quando estiver em execução. Não consideramos como é iniciado. Um aplicativo WebSphere Real Time é iniciado como um aplicativo Java padrão: executando em um java.lang.Thread no heap Java. A partir daí, é necessário iniciar diversos tipos de encadeamentos em diferentes áreas de memória.

Em osso aplicativo, toda a autoinicialização é executada no método startMonitoring() na classe MonitoringSystemImpl , que supomos que seja chamado por um java.lang.Thread em execução na memória heap.

Nossas tarefas de autoinicialização são:

  • Criar um ou mais encadeamentos de pesquisas na imortal.
  • Criar um ou mais objetos de conjunto de encadeamentos na imortal com cada encadeamento em conjunto também criado e em execução na imortal.
  • Criar o objeto de encadeamento de auditoria na imortal, em execução no heap.

É possível criar objetos na memória imortal a partir de um java.lang.Thread usando refletidamente o método ImmortalMemory.newInstance() . Para classes com poucos argumentos do construtor ou se você estiver criando muitos objetos com a mesma classe, isso é viável, mas logo se torna desorganizado para classes cujos construtores têm muitos argumentos.

Diferentemente de java.lang.Threads, RealtimeThreads podem entrar na memória imortal para executarem algum trabalho (fornecendo um objeto que implemente Runnable em ImmortalMemory.enter()) ou fornecendo memória imortal como a área de memória inicial para o encadeamento. A vantagem dessa abordagem é que é possível escrever código Java padrão e cada operação new criará um objeto na memória imortal. A desvantagem é que o código a ser obtido de um java.lang.Thread baseado em heap para um encadeamento RT em execução na imortal inevitavelmente parece desordenado.

No código de exemplo, escrevemos um método utilitário -- Bootstrapper.runInArea -- que pega uma MemoryArea e um objeto Runnable . Internamente, inicia um RealtimeThread de vida curta na área de memória fornecida para executar o Runnable. Essa é uma das abordagens mais organizadas para autoinicialização.

Por mais que se tente, é difícil tornar esse tipo de código de autoinicialização arrumado, organizado e fácil de ler. O salto constante entre áreas de memória e tipos de encadeamentos é difícil de explicar para qualquer pessoa que leia o código sem fazer referência ao diagrama da arquitetura e produz construções que aterrorizariam e frustrariam um programador de Java experiente. O melhor conselho é manter esse código localizado e fazer um esforço adicional na documentação do desenvolvedor para explicar o raciocínio por traz dele.

Agora que cobrimos os principais componentes de nosso design, podemos fazer experiências com o aplicativo concluído.


A demo

Uma implementação do design que acabamos de percorrer acompanha este artigo (download do código de origem). Recomendamos que você navegue pelo código de origem e veja as ideias que discutimos implementadas como código executável.

Juntamente com a implementação do sistema de monitoramento, fornecemos uma linha de produção simulada e console de trabalhadores para permitir que o sistema de monitoramento seja testado. Potes são produzidos em uma distribuição gaussiana que ocasionalmente enche os potes de mais ou de menos.

A demo é executada como um aplicativo de console com mensagens indicando o que está ocorrendo com o sistema.

Criando a demo

O pacote da demo contém os seguintes diretórios e arquivos:

  • src -- A origem Java da demo.
  • build.sh -- Um script bash shell para criar a demo.
  • MANIFEST.MF -- O arquivo de manifesto para o arquivo JAR da demo.

Para desenvolver a demo, descompacte o pacote em um diretório conveniente, vá para o diretório SweetFactory e execute build.sh. É necessário ter as versões de jar, javac e jxeinajar fornecidas com o WebSphere Real Time disponíveis em PATH para que o script build.sh funcione.

O script build.sh executa diversas operações:

  • Cria o diretório bin para armazenar as classes.
  • Desenvolve a origem Java usando javac.
  • Desenvolve um arquivo JAR executável chamado sweetfactory.jar.
  • Compila sweetfactory.jar AOT usando jxeinajar

A execução do script de construção produz saída semelhante a esta:


Listagem 1. Saída do script de construção
                
[andhall@rtj-opt2 ~]$ cd SweetFactory/
[andhall@rtj-opt2 SweetFactory]$ java -Xrealtime -version
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pxi32rt23-20070122 (SR1)
)
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Linux x86-32 j9vmxi32rt23-20070105 (
JIT enabled)
J9VM - 20070103_10821_lHdRRr
JIT  - 20061222_1810_r8.rt
GC   - 200612_11-Metronome
RT   - GA_2_3_RTJ--2006-12-08-AA-IMPORT)
JCL  - 20070119
[andhall@rtj-opt2 SweetFactory]$ ls -l
total 16
-rwxr-xr-x  1 andhall andhall  773 Apr  1 15:41 build.sh
-rw-r--r--  1 andhall andhall   76 Mar 31 14:20 MANIFEST.MF
drwx------  4 andhall andhall 4096 Mar 31 14:16 src
[andhall@rtj-opt2 SweetFactory]$ ./build.sh
Working dir = .
Building source
Building jar
AOTing the jar
J9 Java(TM) jxeinajar 2.0
Licensed Materials - Property of IBM

(c) Copyright IBM Corp. 1991, 2006  All Rights Reserved
IBM is a registered trademark of IBM Corp.
Java and all Java-based marks and logos are trademarks or registered
trademarks of Sun Microsystems, Inc.

Found /home/andhall/SweetFactory/sweetfactory.jar
Converting files
Converting /home/andhall/SweetFactory/sweetfactory.jar into /home/andhall/
SweetFactory/aot//sweetfactory.jar
JVMJ2JX002I Precompiled 156 of 168 method(s) for target ia32-linux.
Succeeded to JXE jar file sweetfactory.jar

Processing complete

Return code of 0 from jxeinajar
[andhall@rtj-opt2 SweetFactory]$ ls -l
total 252
drwxrwxr-x  3 andhall andhall   4096 Apr  1 15:42 bin
-rwxr-xr-x  1 andhall andhall    773 Apr  1 15:41 build.sh
-rw-r--r--  1 andhall andhall     76 Mar 31 14:20 MANIFEST.MF
drwx------  4 andhall andhall   4096 Mar 31 14:16 src
-rw-rw-r--  1 andhall andhall 233819 Apr  1 15:42 sweetfactory.jar

Executar o script build.sh produziu sweetfactory.jar -- uma versão compilada AOT da demo da Fábrica de Doces.

Executando a demo

Agora que já construiu a demo da Fábrica de Doces, é possível executá-la. A demo foi implementada e testada com o release SR1 do WebSphere Real Time v1.0 e recomendamos executá-la com SR1 ou posterior.


Listagem 2. Demo da Fábrica de Doces
                
[andhall@rtj-opt2 ~]$ java -Xnojit -Xrealtime -jar sweetfactory.jar
Sweetfactory RTJ Demo

Usage:

java -Xrealtime -jar sweetfactory.jar [runtime seconds 
[number of production lines [production line period millis] ] ]

Default runtime is 60 seconds
Default number of production lines is 3
Default production line period (time between jars arriving) is 20 milliseconds
No arguments supplied - using defaults
Starting demo
1173021249509: Jar 32 overfilled
1173021250228: Jar 139 underfilled
1173021252770: Jar 521 underfilled
1173021260233: Jar 1640 underfilled
1173021260938: Jar 1746 overfilled
1173021263717: Jar 2162 underfilled
1173021264219: Jar 2238 overfilled
1173021272824: Jar 3528 overfilled
1173021272842: Jar 3529 underfilled
1173021276342: Jar 4054 overfilled
1173021280427: Jar 4667 underfilled
1173021281410: Jar 4815 overfilled
1173021286265: Jar 5542 overfilled
1173021288052: Jar 5810 underfilled
1173021288913: Jar 5940 overfilled
1173021294247: Jar 6739 underfilled
1173021298832: Jar 7426 underfilled
1173021305079: Jar 8362 overfilled
Stopping demo
Run summary:

Production line stats:
Line #  Sweet Type  Jar Type  # of Missed Jars  Max Triage Pool Size  Min Triage Pool Size
0       Giant Gobstoppers       Large   0       10      7
1       Chocolate Caramels      Large   0       10      8
2       Giant Gobstoppers       Large   0       10      8


Total missed jars: 0


Measurement object pool stats:
Minimum queue depth (degree of exhaustion): 391


Audit stats:
Maximum incoming queue depth: 5


Processing stats:
Total overfilled jars: 9
Total underfilled jars: 9
Total jars processed: 8998
Demo stopped
[andhall@rtj-opt2 ~]$

Na saída, é possível ver que a demo, por padrão, inicia três linhas de produção com um atraso de 20 milissegundos entre cada pote que chega.

Observe que estamos passando a opção -Xnojit para a Java VM para que possa usar a versão AOT do aplicativo.

À medida que a demo é executada, vários potes são muito ou pouco preenchidos e uma mensagem é impressa no console, pré-anexada com um registro de data e hora. No final, uma tabela é impressa mostrando quantos potes foram perdidos para cada linha de produção.

As estatísticas no final são uma medida da carga sob a qual o sistema se encontra. A profundidade mínima da fila mostra o nível do conjunto de objetos de medida. Se o conjunto ficou vazio, então potes seriam perdidos, pois os encadeamentos de pesquisas não teriam onde armazenar medidas recebidas.

A profundidade máxima da fila de entrada de auditoria mostra quantos objetos de medida estavam esperando na fila para serem processados pelo encadeamento de auditoria em qualquer momento específico. Se esse número fosse grande, isso sugeriria que o criador de logs de auditoria não estava tendo tempo suficiente para trabalhar e a fila pode crescer.

Experimentando com a demo da Fábrica de Doces

Por padrão, a demo executa bem dentro das capacidades do hardware Opteron no qual foi desenvolvida; não há muita chance de perder um pote. No entanto, a demo pode ser parametrizada para aumentar o número de linhas de produção e para reduzir o tempo entre a chagada dos potes.

Ao alterar os parâmetros, é possível fazer a máquina trabalhar mais e, se a carga de trabalho for aumentada suficientemente, é possível fazer com que a demo comece a perder potes.

A demo aceita até três argumentos: tempo de execução em segundos, número de linhas de produção e período entre potes que chegam em milissegundos.

Antes de começar a acelerar de forma agressiva a carga de trabalho, esteja ciente que aumentar o número de linhas de produção de forma linear aumenta o número de encadeamentos em execução dentro da demo. Cada NHRT tem um escopo anexado, portanto, aumentar o número de encadeamentos aumentará e, eventualmente exaurirá, o espaço total de memória com escopo definido.

É possível ver, ao executar java -Xrealtime -verbose:sizes -version , que o espaço total padrão da memória com escopo definido é 8 MB:


Listagem 3. java -Xrealtime -verbose:sizes -version
                
[andhall@rtj-opt2 SweetFactory]$ java -Xrealtime -verbose:sizes -version
  -Xmca32K        RAM class segment increment
  -Xmco128K       ROM class segment increment
  -Xms64M         initial memory size
  -Xgc:immortalMemorySize=16M immortal memory space size
  -Xgc:scopedMemoryMaximumSize=8M scoped memory space maximum size
  -Xmx64M         memory maximum
  -Xmso256K       OS thread stack size
  -Xiss2K         java thread stack initial size
  -Xssi16K        java thread stack increment
  -Xss256K        java thread stack maximum size
java version "1.5.0"
Java(TM) 2 Runtime Environment, Standard Edition (build pxi32rt23-20070122 (SR1)
)
IBM J9 VM (build 2.3, J2RE 1.5.0 IBM J9 2.3 Linux x86-32 j9vmxi32rt23-20070105 (
JIT enabled)
J9VM - 20070103_10821_lHdRRr
JIT  - 20061222_1810_r8.rt
GC   - 200612_11-Metronome
RT   - GA_2_3_RTJ--2006-12-08-AA-IMPORT)
JCL  - 20070119
[andhall@rtj-opt2 SweetFactory]$

Fomos generosos com a quantia de memória com escopo definido que designamos a cada tarefa: 100 KB por NHRT -- e criamos 11 NHRTs para cada linha de produção. É possível usar isso para fazer uma estimativa da quantia da memória com escopo definido total que precisamos disponibilizar com -Xgc:scopedMemoryMaximumSize para tentar algumas das cargas de trabalho mais agressivas.

Por exemplo, para executar 50 linhas de produção em um período de 10 milissegundos, precisaríamos de pelo menos 55 MB de memória com escopo definido. Vamos usar 60 MB para conceder espaço para algum respiro. O comando que usaríamos para executar esse cenário por 60 segundos é:

java -Xrealtime -Xnojit -Xgc:scopedMemoryMaximumSize=60M -jar sweetfactory.jar 60 50 10   

Se você aumentar o número de linhas de produção de forma suficiente (em torno de 70 a intervalos de 10 milissegundos parece ser o máximo em nosso sistema), a demo começa a perder potes. Quando isso ocorrer, você verá mensagens semelhantes à seguinte impressas no console:

Error: measurement pool exhausted
1175439878160 : Missed 20 jars!

A primeira mensagem vem do encadeamento de pesquisas quando tenta e falha em obter um objeto de medida do conjunto. A segunda mostra quantos potes foram perdidos quando o encadeamento de pesquisas finalmente conseguiu obter um objeto de medida.

Nessas situações, a maior parte do tempo de CPU está sendo gasto na manipulação de medidas recebidas. À medida que a carga aumenta, não há tempo suficiente para executar o Metronome e gravar o log de auditoria. As medidas aumentam na fila na frente do sistema de auditoria, exaurindo o conjunto de medidas. Somente quando as medidas acabam e os encadeamentos de pesquisas são forçados a esperar pelo retorno de mais, o encadeamento de criação de log obtém o tempo de CPU que precisa para gravar o log e retornar algumas das medidas ao conjunto.


Dicas e truques da implementação

Após trabalhar com o WebSphere Real Time por mais de um ano, montamos algumas dicas e truques para obter o máximo de nossos aplicativos RT. Esta seção descreve alguns dos mais úteis que descobrimos.

Inserir verificações para tipo de encadeamento e área de memória

Ao desenvolver com memória não heap, é importante considerar cuidadosamente em qual área de memória você está e em qual tipo de encadeamento está executando cada linha de código.

O potencial é alto para confundir erros causados pela execução de uma designação ilegal ou, por exemplo, tentar inserir uma área de memória de um java.lang.Thread.

Da mesma maneira que inserir instruções assert() em seu código para verificação de integridade de argumentos é uma boa prática de programação para Java SE, no código Java RT é sensato declarar o contexto do encadeamento e a área de memória em que se encontra.

O aplicativo de amostra da Fábrica de Doces inclui uma classe ContextChecker dedicada que fornece um método checkContext e um conjunto de constantes para representar os diferentes contextos.

Separar executáveis e áreas de memória para manipulação de erros

Em código Java padrão -- graças a seu ambiente de memória gerenciada -- a manipulação de erros é apenas outro bloco de código. Em Java RT não heap, a manipulação de erros pode ser uma forte dor de cabeça.

Conforme discutimos, a maioria das tarefas que você irá querer executar em NHRTs usa memória e você deve calibrar seu uso de escopos ou objetos em conjunto para essas tarefas específicas.

Se encontrar um erro, mesmo um comportamento simples, como imprimir uma mensagem de erro, se torna problemático, pois pode não haver memória para executar a operação. Uma opção é fornecer sobrecarga suficiente em todas as situações para imprimir algumas linhas de depuração antes de travar, mas isso pode não ser prático.

A melhor abordagem é criar uma classe para cada condição de erro que estenda Runnable e forneça métodos para fornecer dados sobre a falha (para que você tenha informação suficiente para entender o que aconteceu). Crie uma instância dessa classe logo no início para que esteja pronta quando precisar dela sem precisar consumir memória. Separe uma área de memória com escopo definido grande o suficiente para executar a operação de manipulação de erros.

Com um objeto Runnable pré-alocado e um escopo separado, você deve sempre poder relatar um problema sem usar qualquer memória no contexto no qual o erro ocorreu. Isso é útil para situações como um OutOfMemoryError ser emitido quando a criação de mais objetos seria impossível.

Demonstramos essa técnica na demo de Fábrica de Doces na classe ProductionLinePoller , onde definimos errorReportingRunnable para ser usado se não for possível buscar um Measurement no conjunto.


Conclusão

Mostramos como desenvolver e implementar aplicativos Java RT na plataforma do WebSphere Real Time para atender características cada vez mais deterministas. A programação de NHRT com memória não heap cria de forma significativa mais trabalho em comparação a escrever aplicativos regulares baseados em heap. Considere a demo da Fábrica de Doces. Se estivéssemos escrevendo funções semelhantes em um ambiente de heap, teria sido trivial. A biblioteca padrão Java SE teria fornecido a maior parte da funcionalidade necessária, inclusive conjuntos de encadeamentos e classes de coleta.

O maior obstáculo para se trabalhar com NHRTs é que não apenas há muitas novas técnicas para aprender, mas que muitas de nossas melhores práticas de Java SE que foram difíceis de aprender -- inclusive a maioria dos padrões de design -- não se aplicam e causarão fugas de memória.

Felizmente, é possível realizar muitos dos objetos RT leves com o WebSphere Real Time sem jamais precisar chamar o construtor em um NHRT. O desempenho do coletor de lixo Metronome permite obter execução previsível a uma precisão de alguns milissegundos. No entanto, se você precisar de responsividade máxima e gostar de um desafio, os recursos não heap do WebSphere Real Time deixarão obter isso.



Download

DescriçãoNomeTamanhoMétodo de download
Sweet Factory demo for this articlej-rtj5sweetfactory.tgz16KBHTTP

Informações sobre métodos de download


Recursos

Aprender

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 Real-time Java technology no IBM alphaWorks para localizar TuningFork e outras tecnologias de ponta para Java RT.

Discutir

Sobre os autores

Andrew Hall

Andrew Hall entrou para o Centro de Tecnologia Java da IBM em 2004, iniciando na equipe de teste do sistema onde trabalhou por dois anos. Ele ficou 18 meses na equipe de serviço Java, onde depurou dezenas de problemas de memória nativa em várias plataformas. Atualmente ele é o desenvolvedor da equipe de Confiabilidade, Disponibilidade e Capacidade de Manutenção Java. Nas horas vagas ele se dedica à leitura, fotografia e diversões.

Caroline Gough

Caroline Gough trabalhou como desenvolvedora em uma pequena cada de software por três anos antes de passar a fazer parte da equipe Java Technology Centre System Test no Laboratório da IBM Hursley. Ela é testadora senior com conhecimento em teste de tensão e conjunto de ferramentas RAS (confiabilidade, disponibilidade e capacidade de manutenção). Ela trabalhou no IBM WebSphere Real Time V1.0 e agora está preparando testes para releases futuros da plataforma Java.

Alan Stevens

Helen Masters se formou em 1995 na Universidade de Nottingham e passou a fazer parte da organização IBM Global Services em 1996 para trabalhar em desenvolvimento de software em um grande contrato de defesa. Ela foi transferida para o Laboratório da IBM Hursley em 2000, onde teve diversos cargos de liderança em sua área de conhecimento técnico: teste. Helen atualmente é responsável por liderar a equipe de esforço de teste no IBM WebSphere Real Time V1.0.

Helen Masters

Helen Masters se formou em 1995 na Universidade de Nottingham e passou a fazer parte da organização IBM Global Services em 1996 para trabalhar em desenvolvimento de software em um grande contrato de defesa. Ela foi transferida para o Laboratório da IBM Hursley em 2000, onde teve diversos cargos de liderança em sua área de conhecimento técnico: teste. Helen atualmente é responsável por liderar a equipe de esforço de teste no IBM WebSphere Real Time V1.0.

Ajuda para Relatar Abuso

Relatar abuso

Obrigado. Esta entrada foi sinalizada para atenção do moderador.


Ajuda para Relatar Abuso

Relatar abuso

Falha no envio do Relatório de abuso. Tente novamente mais tarde.


developerWorks: Registre-se


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos de uso do developerWorks.

 


Na primeira vez que você efetua sign in no developerWorks, um perfil é criado para você. Informações selecionadas do seu perfil developerWorks são exibidas ao público, mas você pode editá-las a qualquer momento. Seu primeiro nome, sobrenome (a menos que escolha ocultá-los), e seu nome de exibição acompanharão o conteúdo que postar.

Selecione seu nome de exibição

Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

(Deve possuir de 3 a 31 caracteres.)


Ao clicar em Enviar, você concorda com os termos de uso do developerWorks.

 


Classificar este artigo

Comentários

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=757350
ArticleTitle=Java em Tempo Real, Parte 5: Composição e Implementação de Aplicativos Java em Tempo Real
publish-date=09162011
author1-email=andhall@uk.ibm.com
author1-email-cc=jaloi@us.ibm.com
author2-email=goughc@uk.ibm.com
author2-email-cc=
author3-email=alan_stevens@uk.ibm.com
author3-email-cc=
author4-email=helen_postlethwaite@uk.ibm.com
author4-email-cc=