Em PHP, o gerenciamento de memória é frequentemente negligenciado até que um aplicativo comece a consumir recursos excessivos. As classes WeakReference e WeakMap, introduzidas no PHP 7.4, oferecem mecanismos poderosos para lidar com referências a objetos sem impedir que o garbage collector os libere quando não há mais referências fortes. Nesta aula, vamos mergulhar fundo nesses recursos, entendendo sua semântica, utilidade e como aplicá-los em cenários reais.

Dominar referências fracas é essencial para desenvolver sistemas robustos e escaláveis, especialmente em contextos como caches, pooling de objetos e gerenciamento de dependências. Vamos começar definindo o que são e como se diferenciam das referências tradicionais.

O que são

Uma referência forte é o padrão no PHP: enquanto uma variável aponta para um objeto, ele permanece vivo na memória. Já uma referência fraca não impede que o objeto seja destruído pelo garbage collector quando não houver mais referências fortes. A classe WeakReference encapsula essa ideia, permitindo que você mantenha uma referência a um objeto sem aumentar sua contagem de referências fortes.

Já o WeakMap é um mapa (dicionário) que usa objetos como chaves e armazena valores, mas mantém as chaves de forma fraca. Isso significa que, se um objeto usado como chave não tiver outras referências fortes, ele será removido automaticamente do mapa, juntamente com o valor associado. Isso é particularmente útil para implementar caches ou metadados associados a objetos sem vazamentos de memória.

Um ponto crucial é que WeakReference e WeakMap só funcionam com objetos. Não é possível criar referências fracas para tipos primitivos (strings, inteiros, etc.). Além disso, eles são projetados para funcionar em conjunto com o coletor de lixo do PHP, que identifica e libera objetos sem referências fortes.

Gerenciamento de memória

O PHP utiliza um garbage collector baseado em contagem de referências e um ciclo de coleta para objetos com referências circulares. Quando você atribui um objeto a uma variável, a contagem de referências aumenta. Quando a variável é destruída ou reatribuída, a contagem diminui. Se chegar a zero, o objeto é destruído imediatamente.

Com referências fracas, a contagem de referências fortes não é incrementada. Portanto, um objeto só é mantido vivo se houver pelo menos uma referência forte. Isso permite que você crie estruturas de dados que observam objetos sem impedir que eles sejam liberados quando não são mais necessários. Um exemplo clássico é um cache: se você armazenar objetos em um cache com referências fortes, eles nunca serão liberados, mesmo se não forem mais usados no resto da aplicação, causando aumento de memória. Usando WeakMap, o objeto é removido do cache automaticamente quando deixa de ser referenciado em outros lugares.

É importante entender que o WeakReference não impede que o objeto seja coletado; ele apenas fornece uma maneira de acessá-lo se ainda estiver vivo. O método get() retorna o objeto ou null se ele já foi destruído. Essa verificação é essencial para evitar erros.

Casos de uso

Os principais casos de uso incluem:

  • Caches de objetos: Armazenar objetos em um cache sem impedir que sejam coletados quando não estiverem mais em uso. Por exemplo, um cache de entidades ORM.
  • Metadados associados a objetos: Armazenar informações extras sobre um objeto (como permissões ou configurações) sem modificar a classe original.
  • Eventos e observers: Manter uma lista de listeners sem criar referências fortes que impediriam que eles fossem destruídos.
  • Pooling de recursos: Gerenciar um conjunto de objetos reutilizáveis, como conexões de banco de dados, onde objetos inativos podem ser liberados.
  • Implementação de memoização: Guardar resultados de funções associados a objetos de entrada, liberando-os quando o objeto de entrada é destruído.

Em todos esses cenários, o objetivo é evitar vazamentos de memória e manter o consumo de memória sob controle, especialmente em aplicações de longa duração, como daemons ou servidores web.

Exemplos

Vamos explorar exemplos práticos. Primeiro, usaremos WeakReference:

class MeuObjeto {
    public $dados;

    public function __construct($dados) {
        $this->dados = $dados;
    }
}

$obj = new MeuObjeto('exemplo');
$ref = WeakReference::create($obj);

echo $ref->get()->dados; // exibe 'exemplo'

unset($obj); // remove a referência forte

var_dump($ref->get()); // NULL, pois o objeto foi destruído

Observe que após unset($obj), o objeto não é mais referenciado fortemente, então o garbage collector pode destruí-lo. A chamada $ref->get() retorna null.

Agora, um exemplo de WeakMap para um cache:

$cache = new WeakMap();

$entidade = new stdClass();
$entidade->id = 1;

$cache[$entidade] = ['dados' => 'carregados do banco'];

