Coletor de Lixo
Esta seção explica os méritos do novo mecanismo do Coletor de Lixo (também conhecido
como GC - Garbage Collector) que é parte do PHP 5.3.
Básico sobre Contagem de Referência
Uma variável PHP é armazenada em um contêiner chamado "zval". Um zval
contém, além do tipo e do valor da variável, dois bits adicionais de
informação. O primeiro é chamado de "is_ref" e é um valor booleano
que indica se a variável é parte de um "conjunto de referência" ou não. Com
este bit, o motor do PHP sabe como diferenciar variáveis normais
de referências. Como o PHP permite referências no nível do usuário, como as criadas pelo
operador &, um contêiner zval também tem um mecanismo de contagem de referência
interno para otimizar o uso de memória. Esta segunda parte de informação
adicional, chamado "refcount", contém a quantidade de nomes de variáveis (também
chamadas de símbolos) que apontam para este contêiner. Todos os símbolos são armazenados em
uma tabela de símbolos, e existe uma por escopo. Existe um escopo para o
script principal (ou seja, aquele requisitado através do navegador), assim como um escopo
para cada função ou método.
Um contêiner zval é criado quando uma nova variável é criada com um valor
constante, como em:
Criando um novo contêiner zval
]]>
Neste caso, o nome do símbolo, a, é criado no escopo atual,
e um novo contêiner de variável é criado com o tipo string e o valor
new string. O bit "is_ref" é por padrão definido para &false; porque nenhuma
referência no nível do usuário foi criada. O "refcount" é definido para 1 já
que existe apenas um símbolo que faz uso deste contêiner de variável. Note
que referências (isto é, "is_ref" igual a &true;) com "refcount" igual a 1, são
tratadas como se elas não fossem referências (como se "is_ref" fosse &false;). Se o Xdebug estiver instalado, esta informação pode ser
mostrada chamando-se a função xdebug_debug_zval.
Mostrando a informação zval
]]>
&example.outputs;
Atribuir esta variável a outro nome de variável irá aumentar o "refcount".
Aumentando o "refcount" de um zval
]]>
&example.outputs;
O refcount é 2 aqui, porque o mesmo contêiner de variável está ligado
tanto com a quanto com b.
O PHP é inteligente o suficiente para não copiar o contêiner real da
variável quando não for necessário. Contêineres são destruídos quando
o "refcount" atinge zero. O "refcount" é diminuído em uma unidade quando qualquer
símbolo ligado ao contêiner da variável deixa o escopo (ex.: quando a
função termina) ou quando um símbolo perde a atribuição (ex.: chamando unset).
O exemplo a seguir mostra isso:
Diminuindo o "refcount" de zval
]]>
&example.outputs;
Se agora unset($a); for chamada, o contêiner da variável, incluindo o tipo
e o valor, serão removidos da memória.
Tipos Compostos
As coisas ficam um pouco mais complexas com tipos compostos como arrays e
objects. Ao contrário dos valores escalares, arrays
e objects armazenam suas
propriedades em uma tabela de símbolos própria. Isto significa que o exemplo
a seguir cria três contêineres zval:
Criando um zval de array
'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
?>
]]>
&example.outputs.similar;
(refcount=1, is_ref=0)='life',
'number' => (refcount=1, is_ref=0)=42
)
]]>
Ou graficamenteZvals para um array simples
Os três contêineres zval são: a, meaning, e number.
Regras similares se aplicam para aumento e redução de "refcounts". Abaixo, outro elemento é
adicionado ao array, e define seu valor ao conteúdo de um elemento
já existente:
Adicionando elemento já existente a um array
'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );
?>
]]>
&example.outputs.similar;
(refcount=2, is_ref=0)='life',
'number' => (refcount=1, is_ref=0)=42,
'life' => (refcount=2, is_ref=0)='life'
)
]]>
Ou graficamenteZvals para um array simples com uma referência
Pela saída do Xdebug acima, pode-se perceber que tanto o elemento antigo do array
quanto o novo agora apontam para um contêiner zval cujo "refcount" é
2. Embora a saída do Xdebug mostre dois contêineres zval
com valor 'life', eles são o mesmo. A função
xdebug_debug_zval não mostra isso, mas
pode-se ver isso mostrando o ponteiro de memória.
Remover o elemento de um array é como remover um símbolo de um escopo.
Fazendo isso, o "refcount" de um contêiner ao qual um elemento do array aponta
é reduzido. Novamente, quando o "refcount" atinge zero, o contêiner da
variável é removido da memória. Um exemplo para mostrar isto:
Removendo um elemento de um array
'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
?>
]]>
&example.outputs.similar;
(refcount=1, is_ref=0)='life'
)
]]>
Agora, as coisas ficam interessantes se o próprio array for adicionado como
um elemento do array, o que é mostrado no exemplo a seguir, onde também um operador
de referência foi inserido, senão o PHP criaria uma cópia:
Adicionando o próprio array como um elemento de si mesmo
]]>
&example.outputs.similar;
(refcount=1, is_ref=0)='one',
1 => (refcount=2, is_ref=1)=...
)
]]>
Ou graficamenteZvals para um array com referência circular
Pode-se perceber que a variável do array (a) assim como o segundo elemento
(1) agora apontam para um contêiner de variável que tem um "refcount" de 2.
Os "..." no exemplo acima mostram que há recursão envolvida, e que,
obviamente, neste caso significa que os "..." apontam de volta ao
array original.
Como antes, tirar a atribuição de uma variável remove o símbolo, e a
contagem de referência do contêiner da variável à qual o símbolo aponta é reduzida em uma
unidade. Então, se a variável $a perder a atribuição após execução do código acima,
a contagem de referência do contêiner da variável à qual $a e o elemento "1" apontam
será diminuída em uma unidade, de "2" para "1". Isto pode ser representado assim:
Removendo a atribuição de $a
(refcount=1, is_ref=0)='one',
1 => (refcount=1, is_ref=1)=...
)
]]>
Ou graficamenteZvals depois da remoção do array com um referência circular demonstrando o vazamento de memóriaProblemas na Limpeza
Embora não haja mais um símbolo em nenhum escopo apontando para esta
estrutura, ela não pode ser limpa porque o elemento "1" do array ainda
aponta para este mesmo array. Como não há símbolo externo apontando para
ela, não há como um usuário limpar esta estrutura; e aí acontece o
vazamento de memória. Felizmente, o PHP irá limpar esta estrutura de dados no final
da requisição, mas até lá, ela irá ocupar um espaço valioso na
memória. Esta situação ocorre frequentemente quando se está implementando algoritmos
de interpretação ou outros onde existe um elemento "filho" apontando de volta para um
elemento "pai". A mesma situação também pode com certeza ocorrer com objetos, onde
na verdade existe mais probabilidade de ocorrer, já que objetos são sempre implicitamente
usados "por referência".
Isso pode não ser um problema quanto acontecer somente uma ou duas vezes, mas se
houver milhares, ou até milhões dessas perdas de memória, obviamente
começa a ser um problema. É especialmente problemático em scripts de execução
longa, como daemons onde a requisição basicamente nunca termina,
ou em grandes conjuntos de testes de unidades. Este último já causou problemas durante
a execução de testes de unidades para o componente Template da bilioteca eZ
Components. Em alguns casos, era necessário mais de 2GB de memória, que o servidor
de testes não tinha.
Ciclos de Coleta
Tradicionalmente, mecanismos de memória de contagem de referência, como os
usados anteriormente pelo PHP, falham ao lidar com vazamentos de memória de referência circular;
entretanto, desde a versão 5.3.0, o PHP implementa o algoritmo síncrono do artigo
Concurrent Cycle Collection in Reference Counted Systems
que lida com este problema.
Uma explicação completa de como o algoritmo funciona estaria um pouco além do
escopo desta seção, mas o básico é explicado aqui. Primeiramente,
deve-se estabelecer algumas regras gerais. Se um "refcount" é incrementado, ele
ainda está em uso e, portanto, não é lixo. Se o refcount é reduzido e
alcança zero, o zval pode ser liberado. Isso significa que os ciclos de coleta
somente podem ser criados quando um argumento "refcount" é reduzido para um valor diferente de zero.
Adicionalmente, em um ciclo de coleta, é possível descobrir quais partes são lixo,
verificando se é possível reduzir seus "refcounts" em uma unidade,
e então observando quais dos zvals têm um "refcount" diferente de zero.
Algoritmo de coleta de lixo
Para evitar chamadas de verificação de ciclos de coleta com qualquer
redução possível de um refcount, o algoritmo em vez disso coloca todas as
raízes (zvals) possíveis no "buffer de raízes" (tornando-os "roxos"). Ele também
certifica que cada raiz possível chegue ao buffer apenas uma vez. Apenas quando
o buffer de raízes está cheio é que o mecanismo de coleta se inicia para todos
os diferentes zvals contidos. Veja o passo A na figura acima.
No passo B, o algoritmo executa uma pesquisa em profundidade em todas as raízes possíveis
para reduzir em um os refcounts de cada zval que ele encontra, certificando-se de não
reduzir um refcount no mesmo zval duas vezes (marcando-os de "cinza"). No
passo C, o algoritmo novamente executa uma pesquisa em profundidade a partir de cada nó de raiz,
para verificar o refcount de cada zval de novo. Se ele encontra o valor zero,
o zval é marcado de "branco" (azul na figura). Se ele for maior que
zero, ele reverte a redução do refcount em uma unidade com uma pesquisa em
profundidade daquele ponto em diante, e eles são marcados de "preto" novamente. No último
passo (D), o algoritmo percorre o buffer de raízes removendo as raízes de zval
de lá e, ao mesmo tempo, verifica quais zvals foram marcados de "branco" no
passo anterior. Cada zval marcado de "branco" será liberado da memória.
Agora que há um entendimento básico de como o algoritmo funciona, vejamos
como isto se integra com o PHP. Por padrão, o coletor de lixo do
PHP fica habilitado. Existe, porém uma configuração
do &php.ini; que permite mudar isso:
zend.enable_gc.
Quando o coletor de lixo é habilitado, o algoritmo de pesquisa de ciclos como
descrito acima é executado toda vez que o buffer ficar cheio. O buffer
de raízes tem um tamanho fixo de 10.000 raízes possíveis (embora isso possa ser
alterado mudando-se a constante GC_THRESHOLD_DEFAULT em
Zend/zend_gc.c no código-fonte do PHP, e recompilando-o).
Quando o coletor de lixo é desabilitado, o algoritmo de pesquisa
de ciclos nunca será executado. Entretanto, possíveis raízes serão sempre registradas
no buffer de raízes, não importando se o mecanismo de coleta de lixo tenha
sido ou não habilitado com esta configuração.
Se o buffer de raízes ficar cheio de raízes possíveis enquanto o mecanismo de
coleta de lixo está desabilitado, as possíveis raízes adicionais simplesmente
não serão registradas. Essas raízes não registradas nunca serão
analisadas pelo algoritmo. Se eles fossem parte de um ciclo de referência
circular, eles nunca seriam eliminados e iriam criar um vazamento de memória.
O motivo pelo qual as raízes possíveis são registradas mesmo se o mecanismo
for desabilitado é porque é mais rápido registrar raízes possíveis do que ter que
verificar se o mecanismo está ligado toda vez que uma raiz possível puder
ser encontrada. O próprio mecanismo de coleta e análise de lixo, no entanto,
pode levar um tempo considerável.
Além de mudar a configuração zend.enable_gc,
também é possível habilitar e desabilitar o mecanismo de coleta de lixo
chamando-se gc_enable ou
gc_disable respectivamente. Chamar estas funções tem
o mesmo efeito de ligar ou desligar o mecanismo com a configuração.
Também é possível forçar a coleta de ciclos mesmo se o
buffer de raízes possíveis não estiver cheio. Para isto, pode-se usar
a função gc_collect_cycles. Esta função retorna
quantos ciclos foram coletados pelo algoritmo.
A razão por trás da possibilidade do próprio usuário ligar e desligar o mecanismo, e
iniciar a coleta de ciclos, é que algumas partes de aplicações podem ser
altamente sensíveis a tempo de execução. Nesses casos, pode não ser desejado que
o mecanismo de coleta inicie. Obviamente, desligar o coletor
de lixo para certas partes de uma aplicação cria o risco
de gerar vazamentos de memória porque algumas raízes possíveis podem não
caber no buffer limitado. Portanto, provavelmente é mais sábio chamar
a função gc_collect_cycles logo antes de chamar
a função gc_disable para liberar a memória que poderia ser perdida
através de raízes possíveis que estariam já registradas no buffer. Isso
então leva a um buffer vazio para que haja mais espaço para armazenar
raízes possíveis enquanto o mecanismo de ciclos de coleta está desligado.
Considerações de Desempenho
Já foi mencionado na seção anterior que simplesmente coletar as
raízes possíveis tem um impacto muito pequeno em desempenho, mas isso quando
compara-se o PHP 5.2 com o PHP 5.3. Embora o registro de raízes possíveis
comparado ao não registro, como no PHP 5.2, seja mais lento, outras
mudanças em tempo de execução no PHP 5.3 evitam que esta perda particular
de desempenho apareça.
Existem duas grandes áreas onde o desempenho é afetado. A primeira
é o uso reduzido de memória, e a segunda é o atraso em tempo de execução
quando o mecanismo de coleta de lixo faz suas limpezas de memória. Estes
dois problemas serão mostrados a seguir.
Uso Reduzido de Memória
Primeiramente, o grande motivo pelo qual o mecanismo de coleta de lixo
existe é para reduzir o uso de memória através de limpeza de variáveis
em referência circular assim que os pré-requisitos são preenchidos. Na
implementação do PHP, isso acontece assim que o buffer de raízes fica cheio, ou
quando a função gc_collect_cycles é chamada. No
gráfico abaixo, é mostrado o uso de memória do script a seguir,
tanto no PHP 5.2 quanto no PHP 5.3, excluindo a memória básica que o próprio
PHP usa quando se inicia.
Exemplo de uso de memória
self = $a;
if ( $i % 500 === 0 )
{
echo sprintf( '%8d: ', $i ), memory_get_usage() - $baseMemory, "\n";
}
}
?>
]]>
Comparação de uso de memória entre o PHP 5.2 e o PHP 5.3
Neste exemplo bem acadêmico, está sendo criado um objeto no qual
uma propriedade é definida para apontar para o próprio objeto. Quando a variável $a
no script é re-atribuída na iteração seguinte do loop, um vazamento de
memória normalmente iria acontecer. Neste caso, dois contêineres são vazados
(o zval objeto e o zval propriedade), mas apenas uma raiz possível é
encontrada: a variável que perdeu a atribuição. Quando o buffer de raízes está cheio
depois de 10.000 iterações (com um total de 10.000 raízes possíveis), o mecanismo
de coleta de lixo entra e libera a memória associada com essas
raízes possíveis. Isto pode ser visto claramente com o gráfico irregular
de uso de memória para o PHP 5.3. Depois de 10.000 iterações, o mecanismo entre
e libera a memória associada com as variáveis com referência circular..
O mecanismo em si não tem muito trabalho neste exemplo,
porque a estrutura que é vazada é extremamente simples. Pelo
diagrama, pode-se verificar que o uso de memória no PHP 5.3 é de aproximadamente
9Mb, enquanto que no PHP 5.2 o uso de memória continua crescendo.
Atraso em Tempo de Execução
A segunda área onde o mecanismo de coleta de lixo influencia o
desempenho é o tempo gasto quando o mecanismo entra
para liberar a memória "vazada". Para verificar quanto é este tempo,
o script anterior foi minimamente modificado para permitir um número maior de
iterações e a remoção dos números de uso de memória intermediária. O
segundo script está apresentado a seguir:
Influência do Coletor de Lixo no desempenho
self = $a;
}
echo memory_get_peak_usage(), "\n";
?>
]]>
O script será executado duas vezes, uma com a configuração
zend.enable_gc habilitada,
e outra desabilitada:
Executando o script acima
Em uma máquina específica, o primeiro comando parece levar 10.7 segundos,
enquanto que o segundo leva 11.4 segundos. Isto é um atraso de
aproximadamente 7%. Entretanto, a quantidade máxima de memória usada pelo
script é reduzida em 98%, de 931Mb para 10Mb. Esta referência não é muito
científica, ou mesmo representativa para aplicações do mundo real, mas
demonstra os benefícios de uso de memória que este mecanismo de coleta
de lixo fornece. A boa notícia é que este atraso é sempre de
7% para este script particular, enquanto que as capacidades de
redução de memória economizam mais e mais memória quando referências
circulares adicionais são encontradas durante a execução do script.
Estatísticas de GC Internas do PHP
É possível obter um pouco mais de informação sobre como o
o mecanismo de coleta de lixo é executado no PHP. Mas para fazer isto,
deve-se recompilar o PHP para habilitar o benchmark e o
código de coleta de dados. Deve-se definir a variável de ambiente
CFLAGS para -DGC_BENCH=1 antes de executar
./configure com as opções desejadas. A sequência a
seguir deve fazer este truque:
Recompilando o PHP para habilitar o benchmarking de GC
Quando o exemplo acima for executado novamente com o novo
binário do PHP, o resultado abaixo será visualizado assim que o
PHP terminar a execução:
Estatísticas GC
As estatísticas mais informativas são mostradas no primeiro bloco. Pode-se
ver aqui que o mecanismo de coleta de lixo foi executado 110 vezes, e no
total, mais de 2 milhões de alocações de memória foram liberadas durante estas
110 execuções. Assim que o mecanismo tenha sido executado pelo menos
uma vez, o "Root buffer peak" (pico do buffer de raízes) será sempre 10.000.
Conclusão
Em geral, o coletor de lixo no PHP irá causar um atraso apenas quando
o algoritmo de coleta realmente for executado, enquanto que em scripts normais
(menores), não deve haver nenhum prejuízo no desempenho.
Entretanto, em casos onde o mecanismo de coleta é executado em
scripts normais, a redução de memória que ele vai proporcionar irá permitir
que mais desses scripts possam ser executados ao mesmo tempo no servidor, já que
a quantidade de memória usada no total não será muito grande.
Os benefícios são mais aparentes para scripts de longa execução, como os
scripts de conjunto de testes ou daemons. Adicionalmente, para aplicações PHP-GTK
que geralmente tendem a rodar por mais tempo que scripts para a Web, o novo
mecanismo deve fazer uma diferença considerável em relação a vazamentos
de memória que insistem em acontecer.