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.
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. Entretando, 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 limpados 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 é poque é 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 retornará
quantos ciclos foram coletados pelo algoritmo.
A razão por trás da possibilidade do prório 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, desligando-se 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.