// Em outro ponto do código, podemos acessar o cache:
if (isset($cache[$entidade])) {
    echo $cache[$entidade]['dados']; // imprime 'carregados do banco'
}

// Quando $entidade não for mais usada, ela será removida do WeakMap automaticamente
unset($entidade);

// O WeakMap agora está vazio, pois a chave foi coletada
var_dump(count($cache)); // 0

Neste exemplo, o WeakMap armazena dados associados a um objeto. Quando o objeto é destruído, a entrada é removida automaticamente, evitando acúmulo de memória.

Um caso mais avançado: implementar um pool de objetos com WeakMap para reutilizar instâncias. Vamos criar um pool de conexões fictício:

class ConnectionPool {
    private $available = [];
    private $inUse = new WeakMap();

    public function getConnection(): Connection {
        if (count($this->available) > 0) {
            $conn = array_pop($this->available);
        } else {
            $conn = new Connection();
        }
        $this->inUse[$conn] = true;
        return $conn;
    }

    public function releaseConnection(Connection $conn): void {
        unset($this->inUse[$conn]);
        $this->available[] = $conn;
    }
}

class Connection {
    public function query($sql) { /* ... */ }
}

Aqui, o WeakMap rastreia conexões em uso. Quando uma conexão é liberada, ela é removida do mapa e adicionada à lista de disponíveis. Se uma conexão for destruída (por exemplo, por falta de referências), ela será removida automaticamente do WeakMap, evitando vazamentos.

Vale notar que o WeakMap também pode ser usado para armazenar qualquer valor, não apenas booleanos. A chave é sempre um objeto.

Boas práticas

Ao usar referências fracas, lembre-se de sempre verificar se o objeto ainda existe antes de usá-lo. Isso evita erros de "uso de objeto destruído". Além disso, não use WeakReference ou WeakMap como substituto para um design inadequado; eles são ferramentas específicas para gerenciamento de memória. Em aplicações web típicas, onde o ciclo de vida é curto, o impacto pode ser mínimo, mas em serviços de longa duração, essas classes são indispensáveis.

Outra boa prática é documentar claramente quando e por que você está usando referências fracas, pois isso pode confundir outros desenvolvedores. Comente o código explicando a intenção.

Referências

Exercícios

  1. Explique a diferença entre referências fortes e fracas em PHP, e por que referências fracas são importantes para gerenciamento de memória.

    ✓ Resposta: Referências fortes mantêm o objeto vivo na memória enquanto existirem. Referências fracas não impedem que o objeto seja destruído quando não há mais referências fortes. Isso é importante para evitar vazamentos de memória, especialmente em caches ou estruturas de dados que observam objetos sem impedir sua coleta.
  2. Escreva um trecho de código que cria uma WeakReference para um objeto e demonstre que, após remover todas as referências fortes, o objeto é destruído.

    ✓ Resposta:
    class Foo {}
    $foo = new Foo();
    $ref = WeakReference::create($foo);
    unset($foo);
    var_dump($ref->get()); // NULL
    
  3. Crie um WeakMap que armazene a data de criação de objetos. Demonstre que, quando o objeto é destruído, a entrada é removida automaticamente.

    ✓ Resposta:
    $map = new WeakMap();
    $obj = new stdClass();
    $map[$obj] = new DateTime();
    unset($obj);
    // Forçando a coleta? O PHP coleta automaticamente, mas podemos verificar:
    var_dump(count($map)); // 0 (após a coleta)
    
  4. Implemente um cache simples usando WeakMap que armazena resultados de uma função para objetos de entrada. Use uma função que simula um cálculo caro.

    ✓ Resposta:
    function expensiveCalculation($obj) {
        // Simula um cálculo demorado
        sleep(1);
        return $obj->value * 2;
    }
    
    $cache = new WeakMap();
    
    function getCachedResult($obj, &$cache) {
        if (!isset($cache[$obj])) {
            $cache[$obj] = expensiveCalculation($obj);
        }
        return $cache[$obj];
    }
    
    $obj = new stdClass();
    $obj->value = 21;
    echo getCachedResult($obj, $cache); // 42
    
  5. Discuta um cenário em que o uso de WeakMap seria mais vantajoso do que um array associativo tradicional para armazenar metadados de objetos.

    ✓ Resposta: Em um array associativo tradicional, as chaves são strings, e você teria que usar um identificador (como spl_object_id) e armazenar referências fortes aos objetos, impedindo que sejam coletados. Com WeakMap, as chaves são os próprios objetos e as referências são fracas, então quando o objeto não é mais usado, ele é removido automaticamente, evitando vazamentos de memória. Isso é vantajoso em aplicações de longa duração, como um servidor que mantém metadados para muitas entidades.