A plataforma Java é uma das opções mais populares quando pensamos em desenvolvimento de aplicações, e isso se torna ainda mais evidente quando o escopo são as soluções voltadas para a web. Atualmente, estima-se que cerca de nove milhões de desenvolvedores adotam o Java. Completando 20 anos em breve, a plataforma é classificada por muitos como antiga, nesse universo em que novas soluções surgem e desaparecem num piscar de olhos.
Todo esse tempo, obviamente, possibilitou mais robustez e confiabilidade ao Java, no entanto, por ser projetada com alguns conceitos hoje tidos como obsoletos, a exemplo da arquitetura não-modular, possui uma estrutura monolítica, ou seja, não dividida em módulos. Essa ausência, presente em algumas das linguagens mais modernas, torna mais difícil o reuso e a manutenção do código, restringindo a sua utilização, principalmente, em dispositivos de baixa capacidade de processamento. Por causa disso, há muito tempo a comunidade Java solicita uma grande reforma na estrutura da plataforma, com o objetivo de torná-la modular.
Vale lembrar, ainda, que ao longo de sua história a plataforma Java cresceu de um pequeno sistema criado para dispositivos embarcados para uma rica coleção de bibliotecas, que atende às mais diversas necessidades e que precisam rodar em ambientes com sistemas operacionais e recursos de hardware distintos. Hoje sabemos que possuir um canivete suíço com tantos recursos é essencial ao desenvolvedor, contudo, essa abundância também traz alguns problemas:
· Tamanho: O JDK sempre foi disponibilizado como um grande e indivisível artefato de software, com várias bibliotecas e recursos para o desenvolvedor. A inserção de novidades na plataforma ao longo dos anos culminou no aumento constante deste, tornando-o cada vez mais pesado;
· Complexidade: O JDK é profundamente interconectado, ou seja, as bibliotecas são muito dependentes umas das outras, compondo assim uma estrutura monolítica. Além disso, com o tempo essa estrutura resultou em conexões inesperadas entre APIs e suas respectivas implementações, levando a um tempo de inicialização e consumo de memória maiores, degradando o desempenho de aplicações que necessitam da plataforma para funcionar. Para se ter uma ideia, um programa que escreve um simples “Alô, mundo!” no console, ao carregar e inicializar mais de 300 classes, leva, em média, 100ms em uma máquina desktop.
Sabendo que quanto maior a aplicação maior será o tempo necessário para inicializá-la, é fundamental evoluir esse cenário para aprimorar o tempo de inicialização e o consumo de memória. Com esse objetivo, os desenvolvedores do Java optaram por dividir o JDK, especificando um conjunto de módulos separados e independentes.
Praticamente concluído, o processo de reestruturação do Java em módulos consiste na identificação de interconexões entre bibliotecas e na eliminação dessas dependências quando possível. Isso reduz o número de classes carregadas em tempo de execução e, consequentemente, melhora tanto o tempo de inicialização quanto o consumo de memória, pois ao reduzir o acoplamento a relação de dependência entre certas classes diminuirá. Deste modo, com o JDK modular, apenas os módulos necessários para inicializar a aplicação serão carregados. Outra consequência é que a utilização de módulos poderá ser empregada não apenas pelo JDK, mas também por bibliotecas e aplicações, de tal forma a melhorar cada vez todo o universo Java.
Em conjunto com a comunidade, a modularização do JDK está sendo desenvolvida pela Oracle – sob a liderança de Mark Reinhold – em um projeto de codinome Jigsaw (quebra-cabeça). Parte do JDK 9, está previsto para ser entregue no primeiro semestre de 2017 e, uma vez que é a maior, principal e mais aguardada mudança em relação à versão 8, é o assunto do primeiro tópico apresentado neste artigo.
O segundo tópico abordado será o jShell, ferramenta de linha de comando que possui suporte a REPL (Read Eval Print Loop). Assim, a partir de agora será possível escrever e testar expressões em Java na linha de comando, sem a necessidade de escrever classes e métodos. Essa ferramenta já está disponível para o público através das versões de acesso antecipado (Early Access) do JDK 9 com o intuído de obter retorno da comunidade sobre o seu funcionamento e sugestões para futuras melhorias.
O terceiro assunto que analisaremos neste artigo será o JMH, ou Java Microbenchmarking Harness, o qual fornece uma estrutura para construir, rodar e analisar testes de desempenho em vários níveis de granularidade. A importância dessa funcionalidade vem da necessidade de se obter avaliações de desempenho precisas, uma vez que o tempo de aquecimento (warmup time) para inicializar os recursos para rodar o teste de desempenho, bem como as otimizações automáticas realizadas pelo compilador Java no código a ser testado, causam grande impacto no resultado dessas avaliações. Essa constatação é ainda mais evidente quando se trata de operações que duram apenas micro ou nanossegundos.
O quarto tópico diz respeito à alteração do coletor de lixo (garbage collector) padrão do Java, que passa a ser o G1, o qual funciona melhor em JVMs (Java Virtual Machine) cuja memória de alocação (heap space) é superior a 4GB, característica muito comum nos dias atuais. Dessa forma, é esperado que o gerenciamento de memória da JVM seja mais eficiente, melhorando o desempenho do funcionamento da máquina virtual como um todo.
O quinto tópico destaca a implementação de uma API com suporte ao protocolo HTTP 2.0 e websockets. Essa API é muito importante porque a especificação da versão 2.0 do HTTP foi disponibilizada já há algum tempo, baseada na implementação do protocolo SPDY do Google, o qual recebeu críticas muito positivas, pois de fato possibilita a aceleração da navegação na web. Assim, o objetivo é fazer com que o Java possa se antecipar à adoção massiva do novo HTTP e forneça uma opção para a utilização do mesmo nas aplicações desenvolvidas com essa plataforma.
Por fim, será analisada a melhoria da API de processos do Java, pois até o momento o JDK possui uma grande limitação para controlar e gerenciar processos do sistema operacional. Hoje, para obter um simples número de identificação de um processo no sistema operacional, é preciso utilizar código nativo do SO (em geral em C), o que dificulta a implementação e leva a um alto acoplamento.
Projeto Jigsaw – Modularização do JDK
O projeto Jigsaw é um esforço da Oracle e da comunidade Java para a implementação da modularização da plataforma, tendo em mente os seguintes objetivos, de acordo com a página oficial do projeto:
· Tornar a plataforma Java SE e o JDK mais facilmente escaláveis para dispositivos pequenos e/ou de baixo poder de processamento;
· Melhorar a segurança e manutenibilidade da plataforma Java SE e do JDK;
· Permitir um melhor desempenho das aplicações Java;
· Tornar mais fácil aos desenvolvedores a construção e manutenção de bibliotecas e grandes aplicações nas plataformas Java.
Para alcançar esses objetivos, foi proposto projetar e implementar um sistema de módulos padrão para a plataforma Java SE e JDK. A partir disso, os requisitos para a criação do sistema de módulos foram especificados e, em seguida, foi feito um rascunho (draft) com essa especificação.
De acordo com a JSR 376 (documento de especificação dos requisitos), o sistema de módulos possui duas características fundamentais:
1. Configuração confiável (Reliable configuration): Substituição do mecanismo de classpath padrão por uma forma dos componentes de software declararem dependências explícitas, a fim de evitar conflitos; e
2. Forte encapsulamento (Strong encapsulation): Permissão para um componente de um módulo declarar quais dos seus tipos de dados declarados como públicos são acessíveis a componentes de outros módulos e quais não são.
Nota: Nas versões anteriores do Java, o tipo público especifica que qualquer classe no classpath, independentemente do pacote em que está, pode acessar aquele tipo diretamente. Como os módulos são uma nova entidade que abrange pacotes/componentes, é preciso informar quais tipos que foram declarados como públicos são acessíveis a outros componentes que fazem parte de outros módulos.
Essas características auxiliam no desenvolvimento de aplicações, bibliotecas e da própria plataforma Java, pois garante maior escalabilidade com aplicações/plataformas mais enxutas, e apenas com as dependências necessárias; maior integridade, ao evitar a utilização de dependências cujas versões não são as mais adequadas; assim como grandes melhorias em termos de desempenho, com a resolução de dependências e coleta de lixo mais eficientes, bem como um tamanho menor dos arquivos de execução.
Módulos
Os módulos são um novo tipo de componente do Java, possuindo um nome e um código descritivo, isto é, um arquivo que define os detalhes e propriedades dos mesmos, além de outras informações adicionais. Essas informações adicionais descrevem, basicamente, configurações de serviços ou recursos que o módulo pode utilizar.
No código descritivo, por sua vez, o módulo deve informar se faz uso de tipos (classes, interfaces ou pacotes) de outros módulos, de forma que a aplicação possa compilar e executar sem erros. Para isso, deve-se utilizar a cláusula requires em conjunto com o nome do recurso. Além disso, pode ser necessário informar ao compilador quais tipos desse módulo podem ser acessados por outros, ou seja, declarar quais recursos ele exporta, o que é feito através da cláusula exports.
A partir disso, o sistema de módulos localiza os módulos necessários e, ao contrário do sistema de classpath, garante que o código de um módulo acesse apenas os tipos dos módulos dos quais ele depende. Como complemento, o sistema de controle de acesso da linguagem Java e da máquina virtual também previnem que códigos acessem tipos oriundos de pacotes que não são exportados pelos módulos que os definem.
Com o intuito de evitar a forte dependência entre módulos, um módulo pode declarar que utiliza (uses) uma interface (tipo genérico) em vez de uma classe (tipo específico) de um determinado serviço cuja implementação é fornecida (provided) em tempo de execução por outro modulo. Essa estratégia permite que os desenvolvedores possam, entre outras coisas, estender a API do Java, codificando classes que implementam uma interface que é utilizada pelo módulo em desenvolvimento, interface essa declarada através da sintaxe uses nomedainterface. Isso ocorre porque a dependência do módulo que declara a interface com a classe criada pelo desenvolvedor é resolvida em tempo de compilação e de execução, sem necessidade de quaisquer intervenções. Juntamente a essa capacidade de resolução de componentes genéricos, o sistema de módulos mantém a hierarquia atual de classloaders, facilitando a execução ou migração de aplicações legadas para versão 9 do Java.
Tendo em vista que os módulos são descritos através de um código, a maneira mais simples de declará-los é especificar apenas os seus respectivos nomes em um arquivo, conforme o código a seguir:
module br.com.devmedia {}
Como já mencionado, no entanto, existem mais opções de configuração, como informar que um módulo depende de outro, utilizando a cláusula requires. Desse modo, supondo que o módulo criado anteriormente (br.com.devmedia) dependa de outro (br.com.devmediaOutro), a estrutura utilizada para descrever essa relação será semelhante à exposta na Listagem 1.
Listagem 1. Declaração do módulo br.com.devmedia com dependência ao módulo br.com.devmediaOutro.
module br.com.devmedia {
requires br.com.devmediaOutro;
}
É possível, ainda, declarar os pacotes do módulo cujos tipos públicos podem ser acessados por outros, através da cláusula exports. Na Listagem 2, br.com.devmedia define que os pacotes br.com.devmedia.pacote1 e br.com.devmedia.pacote2 podem ser utilizados por outros módulos. Portanto, se na declaração de um módulo não existe a cláusula exports, o módulo não irá, de forma alguma, exportar pacotes para outros.
Listagem 2. Configuração do módulo br.com.devmedia com dependência e exports declarados.
module br.com.devmedia {
requires br.com.devmediaOutro;
exports br.com.devmedia.pacote1;
exports br.com.devmedia.pacote2;
}
E a declaração do módulo, como é feita? Por convenção, deve ser criado um arquivo descritor, de nome module-info.java, no diretório raiz do módulo, por exemplo:
module-info.java
br/com/devmedia/pacote1/FabricaPacoteUm.java
br/com/devmedia/pacote2/ClassePacoteDois.java
... (outros pacotes/classes/interfaces)
Feito isso, o descritor poderá ser compilado, assim como feito com as classes/interfaces, e resultará em um artefato de nome module-info.class, colocado no mesmo diretório dos demais arquivos compilados.
Por fim, saiba que, do mesmo modo que os nomes dos pacotes, os nomes dos módulos não podem estar em conflito. Assim, a forma recomendada de se nomear módulos, a fim de evitar problemas, é utilizar o padrão de nome de domínio ao contrário, como já demonstrado nos exemplos anteriores.
Artefatos de módulos
Como sabemos, as ferramentas de linha de comando da plataforma Java são capazes de criar, manipular e consumir arquivos JAR. Diante disso, de forma a facilitar a adoção e migração para a nova versão do JDK, os desenvolvedores criaram também o arquivo JAR modular. Como se pode imaginar, um arquivo JAR modular é um arquivo JAR comum que possui a definição do módulo compilada (o arquivo module-info.class) no seu diretório raiz (vide BOX 1). Por exemplo, um JAR para o módulo especificado anteriormente teria a estrutura apresentada na Listagem 3.
Listagem 3. Estrutura do JAR para o módulo exemplo.
META-INF/
META-INF/MANIFEST.MF
module-info.class
br/com/devmedia/pacote1/FabricaPacoteUm.class
br/com/devmedia/pacote2/ClassePacoteDois.class
...(outras classes e pacotes)
BOX 1. Arquivo JAR modular
Pode ser utilizado como um módulo (a partir do Java 9) ou como um arquivo JAR comum (em todas as versões). Assim, para fazer uso de um arquivo JAR modular como se fosse um JAR comum, basta informar o mesmo no classpath da aplicação. Dessa forma o arquivo module-info.class será ignorado. Essa estratégia permite que aplicações legadas, que executam em versões anteriores do Java, possam se beneficiar de bibliotecas desenvolvidas de forma modular, mantendo com isso a retrocompatibilidade.
Módulos da plataforma Java SE 9
A especificação da plataforma Java SE 9 a divide em um conjunto de módulos. Entretanto, uma implementação da mesma pode conter todos ou apenas alguns módulos. Essa opção é importante porque alguns projetos não precisam fazer uso de todos os módulos, tornando assim mais fácil escalar as aplicações, como também o ambiente de execução do Java, para dispositivos cada vez menores, uma vez que os executáveis também serão menores.
O único módulo que deve estar presente em todas as implementações é o módulo base, o qual possui o nome java.base. Este define e exporta todos os pacotes que fazem parte do núcleo da plataforma, incluindo o próprio sistema de módulos (vide Listagem 4).
Listagem 4. Declaração do módulo java.base do JDK9.
module java.base {
exports java.io;
exports java.lang;
exports java.lang.annotation;
exports java.lang.invoke;
exports java.lang.module;
exports java.lang.ref;
exports java.lang.reflect;
exports java.math;
exports java.net;
...
}
Qualquer outro módulo do Java sempre irá depender do módulo base, o qual não depende de ninguém. Como consequência, todos os outros módulos compartilharão o prefixo “java.” em seu nome, como o java.sql (para conectividade com o banco de dados), java.xml (para processamento de XML) e java.logging (para registro de operações/eventos). Entretanto, existem exceções, como os módulos que fazem parte apenas do JDK. Por convenção, esses utilizam o prefixo “jdk.”.
Confira alguns cursos de Java na DevMedia
- Java EE: Construa uma aplicação completa Java EE
- Java: Técnicas Avançadas para Java SE
- Série Java Core
Resolução e dependência
Para melhor compreender como os módulos são encontrados ou resolvidos em tempo de compilação ou de execução, é preciso entender como eles estão relacionados entre si. Supondo que uma aplicação faça uso do módulo br.com.devmedia, mencionado anteriormente, como também do módulo java.sql, da plataforma Java 9, o módulo que conteria o núcleo da aplicação seria descrito conforme a Listagem 5.
Listagem 5. Declaração do módulo br.com.devmedia-app.
module br.com.devmedia-app {
requires br.com.devmedia;
requires java.sql;
}
Note que o módulo núcleo da aplicação, no caso br.com.devmedia-app, seria o ponto de partida para que o sistema encontre as dependências necessárias para a mesma, tendo como base os nomes dos módulos indicados pela cláusula requires do descritor do módulo inicial, como demonstrado no exemplo. Saiba, ainda, que cada módulo do qual o módulo inicial depende pode depender de outros. Nestes casos, o sistema de módulos procurará recursivamente por todas as dependências de todos os outros, até que não reste nenhuma a ser acrescentada. Logo, se essa relação de dependência entre módulos fosse desenhada em um gráfico, o desenho teria a forma de um grafo, no qual cada relação entre duas dependências seria expressa por uma aresta e cada dependência seria um vértice.
Com o intuito de construir o grafo com as dependências do módulo br.com.devmedia-app, o compilador precisa ler as descrições dos módulos que ele depende (java.sql e br.com.devmedia). Logo, para melhor compreender a construção desse grafo, é preciso dar uma olhada na declaração dos módulos que se depende. Em virtude de a declaração do módulo br.com.devmedia já ter sido demonstrada em uma listagem anterior, na Listagem 6 é possível ver a declaração da outra dependência, o java.sql.
Listagem 6. Declaração do módulo java.sql.
module java.sql {
requires java.logging;
requires java.xml;
exports java.sql;
exports javax.sql;
exports javax.transaction.xa;
}
Nota: Não foi criada uma nova listagem com a descrição do módulo br.com.devmedia porque já foi demonstrado na Listagem 2. Além disso, para ser mais breve, também foram omitidas as descrições dos módulos java.logging e java.xml, dos quais o módulo java.sql depende.
Baseado nas declarações dos módulos apresentados, o grafo resultante para a resolução de dependências do módulo br.com.devmedia-app contém os vértices e arestas descritos na Figura 1.
Figura 1. Grafo de módulos da aplicação.
Nessa figura, as linhas de cor azul representam as dependências explícitas, ou seja, que estão descritas no module-info.java, nas cláusulas requires, enquanto as linhas de cor cinza representam as dependências implícitas ou indiretas de cada módulo em relação ao módulo base.
Nota: Quando um módulo depende diretamente de outro, então o código do primeiro será capaz de referenciar tipos do segundo. Para esse tipo de situação, diz-se que o primeiro módulo lê o segundo ou, da forma inversa, que o segundo módulo é legível ao primeiro. No gráfico da Figura 1, o módulo br.com.devmedia-app lê os módulos br.com.devmedia e o java.sql, mas não o java.xml, por exemplo. Essa característica de legibilidade é a base da configuração confiável, pois permite que o sistema de módulos garanta que:
· Cada dependência é satisfeita por um único módulo;
· Dois módulos não podem referenciar um ao outro de forma a criar uma dependência cíclica, mesmo que ambos declarem os respectivos requires e exports. O compilador identificará isso como um erro;
· Cada módulo só pode fazer referência uma única vez a um pacote específico (com mesmo nome e conteúdo), mesmo que indiretamente. Exemplo: se um módulo M1 faz referência a dois módulos (M2 e M3) que dependem de um pacote Z, o compilador perceberá que Z é referenciado duas vezes. Nesse contexto, para evitar desperdício de memória o compilador só carregará esse pacote uma vez, o que faz com que as aplicações também se tornem mais eficientes;
· Se dois (ou mais) módulos definem pacotes com o mesmo nome, mas conteúdos diferentes, ambos podem ser carregados na memória sem que haja comprometimento no funcionamento da aplicação. A princípio, essa garantia pode parecer conflitante com a anterior, mas não o é por envolver dois módulos distintos;
· A configuração confiável não é somente mais confiável, mas também mais rápida, pois diferentemente do que é feito com classpathes na versão anterior do Java, quando um código em um módulo referencia um tipo contido em um pacote, há uma garantia de que aquele pacote está definido em algum módulo do qual ele depende (explicitamente ou implicitamente). Dessa forma, quando for necessário buscar pela definição de algum tipo específico, não há a necessidade de procurar por ele em vários módulos ou por todo o classpath, como era feito em versões anteriores, tornando as execuções das operações mais rápidas.
Serviços
Como sabemos, o baixo acoplamento de componentes de software alcançado através de interfaces de serviços e provedores de serviços é uma poderosa ferramenta para a construção de grandes sistemas. Por esse motivo, o Java suporta há um bom tempo essa técnica através da classe java.util.ServiceLoader, a qual é utilizada pelo JDK e também por bibliotecas e aplicações.
Essa classe localiza provedores de serviços em tempo de execução ao procurar por arquivos de configuração dos serviços na pasta META-INF/services. Entretanto, se um serviço é fornecido por um módulo, esses arquivos não estarão no classpath da aplicação. Sendo assim, foi preciso implementar uma nova estratégia para localizar os provedores de serviços e carregá-los corretamente, como demonstraremos a seguir.
Vejamos um exemplo: suponha que o módulo br.com.devmedia-app usa um banco de dados PostgreSQL e que o driver JDBC (recurso) para o mesmo seja fornecido em um módulo declarado de acordo com a Listagem 7.
Listagem 7. Declaração do módulo org.postgresql.jdbc.
module org.postgresql.jdbc {
requires java.sql;
requires org.slf4j;
exports org.postgresql.jdbc;
}
Conforme pode ser observado nessa listagem, a declaração de exportação de org.postgresql.jdbc refere-se ao pacote do módulo de mesmo nome, que possui o driver JDBC do banco de dados PostgreSQL. Esse driver é uma classe Java que implementa a interface de serviço java.sql.Driver, contida no módulo java.sql (veja a nota a seguir). Além disso, para que o módulo org.postgresql.jdbc possa fazer uso dos módulos dos quais depende (java.sql e org.slf4j), é preciso que os mesmos sejam adicionados ao grafo de módulos em tempo de execução, conforme a Figura 2, tornando possível que a classe de carregamento de serviços ServiceLoader instancie a classe do driver, procurando, através de reflection, por classes no pacote org.postgresql.jdbc que implementem a interface java.sql.Driver.
Nota: Na Listagem 7 pode parecer ambíguo o fato de um módulo chamado org.postgresql.jdbc ter um pacote exportado de mesmo nome. O fato é que as implementações de drivers jdbc para bancos de dados geralmente são anteriores ao sistema de módulos do Java e devido a isso estão encapsuladas dentro de pacotes. Logo, de forma a facilitar a identificação desse pacote “legado”, definiu-se a convenção de criar um módulo com o mesmo nome do pacote que possui a implementação do driver.
Figura 2. Grafo de Módulos com o módulo org.postgresql.jdbc adicionado.
Para realizar essas adições ao grafo, o compilador deve ser capaz de localizar os provedores de serviços nos módulos a ele acessíveis. Uma forma de fazer isso seria realizar uma busca por dependências na pasta META-INF/services, da mesma forma que a classe ServiceLoader faz em versões anteriores do Java, só que agora com módulos. Todavia, uma alternativa melhor, por viabilizar uma melhoria de performance, é possibilitar que os criadores de módulos de acesso a serviços externos (como o acesso a banco de dados através da API JDBC) forneçam uma implementação específica do serviço, através da cláusula provides, como pode ser observado na Listagem 8.
O ganho de performance se deve ao fato de que em vez de realizar uma busca por uma implementação da interface java.sql.Driver na pasta META-INF/services – a qual pode ter centenas de serviços – um módulo que define exatamente a classe que implementa essa interface evita a necessidade de realizar qualquer busca extensiva, ganhando assim tempo na compilação.
Listagem 8. Declaração do módulo org.postgresql.jdbc.
module org.postgresql.jdbc {
requires java.sql;
requires org.slf4j;
exports org.postgresql.jdbc;
provides java.sql.Driver with org.postgresql.Driver;
}
Ao declarar provides java.sql.Driver with org.postgresql.Driver, o criador do módulo está informando que uma instância da classe org.postgresql.Driver deve ser criada para que seja possível utilizar o serviço java.sql.Driver. Por sua vez, é igualmente importante que o módulo que faz uso de um determinado serviço declare esse uso em sua própria descrição, através da cláusula uses (vide Listagem 9).
Listagem 9. Declaração do módulo java.sql.
module java.sql {
requires public java.logging;
requires public java.xml;
exports java.sql;
exports javax.sql;
exports javax.transaction.xa;
uses java.sql.Driver;
}
Analisando as descrições dos módulos anteriores é muito fácil ver e entender que um deles provê um serviço que pode ser utilizado pelo outro. Essas declarações explícitas permitem que o sistema de módulos garanta, em tempo de compilação, que a interface do serviço a ser utilizado (no caso, o java.sql.Driver) está acessível tanto para o provedor quanto para o usuário do mesmo. Mais ainda, garante que o provedor do serviço (org.postgresql.Driver) implementa, de fato, os serviços por ele declarados. Dessa forma, antes da aplicação ser executada é possível verificar uma série de erros que poderiam ocorrer devido à ausência de alguma dependência, o que em versões anteriores do Java só era possível em tempo de execução. Essa característica agrega mais confiança de que o software funcionará corretamente, e também viabiliza mais agilidade ao desenvolvimento de aplicações, pois diminui a necessidade de executar a aplicação com a finalidade de identificar erros.
Class loaders (carregadores de classes)
Outra mudança importante no Java é que agora cada módulo estará associado a um class loader (BOX 2) em tempo de execução, entretanto, cada carregador de classe pode estar associado a vários módulos. Sendo assim, um class loader pode instanciar tipos de um ou mais módulos, desde que esses módulos não interfiram entre si (exportem o mesmo pacote) e que esses tipos só sejam instanciados por um único class loader.
Essa definição é essencial para possibilitar a retrocompatibilidade com versões anteriores da linguagem, uma vez que mantém a hierarquia de class loaders da plataforma. Desse modo, os class loaders bootstrap e extension continuam a existir na nova versão e serão utilizados para carregar as classes pertencentes a módulos da própria plataforma. Já o class loader application/system também foi mantido, mas com a função de carregar tipos oriundos de módulos de dependências da aplicação. Essa flexibilidade facilita a modularização de aplicações legadas, uma vez que os class loaders das versões anteriores podem ser estendidos com o intuito de carregar classes contidas em módulos com poucas alterações.
BOX 2. Class loaders
Os class loaders fazem parte do ambiente de execução do Java (JRE) e são responsáveis por carregar classes dinamicamente na máquina virtual (JVM). Logo, devem localizar as bibliotecas, ler o conteúdo delas e carregar suas respectivas classes.
Quando a JVM é iniciada, três class loaders são utilizados:
· Bootstrap ClassLoader – Responsável por carregar as bibliotecas da plataforma Java, esse class loader foi implementado em código nativo (não é uma classe Java) e carrega classes Java importantes, como a java.lang.Object, a qual é o pai de todos os objetos Java, bem como outros códigos na memória. Até a versão 8 do Java, as classes que esse class loader instancia ficavam empacotadas no rt.jar, presente no diretório jre/lib do JRE. No entanto, como na versão 9 não teremos mais arquivos JAR e sim módulos (ainda que se possa utilizar um módulo como se fosse um JAR para retrocompatibilidade), essas classes foram movidas para arquivos JMOD. A partir disso, o arquivo JMOD que contém a classe Object, por exemplo, é o java.base.jmod, localizado no diretório /jmods/java.base.jmod/classes/java/lang/ do novo JDK.
· Extension ClassLoader – Nas versões anteriores do Java, carregava as classes oriundas de bibliotecas presentes do diretório extension do JRE. Entretanto, com o advento da modularização, as classes do diretório extension foram migradas para módulos. Logo, esse class loader continua a carregar as mesmas classes, só que agora a partir de módulos. A classe que implementa esse class loader é a ExtClassLoader, a qual é uma inner class na classe sun.misc.Launcher;
· Application/System ClassLoaders – Responsável por carregar classes que estão no classpath da aplicação (variável de ambiente CLASSPATH). É implementada pela classe AppClassLoader, a qual também é uma inner-class na classe sun.misc.Launcher. Saiba, ainda, que todas as classes criadas pelos desenvolvedores (por exemplo, as classes main) são carregadas por esse class loader.
jShell = Java + REPL
Como já mencionado, o jShell é uma ferramenta REPL de linha de comando para a linguagem Java, possibilitando assim a execução de declarações e expressões Java de forma interativa. Essa funcionalidade é importante porque permite a iniciantes na linguagem executar, de forma rápida, expressões quaisquer, acelerando o aprendizado ao fornecer uma resposta imediata na tela, sem a necessidade de realizar compilação e execução de código em uma ferramenta de desenvolvimento.
Além de ser uma ótima opção para iniciantes, o jShell viabiliza aos desenvolvedores experimentar algoritmos, criar protótipos ou até mesmo tentar utilizar uma nova API. Essa ferramenta pode ser testada por qualquer pessoa que baixar as versões early access do Java 9 e o JAR do projeto Kulla, projeto cujo objetivo é implementar o jShell.
Passos para a instalação e configuração
O primeiro passo para utilizar o jShell é baixar a versão early access do JDK 9. Feito isso, basta descompactar o arquivo para o destino de sua preferência e configurar a variável de ambiente para apontar para esta versão do JDK. Logo após, pressione as teclas Windows+R para visualizar uma janela de execução que solicitará ao usuário o programa que ele deseja abrir. Neste momento, digite cmd e clique em OK.
Nota: Nesse artigo, a pasta na qual o arquivo foi extraído foi a F:\java9\jdk-9. Deste modo, sempre que houver esse caminho no texto, troque pelo caminho da pasta para a qual você extraiu o JDK em sua máquina.
Com o terminal de linha de comando aberto, digite set %JAVA_HOME%=F:\java9\jdk-9 para criar uma variável de ambiente chamada JAVA_HOME cujo conteúdo é o caminho de instalação do JDK. Em seguida, digite echo %JAVA_HOME% para confirmar que a variável foi criada corretamente. Assim, só falta adicionar esta variável à variável de ambiente Path – utilizada pelo sistema operacional – com a execução do seguinte comando: set Path=%JAVA_HOME%\bin;%Path%.
Com o JDK 9 baixado e instalado, o segundo passo é instalar o jShell. Para isso, basta baixar o arquivo JAR (veja o endereço indicado na seção Links) e renomeá-lo para kulla.jar.
Utilizando o jShell
Esta seção tem o intuito de demonstrar, brevemente, as principais funcionalidades do jShell, de modo que o leitor tenha uma melhor compreensão do funcionamento da ferramenta. Portanto, detalhes de implementação serão omitidos, focando apenas nas formas básicas de uso.
O primeiro passo é executar o terminal de linha de comando, tal qual informado anteriormente: teclas Windows + R e digitar cmd.
Com o terminal aberto, mude o diretório do mesmo para o diretório no qual está o arquivo kulla.jar, digitando cd /d f:\java9\kulla\. Uma vez alterado o diretório, o jShell pode ser executado com o comando java -jar kulla.jar.
Ao utilizar o jShell no terminal é possível inserir trechos de código como declarações, variáveis, métodos, definições de classes, importações e expressões que a ferramenta automaticamente tomará as providências necessárias para executá-los e mostrar o resultado.
Deste modo, pode-se, por exemplo, digitar System.out.println("Oi!"); sem a necessidade de criar uma classe e/ou um método para imprimir um texto na tela, conforme expõe a Figura 3.
Figura 3. Imprimindo um texto com o jShell.
Por padrão, o jShell mostra informações descritivas sobre os comandos que foram executados pelo usuário como, por exemplo, na definição de uma variável (vide Figura 4). Essa funcionalidade é importante porque permite ao usuário compreender exatamente o que o jShell fez, principalmente quando se está aprendendo a utilizar a linguagem.
Figura 4. Inicializando uma variável no jShell.
É possível também executar expressões e cálculos em geral. Nesses casos, toda vez que o usuário fornecer uma expressão como 2+2 uma variável temporária será criada com o resultado da operação, de tal forma que o usuário possa fazer referência à mesma posteriormente. Isso pode ser verificado na Figura 5, na qual uma variável temporária, chamada $3, é criada com o resultado da expressão 2+2 e em seguida o valor dessa variável é somado ao valor da variável x (valor = 45), resultando no número 49.
Figura 5. Operações matemáticas e expressões no jShell.
E quando é necessário informar códigos com mais de uma linha, como na definição de métodos e classes, o usuário não precisa se preocupar com detalhes de implementação, pois o jShell consegue detectar que os dados fornecidos pelo usuário estão incompletos. Esse comportamento pode ser observado na Figura 6, na qual foi definido um método que multiplica um número inteiro por 2.
Figura 6. Criando e executando um método Java no jShell.
JMH – Java Microbenchmarking Harness
Outra interessante funcionalidade do novo JDK é o JMH (Java Microbenchmarking Harness). Esse recurso nada mais é do que uma ferramenta/biblioteca para construir, rodar e analisar nano, micro, mili e macro benchmarks (vide BOX 3) que foi desenvolvida pelo russo Aleksey Shipilev (vide BOX 4) e posteriormente integrada ao Java 9 como solução oficial para testes de desempenho na plataforma Java.
Essa ferramenta foi muito requisitada porque o Java não possuía uma opção para realizar esses tipos de medição, dependendo de soluções de terceiros e dificultando, dessa forma, a realização de testes de desempenho confiáveis.
Visto que vários fatores podem distorcer os resultados, como otimizações realizadas pelo próprio compilador ou pelo hardware, a realização desse tipo de teste é uma tarefa difícil. Saiba que quando se trata de testes de desempenho em baixíssima escala, como é o caso de microbenchmarks, cada operação que está sendo medida leva tão pouco tempo que qualquer tarefa que for executada antes ou depois da mesma e estiver dentro do escopo de medição levará mais tempo que a operação em análise, distorcendo assim o resultado.
BOX 3. Micro/nano/mili/macro benchmarking
É a medição de performance de pequenos trechos de código que levam micro/nano/milissegundos para serem executados. Esse tipo de medição é ideal para avaliar a performance de um componente pequeno ou a chamada a um método/função de um programa, por exemplo.
BOX 4. Aleksey Shipilev
Engenheiro de performance em ambientes Java formado em Ciência da Computação pela Universidade de ITMO, em São Petersburgo, que trabalha atualmente na Oracle. Desenvolveu o JMH como um projeto de código aberto com outros colegas, e devido à qualidade deste, a Oracle optou por incorporá-lo como um framework para testes de desempenho no JDK 9, a fim de auxiliar no desenvolvimento de suítes de testes de desempenho mais precisas, bem como facilitar a execução desses testes em pequenos trechos de código. Além desse, Shipilev contribui ativamente com vários projetos de código aberto.
Essa observação é importante porque é possível que o desenvolvedor tenha escrito um trecho de código que o compilador entenda, por exemplo, que não realiza nenhuma tarefa realmente necessária, sendo, portanto, removido pelo mesmo em tempo de compilação, de forma a otimizar a execução da aplicação. Com a otimização do código, no entanto, a aplicação executará mais lentamente do que sem quaisquer intervenções do compilador. Isso porque as melhorias são realizadas apenas quando a aplicação é inicializada, fazendo com que o tempo total para a mesma terminar todas as tarefas de uma determinada execução seja maior, já que contabiliza o tempo levado pelo compilador para otimizar o código, mais o tempo que o código otimizado precisa para finalizar a execução.
Deste modo, os resultados dos testes de desempenho desse trecho de código estarão distorcidos. Com isso em mente, saiba que uma das premissas para bons testes de desempenho é fazer com que a contagem do tempo de execução do teste só inicie após as otimizações no código da aplicação, viabilizando testes que possibilitam obter, com precisão, o tempo levado pela mesma para realizar as tarefas. Esse é o propósito do JMH.
Nota: Com o objetivo de garantir a precisão do tempo medido, o JMH faz uma série de operações que permitem que a contagem do tempo só comece após as otimizações do compilador.
Enfim, com o JMH será possível escrever testes de desempenho mais confiáveis, uma vez que o mesmo é fortemente integrado à plataforma Java, o que facilita o monitoramento do gerenciamento de memória da JVM, das otimizações do compilador, entre outros fatores que podem distorcer os resultados dos testes.
Nota: Não faz parte do escopo deste artigo entrar em detalhes sobre o funcionamento do JMH e como escrever testes de desempenho com essa ferramenta.
Novo garbage collector padrão (G1)
Um dos equívocos que grande parte dos desenvolvedores Java comete é pensar que a JVM possui apenas um coletor de lixo (garbage collector), quando na verdade dispõe de quatro: o serial, o paralelo, o cms e o G1. Relacionado a isso, recentemente a Oracle percebeu que boa parte dos usuários tem uma melhor experiência com as aplicações quando elas rodam sobre um ambiente que faz uso do coletor de lixo G1, em vez do paralelo, que era utilizado como padrão. Assim, a empresa fez uma proposta de melhoria à comunidade, através da JEP 248, para tornar o G1 o coletor padrão a partir do Java 9.
O coletor de lixo paralelo (Parallel GC) tem a vantagem de utilizar vários processos Java para fazer a limpeza da memória e compactá-la, liberando o espaço mais rapidamente para a alocação de mais recursos. Por outro lado, a desvantagem é que para conseguir realizar a limpeza a JVM precisa pausar os processos do software que está rodando, escanear toda a memória do mesmo, com o intuito de identificar quais objetos não podem ser acessados, e descartar esses objetos, para então voltar a executar os processos. Essa paralisação é crítica para grandes aplicativos que precisam ser altamente responsivos, pois quanto mais memória o aplicativo usa, maior o tempo de paralisação e resposta. Um dos motivos para isso acontecer é que assim como os coletores de lixo serial e CMS, o paralelo estrutura a memória utilizável pela JVM em três seções: young generation, old generation e permanent generation, cada uma com um tamanho fixo (veja a Figura 7).
Figura 7. Estrutura de memória da JVM com coletor de lixo diferente do G1.
Por sua vez, o coletor de lixo G1 é mais moderno, tendo sido incluído na atualização 4 do JDK 7, e foi desenvolvido visando a necessidade de suportar melhor uma quantidade de memória disponível para a JVM superior a 4GB, o que passou a ser bastante comum em máquinas mais modernas e, principalmente, em servidores. Para isso, o G1 possui outra estratégia de gerenciamento: dividir a área de memória em blocos de mesmo tamanho, cada um como um pedaço contíguo da memória virtual.
Assim, cada bloco pode assumir o estado de eden, survivor ou old para designar que pertencem às áreas de mesmo nome, tal qual ocorre nos outros coletores, com a diferença de que não existe um número fixo para a quantidade de blocos para cada área, conforme demonstra a Figura 8. Portanto, o tamanho dessas áreas pode variar de acordo com a necessidade do coletor de lixo, o que garante uma maior flexibilidade no uso da memória.
Figura 8. Estrutura de memória da JVM com o coletor de lixo G1.
No momento de executar a coleta de lixo na memória, o G1 percorre todas as três regiões (eden, survivor e old generation) e faz a transição de objetos alocados de uma região para a outra, conforme o tempo de vida de cada um. Em seguida, inicia a desalocação dos objetos pela região mais vazia até a mais cheia, potencializando a liberação de memória. Esse método assume que as regiões mais vazias têm mais probabilidade de possuir objetos que podem ser descartados.
Nota: É importante mencionar que o G1 não executa a coleta de lixo a todo momento. Em vez disso, os desenvolvedores especificam um tempo de pausa aceitável para a aplicação e, baseado nisso, o coletor de lixo adota um modelo de predição para alcançar esse tempo desejado. Além disso, considerando dados de outras coletas, estima quantas regiões podem ser coletadas durante o período especificado.
HTTP 2.0
A versão 2.0 do protocolo HTTP foi definida como padrão em fevereiro de 2015 pelo IESG (Internet Engineering Steering Group) e até o fim desse mesmo ano os navegadores web mais utilizados adicionaram o suporte à mesma. De forma simples, podemos descrever o HTTP 2.0 como uma evolução da versão 1.1, tendo sido baseada no protocolo aberto SPDY, cujo desenvolvimento foi iniciado pelo Google.
Como o HTTP 1.1 data de 1999 e não recebeu melhorias desde então, essa versão possui uma série de limitações/problemas, principalmente devido à evolução da web, a qual passou de uma coleção de hyperlinks e textos para a necessidade de viabilizar a transmissão de conteúdos mais pesados, como áudios, vídeos, interfaces mais complexas, animações, entre outras coisas. Essa evolução evidenciou as limitações do protocolo e serviu de motivação para a criação de uma versão mais nova, a 2.0.
Para compreender melhor esse cenário, considere a seguinte a analogia: imagine que todas as ruas do mundo tivessem sido construídas na época das carruagens e fossem estreitas, esburacadas e com baixo limite de velocidade. Provavelmente levaria um bom tempo para chegar de um ponto a outro, principalmente por causa da velocidade dos cavalos. Agora, imagine que nada mudou nas estradas, mas ao invés de cavalos são utilizados carros. Nesse cenário, a velocidade do cavalo deixa de ser um problema, mas congestionamentos começam a surgir porque a quantidade de carros aumentou (a população cresceu) e eles ocupam mais espaço.
O que ocorre com o tráfego de dados na web não é muito diferente dessa analogia. O protocolo HTTP foi criado há 25 anos e, como já citado, a versão mais atualizada foi padronizada apenas em 1999. Todo esse tempo é uma eternidade quando lidamos com tecnologia da informação, pois em nossa área as evoluções são muito frequentes. Portanto, da mesma forma que as estradas estreitas e esburacadas de outrora, a web, quando da padronização do protocolo HTTP, era bem diferente da atual: as páginas web eram pequenas, as conexões de internet, mais lentas, e os recursos de hardware, limitados e caros. De certa forma, as máquinas e a ausência de banda larga eram o gargalo para “viagens” mais rápidas.
Quando um navegador carrega uma página web utilizando o HTTP 1.1, ele só pode solicitar um recurso (uma imagem, um arquivo JavaScript, etc.) por vez a cada conexão com o servidor, conforme a Figura 9. Deste modo, note que o navegador passa um bom tempo apenas aguardando a resposta de cada solicitação.
Já que o HTTP 1.1 não permite que sejam feitas múltiplas solicitações em paralelo numa mesma conexão, pois analogamente ao caso das estradas, só há uma faixa para ir e voltar, e os navegadores tentam contornar essa limitação criando mais uma conexão com o servidor (criando uma nova faixa na estrada), como pode ser observado na Figura 10.
Figura 9. Modelo de comunicação do HTTP 1.1.
Figura 10. Navegadores tentam paralelizar criando mais conexões.
Acontece que utilizar duas conexões melhora um pouco a velocidade, mas não resolve o problema, pois como pode ser observado na imagem, o navegador ainda passa um bom tempo esperando a resposta da solicitação e só conseguirá baixar dois recursos por vez. Embora seja possível criar até seis conexões com o servidor de destino, essa ainda não é uma boa opção, porque cada conexão será usada de maneira ineficiente, desperdiçando tempo. Neste cenário, se levarmos em conta que uma página comum em um site moderno possui, por exemplo, 100 recursos, o tempo para baixar cada recurso culmina em uma página que será carregada lentamente.
Essa ineficiência para carregar recursos é o motivo da adoção por muitos desenvolvedores web da compactação de arquivos JavaScript e CSS, pois uma vez o recurso possuindo um tamanho menor, o site carregará mais rápido. Entretanto, medidas como essas são apenas formas de contornar o problema e não de resolvê-lo. Embora seja recomendado continuar com a otimização das páginas web para baixar cada vez menos recursos e em tamanhos menores, o problema não será resolvido até que haja uma alteração fundamental na forma de comunicação do protocolo HTTP, maximizando a utilização das conexões e minimizando o tempo de espera/resposta. Eis que surge o HTTP 2.0.
A solução
O HTTP/2 busca fazer melhor uso das conexões com os servidores web ao modificar como as solicitações e respostas trafegam pela rede, o que era uma grande limitação da versão anterior do protocolo. Com a nova versão, o navegador realiza uma única conexão com o servidor web e então pode, de acordo com a necessidade, efetuar várias solicitações e receber várias respostas ao mesmo tempo, como sinaliza a Figura 11.
Figura 11. Modelo de comunicação do HTTP 2.0
De acordo com essa figura, o navegador utiliza uma única conexão e enquanto está enviando mais uma solicitação ao servidor (#6), também está recebendo múltiplas respostas para solicitações feitas anteriormente como, por exemplo, o cabeçalho para o arquivo #3, o corpo do arquivo solicitado pela requisição #1, o corpo do arquivo solicitado pela requisição #3, cujo cabeçalho já foi recebido, e o cabeçalho do arquivo solicitado pela requisição #2.
Note que com apenas uma conexão o navegador consegue enviar várias requisições e receber várias respostas, em ordem diversa, otimizando assim o tráfego de dados. Note, também, que as respostas para uma única solicitação podem ser fragmentadas em vários pedaços menores, como aconteceu com os dados solicitados pela requisição #3. Isso é importante porque permite que o servidor responda mais rápido, ainda que com partes menores. Além disso, evita que o servidor só processe a requisição seguinte após terminar a atual – o que causa gargalo no tráfego das informações – visto que agora tem a capacidade de enviar várias respostas de uma única vez. Logo, ao dividir o tráfego em pequenos blocos, tanto o cliente (que faz a solicitação) quanto o servidor (que devolve o que o cliente pediu) passam a ser capazes de lidar com múltiplas solicitações e respostas, aumentando, portanto, a velocidade do diálogo entre as partes.
Analogamente, essa situação seria como ir a um supermercado e pedir a alguém para pegar uma lista completa de itens de uma única vez, por exemplo: “Pegue o leite, os ovos e a manteiga”. Logo, a pessoa retornará com todas as coisas da lista solicitada. No HTTP 1.1, seria necessário pedir um item por vez, por exemplo: “Pegue o leite” -> “Aqui está o leite”, “Agora pegue os ovos” -> “Aqui estão os ovos” e assim sucessivamente.
É fácil observar que é muito mais eficiente pedir todos os itens de uma só vez, economizando tempo e, assim, agilizando todo o processo. Em outras palavras, toda a comunicação é muito mais eficiente fazendo uso de uma única conexão com multiplexação, como ocorre no HTTP 2.0. Essa abordagem, apesar de um pouco mais complexa de entender, é fundamental para uma internet cada vez mais rápida.
Em primeiro lugar, as conexões não precisarão esperar que um recurso termine de ser transferido para atender a outras solicitações. Por exemplo, suponha que existe uma página web com várias imagens (algo como o Google Imagens) e o navegador precisa fazer uma requisição para receber cada uma. Com a multiplexação, em vez de esperar que o servidor termine de enviar todas as partes de uma imagem para que possa requisitar a próxima, ele pode solicitar um conjunto de imagens ou todas elas de uma vez e começar a recebê-las em partes. Nesta proposta, caso seja menor e possua menos partes a serem baixadas, a segunda imagem, por exemplo, pode ser renderizada antes mesmo da primeira. Essa característica previne um problema bastante conhecido na versão anterior, chamada de head-of-line blocking (bloqueio do primeiro da fila, em tradução livre), que ocorre quando um recurso de tamanho grande (por exemplo, uma imagem de fundo de 1MB) bloqueia todos os outros recursos de serem baixados até que ele seja baixado completamente.
Outra vantagem do HTTP 2 é o conceito de Server Push. Este representa a capacidade de um servidor de enviar um recurso ou conteúdo para um cliente/navegador web sem que o mesmo tenha solicitado. Por exemplo, quando um navegador visita um site, o servidor envia o logotipo ao mesmo sem que seja necessário haver uma solicitação. Essa atuação proativa do servidor permite que os recursos sejam carregados muito mais rápido do que anteriormente.
Vale ressaltar, ainda, que o HTTP 2 funciona melhor utilizando a proteção dos dados trafegados via SSL. A conjunção entre o protocolo HTTP e a criptografia SSL é chamada de protocolo HTTPS. Saiba que, embora seja possível fazer a transferência de dados sem essa proteção na versão 2, o protocolo SPDY exigia a utilização do HTTPS. Deste modo, por questões de compatibilidade com o SPDY, boa parte dos servidores web requer a segurança via SSL, o que acaba sendo positivo, pois força a proteção dos dados dos usuários na comunicação entre computadores.
O que isso significa para o Java?
O Java, desde a versão 1.0, provê a utilização do protocolo HTTP. Entretanto, grande parte do código que garante esse suporte é de um período anterior ao surgimento desse protocolo e por conta disso foi baseado em bibliotecas de comunicação independentes de redes (classe URL, por exemplo). Nesse ponto é interessante lembrar que quando o protocolo foi inserido na API do Java não era esperado que a web pudesse se tornar tão dominante e acessível como é hoje. Além disso, nesse mesmo período, como o HTTPS ainda não tinha sido criado, por motivos óbvios não foi incluído na API, o que ocorreu posteriormente.
Lembre-se, ainda, que o protocolo HTTP e a web possuem décadas de existência e somando-se o fato de não terem acompanhado a demanda (uso de streaming, interfaces responsivas e mais pesadas, entre outros), apresentam hoje uma baixa eficiência para trafegar dados. Como se não pudesse piorar, atualmente a preocupação com a segurança dos dados é tão grande que mesmo os sites mais simples estão adotando o HTTPS. No entanto, esse protocolo é ainda mais lento que o HTTP, pois necessita criptografar e descriptografar tudo o que transporta. Como resultado de tudo isso, a baixa capacidade de lidar com um mundo cada vez mais ágil e conectado e volumes de dados cada vez maiores através da rede culmina em críticas por parte dos usuários, que têm a sua experiência de navegação e utilização comprometidas.
Para dificultar ainda mais, é válido ressaltar que o recurso fornecido pelo JDK ao HTTP também possui grandes limitações, não permitindo, por exemplo, fazer uso do mesmo na íntegra, ou seja, os desenvolvedores, além de terem que lidar com as deficiências do protocolo, não conseguem explorar todos os seus recursos. Essa deficiência do JDK faz com que muitos desenvolvedores adotem bibliotecas de terceiros, como a Apache HttpComponents, o que torna a criação de uma API que forneça suporte completo ao HTTP/2 algo primordial para o Java, visto que esse será um dos recursos mais importantes durante a próxima década.
Em vista disso, a nova API do HTTP – contida no Java 9 – servirá de base para codificar com a versão 2 do protocolo. Para isso, a API Java que era compatível com a versão 1 do HTTP teve seu código reescrito, valendo-se da necessidade de ser independente da versão do HTTP (retrocompatível) e ofertar formas de utilizar as funcionalidades do HTTP/2. Ademais, para utilizar esse protocolo em aplicações Java será bastante simples, bastando implantá-las em servidores web que funcionem com a nova versão desse protocolo. Essa facilidade é um incentivo ao desuso de soluções incomuns (hacks ou gambiarras) que buscavam contornar as limitações do HTTP 1.1 nas aplicações legadas.
Melhorias na API de processos
Atualmente o Java fornece um recurso limitado para o controle de processos (threads ou tarefas) do sistema operacional, pois provê apenas uma API básica para configurar o ambiente e inicializar tarefas, dificultando a manipulação das mesmas. Para conseguir o número de identificação de um processo (PID - Process ID), por exemplo, é preciso usar código nativo do SO. Assim, caso a aplicação seja executada em diferentes sistemas operacionais, é necessário ter uma versão implementada com o código específico para cada um.
A Listagem 10 expõe o código para recuperar o PID de um processo no Linux utilizando a versão atual do Java. Note que há, na segunda linha, um código que faz uso de comandos nativos (por exemplo, /bin/sh) para realizar uma tarefa simples. Com isso, a implementação se torna mais difícil, o mesmo vale para a manutenção, e o código ainda estará mais sujeito a erros. Enfim, é fundamental oferecer uma solução que evite o uso de código nativo para controlar processos.
Listagem 10. Impressão do número de identificação de um processo utilizando a versão atual da API.
public static void main(String[] args) throws Exception{
Process proc = Runtime.getRuntime().exec(new String[]
{ "/bin/sh", "-c", "echo $PPID" });
if (proc.waitFor() == 0)
{
InputStream in = proc.getInputStream();
int available = in.available();
byte[] outputBytes = new byte[available];
in.read(outputBytes);
String pid = new String(outputBytes);
System.out.println("ID do processo: " + pid);
}
}
Com base nisso, o Java 9, através da JEP 102, aprimora a API de processos oferecendo uma solução simples, mas com várias funcionalidades, que permite um maior controle sob os processos do sistema operacional. Dentre as funcionalidades, destaca-se a capacidade de obter o PID ou outros detalhes do processo da JVM sobre a qual a aplicação está rodando, bem como informações de processos criados através da API, como pode ser observado nas Listagens 11e 12.
Na primeira listagem temos a implementação de uma classe que tem a finalidade de obter as informações do processo da JVM que está executando a aplicação e imprimi-las no terminal do sistema operacional. Para isso, a variável processoAtual, da classe ProcessHandle, obtém o controle do processo que está em execução através do comando ProcessHandle.current(). Em seguida, há a execução do System.out.println(), que imprime um cabeçalho, e uma chamada ao método imprimeDetalhesProcesso(), que recebe como parâmetro processoAtual. Esse método obterá as informações do processo através do comando processoAtual.info() e irá armazená-las na variável processoAtualInfo, da classe ProcessHandle.Info. Feito isso, com as informações do processo em mãos, basta imprimi-las no terminal através do println(), que pode ser observado no restante do código.
Listagem 11. Impressão na tela das informações do processo atual com a nova API.
public class ExemploProcessoAtual{
public static void main(String[] args){
//Pega o controle (handle) do processo em andamento
ProcessHandle processoAtual = ProcessHandle.current();
System.out.println("**** Informacao do processo atual ****");
imprimeDetalhesProcesso(processoAtual);
}
public static void imprimeDetalhesProcesso(ProcessHandle processoAtual){
//Retorna uma instância do objeto que referencia as informações do processo
ProcessHandle.Info processoAtualInfo = processoAtual.info();
if (processoAtualInfo.command().orElse("").equals("")){
return;
}
//Imprime o id do processo na tela
System.out.println("PID: " + processoAtual.getPid());
//Imprime o comando de início do processo na tela. Se não tiver
um comando, retornar vazio - orElse(“”)
System.out.println("Comando: " + processoAtualInfo.command().orElse(""))
//Imprime os parâmetros do comando de início do processo na tela
String[] parametros = processoAtualInfo.parametros().orElse(new String[]{});
if ( parametros.length != 0){
System.out.print("parametros: ");
for(String parametro : parametros){
System.out.print(parametro + " ");
}
System.out.println();
}
//Imprime o horário de início do processo na tela
System.out.println("Inicio:" + processoAtualInfo.startInstant().orElse(Instant.now())
.toString());
//Imprime o tempo que o processo está rodando
System.out.println("Rodando:” + processoAtualInfo.totalCpuDuration().
orElse(Duration.ofMillis(0))
.toMillis() + "ms");
//Imprime o nome do usuário ao qual o processo está vinculado
System.out.println("Usuario: " + processoAtualInfo.user()
.orElse(""));
}
}
Já na Listagem 12 temos a implementação de uma classe que cria um processo que abre o terminal do Windows e lista todas as informações desse processo. Essa classe inicia sua execução através da chamada ao método exec() – Runtime.getRuntime().exec(“cmd /k dir”) – o qual retorna um objeto Process que representa o processo recém-criado para a execução do comando cmd /k dir, passado como parâmetro. Em seguida, o método imprimeDetalhesProcesso(), cujo código pode ser visto na Listagem 11, é invocado, recebendo como parâmetro um controlador do processo (ProcessHandle) obtido através de processo.toHandle().
Listagem 12. Código que cria um processo e imprime suas informações.
import java.io.IOException;
import java.io.*;
import java.util.*;
public class Exemplo2{
public static void main(String[] args) throws IOException{
//Cria um processo
Process processo = Runtime.getRuntime().exec("cmd /k dir");
System.out.println("**** Detalhes do processo criado ****");
//Imprime detalhes do processo criado
imprimeDetalhesProcesso(processo.toHandle());
}
public static void imprimeDetalhesProcesso(ProcessHandle processoAtual){
//conteúdo igual ao mesmo método na listagem 11
}
}
A Listagem 13, por sua vez, apresenta um código que demonstra a capacidade de listar processos que estão rodando no sistema operacional, bem como suas propriedades (PID, nome, estado, etc.). Contudo, como a lista de processos é muito grande, para demonstrar mais um recurso da nova API codificaremos alguns filtros para limitar o resultado. Assim, filtraremos os processos que possuam um comando de inicialização associado, de forma a não obter subprocessos – uma vez que eles não possuem comandos para inicializá-los –, e limitaremos a seleção a três processos.
Nota: Quando se fala em subprocessos não ter comandos para inicializa-los, não quer dizer que eles não realizam tarefas, mas sim que os processos que os gerenciam que são responsáveis por inicializá-los, manipulá-los e encerrá-los.
Como precisamos acessar cada processo para obter suas informações, utilizamos o método ProcessHandle.allProcesses(). Esse método retorna uma stream e a partir dela realizamos as filtragens com o comando .filter(processHandle -> processHandle.info().command().isPresent(), que usa o controlador para acessar as informações dos processos que têm um comando associado (isPresent()). Em seguida, pode-se identificar que a lista será restrita a três processos com a chamada a .limit(3). Por último, tem-se a etapa da execução do código que envolve imprimir as informações de cada processo da lista final. Para isso, é chamado o método forEach(), que percorre a lista de processos e imprime no terminal de linha de comando as informações dos mesmos valendo-se do método imprimeDetalhesProcesso().
Listagem 13. Imprime as informações de três processos que estão rodando no sistema operacional
public class ExemploListagemTodosProcessosSO{
public static void main(String[] args){
//lista os processos que estão rodando no sistema operacional
ProcessHandle.allProcesses()
.filter(processHandle -> processHandle.info().command().isPresent())
.limit(3)
.forEach((processo) ->{
imprimeDetalhesProcesso(processo);
});
}
public static void imprimeDetalhesProcesso(ProcessHandle processoAtual){
//conteúdo igual ao mesmo método na listagem 11
}
}
As demonstrações realizadas nas listagens anteriores, principalmente na Listagem 10, reforçam a necessidade de implementação de uma solução para tal problema. Felizmente, a nova versão da API de processos resolve essa antiga pendência, permitindo controlar centenas de processos com uma única thread, diminuindo a complexidade da codificação e maximizando o desempenho das soluções que fazem uso desse recurso.
Portanto, com a nova API os desenvolvedores terão muito mais controle sobre os processos em um sistema operacional, além do óbvio ganho em produtividade e segurança. Apesar disso, é recomendado muito cuidado ao utilizar todo esse controle, pois pode interferir na estabilidade do ambiente que roda a aplicação e por isso é imprescindível ter um conhecimento aprimorado desses recursos.
Como verificado, o Java 9 trará melhorias importantes e há muito esperadas pela comunidade, empresas e desenvolvedores que fazem uso da plataforma. Essas novidades, em sua maioria, têm como meta obter ganhos em termos de desempenho e, principalmente, escalabilidade, a fim de atender também dispositivos com pouco poder de processamento. Sendo assim, podemos afirmar que a novidade de maior destaque é mesmo o sistema de módulos.
Essa nova estrutura do Java possui muitas facetas e por isso alguns detalhes não foram descritos neste documento, devido ao espaço. No entanto, grande dos desenvolvedores precisa conhecer apenas alguns conceitos, como os apresentados, pois são os que realmente serão utilizados no dia a dia, a exemplo da declaração de módulos e da criação de arquivos JAR modulares.
Em relação ao JShell, espera-se que seja uma ferramenta notável para diminuir, principalmente, a curva de aprendizado da linguagem. Logo, a expectativa é que os aprendizes façam bastante uso da mesma, como também os usuários mais avançados, a fim de testar alguns comandos e expressões antes de efetivamente inseri-los nas aplicações. Ademais, essa funcionalidade aproxima a linguagem Java das linguagens de script, as quais já possuem soluções como essa por padrão.
O JMH, por sua vez, é um recurso que deverá ser explorado por desenvolvedores mais avançados, que possuem conhecimento em testes de desempenho, visto que é difícil obter resultados precisos com esse tipo de teste. Entretanto, é uma API de extrema importância, uma vez que o Java não possuía uma forma de implementar tais testes sem recorrer a bibliotecas de terceiros, o que dificultava bastante a obtenção de resultados satisfatórios.
Já a definição do G1 como o coletor de lixo padrão é uma alteração bem-vinda e bastante esperada pela comunidade, após longos debates acerca do assunto. Essa alteração permitirá menos pausas nas aplicações e garantirá uma melhor experiência ao usuário. Nesse momento algumas pessoas podem argumentar que a opção de escolher o G1 como coletor de lixo já existia nas versões anteriores, porém isso não deixa essa mudança menos importante, pois muitos desenvolvedores e administradores não sabiam da existência dessa opção.
Com relação ao HTTP 2.0, é um protocolo que promete ser uma revolução na Internet, ao oferecer maior capacidade de transferência de conteúdo através da multiplexação e compressão de dados. Essa é uma evolução indispensável e que vem para suprir as deficiências da versão atual do HTTP. Apesar de ainda pouco adotado, por ser bastante recente, a tendência é que seu uso cresça exponencialmente ao longo dos anos. Portanto, é fundamental que o Java se antecipe e disponibilize uma API que possibilite o uso do novo protocolo nas aplicações, o que se materializará com o advento da versão 9 da plataforma.
Por último, temos as melhorias na API de processos, que embora pareçam de menor relevância, serão de grande ajuda para assegurar a manutenibilidade do código de aplicações, frameworks e servidores de aplicação (WildFly, GlassFish, WebLogic, Jetty, etc.) que necessitam fazer uso de paralelismo ou acessar processos do sistema operacional. Essas mudanças, assim como a modularização, exigiram bastante trabalho por parte da comunidade, pois foi necessário modificar a API original de cada plataforma para a qual a JVM está disponível.
Como verificado, com o Java 9 grandes avanços serão possíveis, o que torna imprescindível aos desenvolvedores dominar as novidades a fim de tirar o melhor proveito de todos os recursos. Com esse intuito, neste artigo começamos a dar os primeiros passos.
Links
Modularidade
do Java 9.
http://paulbakker.io/java/java-9-modularity/
Especificação
do sistema de módulos.
http://openjdk.java.net/projects/jigsaw/spec/sotms
Especificação
para tornar o G1 o coletor de lixo padrão.
http://openjdk.java.net/jeps/248
Especificação
do JMH.
http://openjdk.java.net/jeps/230
Especificação
das melhorias da API de processos.
http://openjdk.java.net/jeps/102
Especificação
da API do HTTP/2.
http://openjdk.java.net/jeps/110
Projeto
Kulla
https://adoptopenjdk.gitbooks.io/adoptopenjdk-getting-started-kit/content/pt/openjdk-projects/kulla/kulla.html