Será que minha “birra” com Java é justificada?

Tenho “metido o pau” em linguagens como Java há algum tempo. E, por isso, alguns fanboys fazem questão de me xingar! Eles têm razão, até certo ponto… Quero aproveitar para dizer que minhas análises sobre uma linguagem ou sistema operacional serem uma “bosta”, ou não, referem-se, principalmente, à performance, à velocidade de processamento, quase nunca estabeleço uma análise com base nos “recursos”.

Java é ruim? Não, não é! De fato, muito trabalho foi feito desde a criação da linguagem e do conceito de virtual machines (vm) que fazem de Java uma das excelentes linguagens de programação existentes hoje (aliás, a máquina virtual java é conhecida pela sigla jvm, de Java Virtual Machine)… Melhor até que muitas outras baseadas em scripting, como Python, Perl e PHP. Java pegou o que há de melhor no Smalltalk e misturou com C++, deixando de lado algumas idiossincrasias da última, como construtores de cópia, sobrecarga de operadores e herança múltipla… Ela continua sendo uma linguagem mista, com ênfase em orientação à objetos…

Java é bom? Depende de como vocẽ vê. Do meu ponto de vista (o da performance), C e C++ são imbatíveis − perdendo apenas para o desenvolvedor habilidoso de códigos em assembly, que é impraticável em projetos grandes. Do ponto de vista do controle do código gerado pelo compilador Java também deixa a desejar, já que o código “final” é um pseudo-código (pcode) que será “executado” (interpretado!) pela jvm. O desenvolvedor, fanboy, Java alega que o código é “tão rápido quanto C ou C++”, o que é uma patente mentira, que é facilmente demonstrada medindo o tempo de execução de processos similares… Java, em certos casos, chega a ser quase 100 vezes mais lento que o mesmo código compilado em C e C++ sem otimizações agressivas por parde do compilador daessas duas outras linguagens!

A vantagem de Java sobre C++, por exemplo, é a simplicidade com que as bibliotecas são implementadas. Em C++, se você já mexeu com STL ou boost, deve ter quase arrancado os cabelos com a quantidade de vezes que teve que consultar a documentação para saber exatamente qual é o comportamento de uma classe ou template. Em Java isso também ocorre, no entanto, existem padrões tradicionais e padronizados que facilitam muito o entendimento. A turma do Java costuma usar, pesadamente, design patterns e nomear suas classes com a intenção expressa de dizer qual padrão é usado… Por exemplo, se você ver uma classe com nome SessionAdapter, é quase garantido que essa classe lidará com algo que foi denominado Session e é usada como um adaptador, ou seja, usa o padrão Adapter, mais ou menos como definido pela “gangue dos quatro”.

Existem algumas tradições que me soam muito esquisitas, mas fazem sentido… Um exemplo é a declaração packages como se fossem URIs reversas quando são, na verdade, paths. Uma classe pode ser identificada como, por exemplo, “org.apache.commons.Codec“. Ou seja, temos a classe Codec no path “org.apache.commons”, seja lá onde essa estrutura de diretórios esteja (o diretório base pode ser definido na opção -classpath, tando do compilador javac quando da jvm java). Isso me parece estranho porque coloca esses códigos em uma longa árvore de diretórios… Faz sentido porque isola e organiza os códigos…

A outra ‘birra’ referece ao modelo de gerenciamento de memória. O modelo padrão dos sistemas operacionais é aceitar pedidos de alocação de memória e pedidos de liberação da mesma. Em teoria, enquanto o bloco de memória requisitado ao sistema operacional estiver sendo usado pela aplicação, ele não será descartado sem aviso. Java (e a maioria das linguagens baseadas em script) usam a analogia do coletor de lixo. Seu processo requisita a alocação de memória e, quando não precisa mais dessa memória, simplesmente a deixa de lado, esperando que um lixeiro bondoso faça a colheita, liberando o espaço previamente ocupado para você de forma transparente… É uma boa idéia e nada nova. Até a estensão de meu conhecimento, a primeira linguagem a implementar essa idéia foi LISP (em 1958!). A idéia é boa porque deixa o desenvolvedor livre de preocupações sobre os recursos usados no seu código… em teoria!

Na prática, o Garbage Collector é problemático… Existem dois métodos preferidos de implementá-lo… O primeiro, usado na maioria das linguagens script e em bibliotecas como COM e CORBA é reference counting. Para entender como essa técnica funciona é necessário conceituar referências e objetos. Uma referência é um ponteiro (um endereço) de um objeto concreto (que existe na memória). Todo acesso a objetos é feito por referências (seja de forma direta ou não… uma variável ‘x’, mesmo de tipos primitivos como int, é uma referência para um objeto contido na memória, por exemplo)… A técnica de reference counting funciona mantendo um contador dentro do objeto referenciado… Sempre que criarmos uma referência a um objeto (uma variável que o acessa), incrementamos esse contador. E, sempre que essa referência “sair do escopo” ou for destruída, decrementamos o contador dentro do objeto… Quando esse contador chegar a zero, não há referências ao objeto e ele pode ser, seguramente, apagado da memória.

