Dentre os campos armazenados na clas- se CacheKey (veja a Figura 3 da parte 1),
podemos destacar o campo type, que é atribuído a uma implementação de uma interface de mesmo nome. A interface org.
hibernate.type.Type define os contratos
de mapeamento entre o mundo orientado a objetos e o banco dados.
A API do Hibernate apresenta dezenas de implementações de Type e toda enti- dade é invariavelmente associada a um conjunto dos mesmos, de acordo com o tipo de seus campos e relacionamentos, o que permite ao Hibernate gerar corre- tamente as instruções SQL nas operações de CRUD. O Type armazenado em Ca-
cheKey corresponde ao tipo do campo
utilizado como identificador da entidade.
Por exemplo, se o id for um campo do tipo
Long, uma CacheKey para uma instância
da entidade irá referenciar uma instância de org.hibernate.type.LongType.
O que é interessante observar é que todas as instâncias dessas classes de mapeamento são de fato metadados e em sua grande maioria são definidos como singletons pelo Hibernate. Dentre os tipos que não são implementados como singletons, estão os tipos que devem ser parametrizados em tempo de execução (durante o “parsing” das entidadesque ocorre na inicialização da SessionFactory que irá gerenciá-las), como por exemplo, o tipo ManyToOneType, que marca qual
entidade é referenciada por uma anotação
@ManyToOne.
Independentemente de serem ou não sin- gletons, as implementações de um deter- minado Type são mantidas em memória e devem ser de natureza imutável, isto é, uma vez que a SessionFactory é cons- truída, os tipos não devem ser alterados. A estrutura responsável por manter os tipos associados a uma determinada en- tidade é a classe org.hibernate.metadata.
ClassMetadata (CMD), que pode ser pen-
sada como sendo a versão do Hibernate de um descritor de tipo do Java (OSC).
Infelizmente a API de C2N do Hibernate não se aproveita do fato de tipos serem singletons (ou mantidos em memória pela
SessionFactory) quando o assunto é seria-
lização, pois trata seus metadados como objetos comuns. Este ‘descuido’ faz com que o benefício da utilização do design pat-
tern desapareça quando um elemento do C2N é lido a partir do disco, considerando que novos objetos são recriados durante o processo de deserialização. Dentre as im- plicações associadas a essa negligência por parte da API de C2N, podemos destacar dois efeitos colaterais:
• O custo de serialização de tipos é eleva- do. Serializar um LongType requer 883 bytes;
• Quando uma CacheKey migra do disco para a memória, ela fica com uma cópia de um tipo do Hibernate em memória. Para entender por que isto acontece, consideremos um cache que armazena alguns objetos em memória e um número muito maior em disco. Podemos imagi- nar o layout inicial da memória como o representado na Figura 7. Nesta figura, as instâncias de CacheKey guardam valores inteiros e consequentemente referenciam o tipo IntegerType do Hibernate. Os valores associados às chaves com identificadores 1 e 2 são mantidos em memória e os outros valores são mantidos em disco.
Quando se submeter uma leitura do identificador de número 3, o valor cor- respondente que estava em disco migrará para a memória e, se o número máximo de elementos em memória for atingido, al- gum elemento da memória poderá migrar
Figura 6. Garbage Collection: estruturas padrão (esq) e otimizadas (dir).
para o disco. Nesse processo, ilustrado na
Figura 8, a chave que foi deserializada do
disco substituirá a chave em memória, e esta nova instância estará associada a uma também nova instância de IntegerType, que deixará de ser um singleton por estar duplicado na memória.
Assim, se você dimensionou seu cache para manter até um milhão de objetos em memória, eventualmente haverá um des- perdício devido às cópias de tipos. Cada instância dos tipos “simples” (LongType,
IntegerType, etc.) custa cerca de 64 bytes
no Heap. Portanto, se houver um milhão de cópias, estamos falando de cerca de 62MB de desperdício.
Queries
Consultas sofrem os mesmos problemas que discutimos anteriormente e podem ser ainda mais agravados dependendo do número e tipos dos parâmetros utilizados, além do tamanho da String de consulta em si.
No primeiro artigo da série verificamos o custo associado ao se adotar Criteria no lugar de NamedQueries, para o C2N em memória. Enquanto que em memória fica evidenciado um ganho da ordem de 200% ao se empregar NamedQueries com C2N em memória, em disco o ganho é muito menos pronunciado (~20%) devido ao fato das consultas serem serializadas e perder- mos o benefício do cálculo de equals como uma operação de tempo constante – O(1) – quando uma String migra do disco para a memória (ver Figuras 7 e 8). Pelas mesmas razões, a redução de memória que obtemos pela escolha de NamedQueries ao invés de Criteria também é anulada quando o C2N é utilizado em disco.
As Tabelas 2 e 3 mostram o resultado da execução do mesmo teste apresentado na seção “Prefira NamedQueries à Criteria” da parte 1, utilizando no entanto, o C2N em Disco. Nesse caso as otimizações apre- sentam um ganho da ordem de 60%.
otimizando as estruturas do C2n
Conforme observamos nas seções ante- riores, os objetos armazenados no C2N não possuem uma estrutura compacta quando serializados. Em poucas palavras
Figura 8. Layout da Memória pós-migração
HDD Sem Cache Cache em Memória Cache em Disco Cache em Disco, otimizado
Toshiba, mecânico, 5400rpm 6867s 140ms 3115 1984 Corsair Neutron GTX, SSD 6605ms 137ms 3080ms 1957ms
Tabela 2. Microbenchmarks de consultas com NamedQuery utilizando diferentes camadas de armazenamento
HDD Sem Cache Cache em Memória Cache em Disco Cache em Disco, otimizado
Toshiba, mecânico, 5400rpm 6001s 412ms 3715 2285 Corsair Neutron GTX, SSD 5795ms 416ms 3635ms 2260ms
Tabela 3. Microbenchmarks de consultas com Criteria utilizando diferentes camadas de armazenamento
poderíamos dizer que “há muita informa- ção estática sendo armazenada de forma desnecessária”. Como a API do Hibernate não é “plugável” com respeito às classes que são utilizadas, se desejarmos otimizá- las temos três possibilidades:
1. Otimizar o mecanismo de serialização do EhCache. Embora possível, uma solu- ção genérica não teria conhecimento das características específicas das classes do C2N do Hibernate;
2. Criar as classes otimizadas, que não violam os contratos do Hibernate, compilá-las e reempacotá-las no JAR do Hibernate;
3. Utilizar um Java Agent, que intercepta classes do C2N no momento em que são carregadas e as transforma em suas ver- sões otimizadas.
Neste artigo optamos pela terceira opção por ser menos intrusiva e nos permitir
testar diferentes formatos sem ter todo o trabalho de reconstruir um JAR. Antes de discutir como vamos utilizar agente, primeiramente vamos mostrar as transfor- mações que desejamos fazer com relação às classes do C2N. Essas transformações essencialmente envolvem implementar o padrão Flyweight de modo a otimizar o acesso a tipos do Hibernate e descritores de classe. A otimização de descritores é comumente empregada em frameworks de caches distribuídos como o próprio EhCache (quando conectado ao Terracotta) e o Oracle Coherence.