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 <type>array</type> 'life', 'number' => 42 ); xdebug_debug_zval( 'a' ); ?> ]]> &example.outputs.similar; (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42 ) ]]> Ou graficamente Zvals 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 graficamente Zvals 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 graficamente Zvals 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 <varname>$a</varname> (refcount=1, is_ref=0)='one', 1 => (refcount=1, is_ref=1)=... ) ]]> Ou graficamente Zvals depois da remoção do array com um referência circular demonstrando o vazamento de memória Problemas 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.