A  vantagem do reference counting é que os objetos são destruídos imediatamente quando a contagem de referências chega a zero. A desvantagem: Referências circulares podem fazer com que o objeto exista para sempre, ou, pior, em certos casos a contagem pode “estourar” (overflow) e ele ser apagado sem motivo aparente… Este último caso é raro, mas possível.

Java (e .NET) não implementa reference counting no seu Garbage Collector. Outra técnica é usada, chamada de mark and sweep (“marcar e varrer”). Esses ambientes mantém grafos (estruturas parecidas com árvores) de referências (já que podemos ter, por agregação, uma referência dependendo da outra). Quando uma referência local, usada num método, sai do escopo, essa referência é marcada como “em deuso” e a jvm executará, de tempos em tempos, uma thread de baixa prioridade que percorrerá todo o grafo, marcando as referências dependentes das referências “em deuso” como, também, “em deuso”… Mas, se uma referência já estiver marcada como “em deuso”, antes da marcação, uma rotina especial (definida em java.lang.Object) chamada de “finalizador” será chamada (se for implementada na classe do objeto). Neste caso, o objeto não pode ser imediatamente destruído… Isso nos deixa com outro conceito da jvm (e do CLR – Common Language Runtime, do .NET): Espaços generacionais…

Java e .NET categorizam objetos como “novos” ou “velhos”. Sempre que um objeto “novo” não puder ser destruído ele é promovido a “velho”. Ou seja, um objeto “novo” pode sobreviver à sua destruição e tornar-se “velho”. Assim, Java divide o espaço da memória da jvm em duas categorias: Young e Old (ou Eden e Elder). Por causa da ausência de finalizadores na maioria das classes, os objetos na geração Young tendem a sumir mais rapidamente do que na geração Old (mesmo porque, em Java, as coletas nessas gerações são feitas por threads diferentes, com prioridades diferentes).

Desconsiderando os finalizadores, ao encontrar uma referência “em desuso” o GC percorre o grafo procurando por referências que dependem dessa e as marca como “em deuso” também… Como você deve estar considerando, agora, isso toma tempo porque é um procedimento complexo (em termos de performance).

Uma vez que todas as referências foram analizadas e marcadas, o GC passa para a segunda fase: Ele vai se livrar da memória ocupada por todos os objetos cujas referências estão marcadas como “em deuso”. Trata-se de uma varredura simples, linear… portanto, rápida. O problema com isso é que, necessariamente, a região de memória usada pela jvm ficará fragmentada. Quer dizer, existirão diversos blocos pequenos livres, espalhados por todo o grande bloco de memória disponível. Daí, o GC entra na terceira fase: Defragmentação… Todos os objetos que sobreviveram à coleta de lixo são copiados para o início da memória livre, um depois do outro. Para poder fazer isso sem perder as referências (endereços) aos objetos, Java mantém uma outra região de memória chamada Permanent. A região permanente mantém as referências e as outras duas regiões (Young e Old) mantém os objetos em si.

Este é o motivo, em alguns casos, onde a jvm é configurada como tendo um espaço absurdamente grande para as gerações Young e Old e, mesmo assim, o usuário toma uma exceção OutOfMemory na cara! Como todas as referências estão na região Permanent, se não houver espaço suficiente para mantê-las, a jvm simplesmente não tem outra alternativa do que te dizer que “não há memória para as referẽncias”… Para evitar isso existem consigurações para as duas gerações (Young e Old) e para a região Permanent, na jvm: As opções -Xms e –Xmx controlam o tamanho total do “heap” (existem opções para controlar a razão entre o tamanho das gerações Young e Old também!) e a opção -XX:PermSize controla o tamanho dessa outra região.

Deu pra notar que GC, em Java é algo complicado e exige o controle total sobre as referências contidas no processo. Por isso, quando ocorre uma coleta de lixo, acontece também um troço conhecido como Stop-the-world. A thread de baixa prioridade assume o controle do processo, parando a thread principal e todas as threads secundárias, para fazer sua mágica. Literalmente “parando tudo”. Em processos “famintos”, isso pode ter um grave impacto na performance…

Mas é o preço a se pagar para livrar-se dos famigerados memory leaks, né? Ok, mas eles também existem em Java… Note que, as referências dependentes são marcadas como “em deuso” somente se a referência mãe o estiver… Se a referência mãe estiver em uso, as filhas nunca serão marcadas como tal e todos os objetos continuarão “vivos”, mesmo se forem “velhos”. Técnicamente, “memory leak” não é o termo apropriado para usarmos com ambientes gerenciados (Java e .NET), já que o comportamento do GC é bem conhecido (ou, pelo menos, a analogia)… É esperado que se referências mãe não estejam “em deuso”, as filhas as sigam! O termo correto é loitering. Mas, prefiro “memory leak”, de qualquer jeito.

Esses “memory leaks” podem acontecer em casos como:

  • Declarar uma referência como membro de instância de um objeto “persistente”;
  • Obter uma referência de um objeto que está associado a um recurso do sistema operacional

São apenas dois exemplos (existem outros). Mas, são casos diferentes… O primenro é óbvio: A referência declarada como membro de instância é dependente da referência da instância… Se a referência da instância nunca for marcada como “em deuso”, a filha também sobrevive. O segundo caso é mais complicado… O processo java (a jvm) lida com o sistema operacional e, se usar recursos deste, referências podem jamais serem liberadas se explicitamente não nos livrarmos desses recursos… Por exemplo: Se instanciarmos um objeto que lida com uma fonte (da Graphical User Interface) e não informarmos ao sistema operacional que não precisamos mais do objeto, o processo java poderá jamais livrar-se do objeto que acomoda o recurso “fonte”.

Este é o motivo do porque algumas classes, em Java, implementarem a interface java.lang.Closeable. Ou seja, terem que implementar uma função close() no corpo da classe. É, mais ou menos, o caso do framework Hibernate e sua classe HibernateSession. Uma sessão, nele, precisa ser obtida de HibernateSessionManager e, quando não for mais necessária, o método close() precisa, necessariamente, ser chamado… Se não fizer isso, o objeto da classe HibernateSession jamais morrerá (porque HibernateSessionManager mantém uma lista de referências, mas também porque conexões ao banco de dados podem – e geralmente são – implementadas como chamadas a uma biblioteca binária!).

Então… Java é bom? É! Mas, como sempre, para usar um troço direito, você tem que saber exatamente o que está acontecendo, por debaixo dos panos… Senão, aquela “facilidade” acaba se tornando um “pesadelo”. Já perdi a conta de quantos sistemas complexos, construídos em Java, sofrem do mal do alto consumo de memória (o que é normal na analogia do GC), mas também sofrem de “fome” porque não conseguem se livrar de objetos que deveriam tornarem-se descartáveis com o tempo…

Anúncios

12 comentários sobre “Será que minha “birra” com Java é justificada?

  1. Não tem nem comparação uma linguagem multiparadigma que oferece opções para praticamente tudo comparar a uma linguagem multiplataforma com a ideia de “facilitar” a curva de aprendizado. Entretanto como o tempo de desenvolvimento tende a ser cada vez mais curto, e prazos cada vez mais apertados, é natural que a padronização em linguagens de programação como o java tem a tendência a aumentar sua utilização, pois como já mencionado, c++ não raro exige muita leitura de documentação, e isso para muitos diretores é perda de tempo. Logo justifica-se o uso maior de java em detrimento de c++. Óbvio que para fazer algo mais elaborado, com mais opções ainda temos de recorrer o bom e velho C/C++, Assembly. Não há dúvida de que a abstração de dados, o deixar de preocupar-se com, proporcionou um salto enorme no desenvolvimento, por exemplo; Consegue imaginar quem programará o software ter de programar o firmware do hd para poder utilizá-lo ? Provavelmente existem problemas, badblocks que seria possível eliminar programando o firware. A questão é até que ponto vale a pena fazer isso ??

    1. Essa é a desculpa que todo desenvolvedor Java tem na ponta da lingua, Danilo… O fato é que Java não oferece tanta produtividade assim… O desenvolvedor Java AINDA tem que estudar um bocado sobre a sopa de letrinhas começadas com J de todas as metáforas usadas nos frameworks que usa e, se quiser que seu projeto seja “robusto” terá que entender o funcionamento da JVM… Sem contar com um conhecimento mais ou menos profundo de abstrações (vide Design Patterns), dentre outras tantas coisas…

      Digo por experiência: NUNCA vi um projeto em Java que não tenha problemas graves graças à pressa em obter “produtividade”…

      1. Isso é óbvio, entretanto quando quem está pagando para desenvolver exige que seja em java, devido a pressa na produtividade. O que podemos fazer ? Não somos obrigados a desenvolver em java ?

      2. É claro… o chefe é quem manda… mas isso não significa que Java seja uma boa linguagem ou a JVM seja um bom ambiente…

        Se alguém te paga para tirar pregos de madeira com os dentes você não poderá usar uma ferramenta…

    2. E sim… comparar performance entre ambientes operacionais é justo. Especialmente se um dos lados diz que sua aproximação é “melhor” que a outra…

  2. Acredito que o que ocorre é a confusão do melhor que, com a performance semelhante a … Para uma plataforma que de imediato “simplifica” a complexidade do desenvolvimento, focando mais nas regras de negócio a maioria acredita que compensa o overhead na performance. Muitos justificam que o número de núcleos por processador aumenta cada vez mais, bem como a velocidade, hoje em torno dos 3.8 Ghz por núcleo, Algo que era inimaginável a 20 anos atrás. Até me peguei pensando, poxa um celular de quatrocentos reais hoje tem mais memória RAM que o computador desktop que via ser utilizado na escola ( 128 MB ). É impossível negar o poder do C++, entretanto a fatia de mercado certamente tem a tendência a diminuir

    1. Acontece que multiprocessamento não necessariamente “aumenta a performance”. A discussão sobre threads (acredito que eu tenha escrito vários artigos sobre isso), demonstra…

      1. Meu Frederico, nós estamos na internet e fazendo papel de bobos, como especialista na tecnologia, sabemos que existem processos que possuem subrotinas, como cálculo de matrizes que podem ser processador paralelamente, se assim não fosse a Intel não fabricaria processadores com mais de um núcleo https://software.intel.com/pt-br/articles/o-que-o-intel-xeon-phi-e-como-ele-atinge-o-impressionante-processamento-de-1-tflops Meu chega de discussões radicais acaloradas ao estilo xiita … O fato é que a 20 anos atrás era inimaginável alcançar 3.8 Ghz por núcleo como hoje (sem overclock 5 Ghz com overclock), é óbvio que algo feito com camada de abstração mais baixa pode ser muito mais eficiente, mas entretanto com a velocidade dos recursos atuais mais camadas de abstração, como o uso de máquinas virtuais, linguagens de programação podem compensar a perda de desempenho, principalmente quando o tempo de desenvolvimento e entrega do produto final é mais rápido. Mas eu não comentarei novamente. Até por que para um especialista, defender a posição de que determinada tecnologia sempre será mais adequada para determinada solução, no mínimo depõe contra. Cada ferramenta/linguagem se propõe a uma coisa, c/c++ tem propósito de uso geral, mas o preço pago por isso é a complexidade de desenvolvimento, e programadores que conheçam as regras de negócio nos mínimos detalhes são cada vez mais raros de encontrar, até por que como no Brasil existe muito mais mão de obra que oferta de trabalho, prefere-se não investir muito no programador, se ele está ganhando demais, manda ele embora e comece a treinar outro … Nisso as novas plataformas estão ganhando cada vez mais mercado no Brasil … Nos EUA onde existem muito mais oportunidades do que mão de obra linguagens como C++, imperam, a pópria Java Virtual Machine é feita em C++, Linux, Unix, BSDs … Obviamente um especialista como o Frederico Lamberti Pissarra não terá necessidade de enfrentar um mercado com alta rotatividade, pois conhece muito bem regras de negócio, que nós novatos levaremos anos, talvez décadas para aprender. Um especialista único no país … Abração Frederico você realmente é o cara!

      2. Danilo, sério que você está fazendo questão de vir ao MEU BLOG encher o MEU SACO com essa atitude irônica?

  3. Adorei este artigo. Sempre ouvi vc falar desta sua birra. Agora eu acho que entendi a explicação. Só não gostei quando vc fala mal do lisp e do python (>-()…

    Agora o que eu soube há pouco é que há uma luta no chrome contra o “java”, devido às exposições do sistema devido aos problemas de segurança. A Google pretende, pelo que entendi, acabar com a interface em setembro vindouro…

    Isto eu li pois estou com um lindo cartão smart (de certificado) da merda da caixa, que tem a merda do java, que dá uma merda de problema quando tento inicializa-lo. Graças a este mico, estou tendo que entender um pouco deste “disquete” moderno chamado smartcard.
    Veja sobre o “end of java” aqui: https://www.aeteurope.nl/our-company/news/20-safesign-ic-for-ios-supports-all-major-java-cards

    1. Hummmm… Provavelmente os certificados são armazenados como JKS (Java KeyStore)… Não tem um “java” no smartcard, que é, essencialmente, um “pendrive” com pouca memória! :)

      Não sabia do lance do Google…. :)

      [[]]ão, amigão! :)

  4. Minha única barreira com java é o fato de não ter um framework de interface desktop decente, todos são lixo, awt, javafx, essa porcariada toda. Se tivesse um fw que prestasse eu largaria do C# e migraria para o Java.
    Atualmente se eu precisar fazer algo em linux faço em PHP ou recuso o serviço se não for possível.

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s