heuripedes - home

Paper: Garis de memória

09 Nov 2011 - Higor Eurípedes

Repostagem do paper que escrevi para a e-zine Cogumelo Binário.

			    GARIS DE MEMÓRIA

		 Higor Eurípedes (a.k.a. enygmata ou fmul)
		       heuripedes at gmail dot com
			    Outubro de 2011



Conteúdo
==============================================================================

1 - Introdução
2 - O que são garbage collectors
2.1 - Alguns conceitos interessantes
3 - Tipos
3.1 - Tracing Garbage Collectors
3.2 - Reference Counting Garbage Collectors
3.2.1 - Um refcounted GC por dentro
4 - Citações, links e referências



1 - Introdução
==============================================================================

Na-não, espere! Este não é um texto sobre os nossos mal tratados lixeiros, 
menos ainda sobre Alzheimer. Estou aqui para falar sobre os Garbage 
Collectors, ou GCs.

Neste artigo irei utilizar os termos "bloco" e "objeto", entenda-os como sendo 
regiões na heap do programa a menos que fique explícito que estou falando de 
objetos do paradigma de orientação à objetos.



2 - O que são garbage collectors
==============================================================================

Coletores de lixo ou garbage collectors são peças de código que tem a função 
de liberar recursos que não são mais utilizados pelo programa a.k.a. o lixo do 
programa. Eles são comumente utilizados no gerenciamento de memória de 
linguagens de alto nível (principalmente naquelas que não permitem 
gerenciamento explícito) e por bibliotecas que permitem o compartilhamento de 
objetos entre programas.

Os GCs livram o programador de algumas preocupações, como chamar free() menos 
ou até mais que o necessário (causando erros double free), mas não é garantia 
de que não ocorrerão vazamentos de memória. Utilizar coletores de lixo também 
diminui as chances de que ocorram situações onde não existe nenhum ponteiro 
apontando pra um determinado bloco na heap, os rebeldes dangling pointers.

Mas, como diz o ditado e o meu colega sigsegv, "rapadura é doce mas não é 
mole". Nem tudo são flores no mundo dos garbage collectors, o uso de coletores 
implica em custos pro sistema e o uso incorreto pode causar pausas em 
programas multitarefa, picos de processamento aleatórios e fragmentação da 
memória do programa.


2.1 - Alguns conceitos interessantes
------------------------------------------------------------------------------

Quando falamos sobre garbage collectors repetimos sempre o termo "referência".  
As referências são ligações entre objetos na memória, são ponteiros, e podem 
ser classificadas quanto a força que exercem sobre a vida do objeto 
referenciado. As fracas são referências que muitas vezes sequer contem 
informações sobre o objeto referenciado (são ponteiros "crus") e não previnem 
que o mesmo seja coletado. Objetos referenciados fortemente não são coletados 
até que todas as referencias sejam removidas.

Outros conceitos importantes são "coleta" e "reciclagem", o primeiro implica 
em liberar a memória e o segundo significa informar o alocador que aquela 
região está disponível para reuso. A implementação destes dois conceitos vai 
depender dos requisitos do sistema.


3 - Tipos
==============================================================================

Basicamente, existem duas vertentes entre os coletores de lixo: os tracing 
garbage collectors (tracing GC) e os reference counting garbage collectors 
(refcount GC). É possível também encontrar híbridos. Todos eles, porém, exigem 
que o programador utilize funções de alocação específicas ou que de alguma 
forma identifique a memória alocada como passível de coleção.


3.1 - Tracing Garbage Collectors
------------------------------------------------------------------------------

Os tracing GCs, em geral, trabalham escaneando a memória em busca de 
referências que possam ser alvos de coleta. Estes coletores costumam ser 
cíclicos, pois não coletam os objetos assim que deixam de ser utilizados, mas 
durante alguma etapa do ciclo de coleta.

Como o GC se comporta depende do algoritmo utilizado, o mark-sweep, por 
exemplo, possui três etapas: na primeira o GC identifica as referências; na 
segunda ele marca os objetos que ainda possuem referências; e, na terceira 
etapa, o GC coleta todos os objetos que não estão marcados.

A linguagem Java (por padrão) e as linguagens que são implementadas com a 
Common Language Infraestructure da .NET Framework utilizam um tipo de tracing 
GC chamado efêmero ou geracional, este GC técnicas de heurística para 
encontrar blocos não utilizados e por isso possui um certo nível de não 
determinismo, ou seja, torna-se difícil prever quando ocorrerá um ciclo. O uso 
deste tipo de GC pode causar dores de cabeça em sistemas com carga alta como o 
que aconteceu no site de perguntas e respostas Stack Overflow (veja o link 
sobre o GC da .NET no fim do artigo).


3.2 - Reference Counting Garbage Collectors
------------------------------------------------------------------------------

Os refcounting GCs são mais humildes que seus primos e utilizam somente 
contadores de referência para determinar quando um bloco está pronto para ser 
coletado. A ideia é simples, sempre que um bloco estiver com zero referências,
ele estará pronto para ser coletado ou reciclado. Diferente dos tracing GCs, 
os refcounting GCs costumam guardar a informação sobre as referência junto ao 
bloco que é alocado para o usuário, muitas vezes na forma de cabeçalho. Este 
tipo de coletor é bastante utilizado em aplicações que necessitam que os 
recursos sejam liberados o mais cedo possível.

Uma desvantagem dos coletores baseados em contagem de referência é que eles 
não lidam com referências cíclicas sem que a complexidade aumente 
consideravelmente; na linguagem Perl, por exemplo, objetos com referência 
cíclica só são liberados numa etapa de coleta que ocorre no fim do programa.  
A solução mais simples nesses casos é utilizar um misto de referências fracas 
e fortes entre os objetos envolvidos ou simplesmente proibir que este tipo de 
referência aconteça.

Além da Perl, as linguagem Python e PHP e as bibliotecas GObject (parte da 
GLib e base da biblioteca Gtk+) e COM+ (uma biblioteca para IPC nos sistemas 
Windows) também utilizam contagem de referência para evitar problemas com 
alocação de recursos.

3.2.1 - Um refcounted GC por dentro
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Como dito antes, os blocos alocados pelos coletores possuem um cabeçalho e 
também um corpo, onde ficam os dados do usuário. O cabeçalho, em geral, tem um 
tamanho fixo, o suficiente para armazenar o contador de referência.

  Bloco alocado pelo coletor:
    +--------------+------------------->
    | Cabeçalho    | Corpo 
    +--------------+------------------->
    ^-Tamanho fixo-^-Tamanho variável-->

Em C você pode considerar que a estrutura básica do coletor é definida da 
seguinte forma:

  typedef struct {
      size_t refcount; /* Número de referências. */
  } GCHeader;

Nota: O corpo do bloco não tem delimitação e por motivos de portabilidade não 
vamos definir um membro ou estrutura pra ele.  Vale lembrar que é possível 
criar uma estrutura para representar todo o bloco utilizando arrays de tamanho 
variável (C99) ou zero-length arrays.

No momento da alocação, o tamanho do cabeçalho é somado ao tamanho do bloco 
que o usuário deseja alocar e em seguida o contador é inicializado com o valor 
um. O ponteiro pro bloco recentemente alocado é movido para a próxima posição, 
ele agora aponta para o corpo do bloco.

  /* aloca um bloco e retorna seu corpo */
  void *gc_alloc(size_t size) {
      size_t new_size = size + sizeof(GCHeader);
      GCHeader *block = malloc(new_size);

      block->refcount = 1;
      block++;

      return block;
  }

Para facilitar o trabalho, é uma boa ideia criar funções para acessar a região 
do cabeçalho:
 
  /* retorna o cabeçalho do bloco */
  GCHeader *gc_get_header(void *body) {
      return ((GCHeader*)body)-1;
  }

E agora a implementação das funções que aumentam ou diminuem o numero de 
referências. A função para aumentar o número de referências não tem mistério, 
é só pegar o ponteiro pro cabeçalho e somar um à refcount:

  /* adiciona uma referência ao bloco e retorna um ponteiro para o corpo */
  void *gc_ref(void *body) {
      GCHeader *header = gc_get_header(body);

      header->refcount++;

      return body;
  }

Já a função para diminuir o número de referências tem é um pouquinho mais 
complicada: precisamos checar se a quantidade de referências chegou a zero e 
liberar a memória se isso acontecer.

  /* remove uma referência e retorna um ponteiro para o corpo (ou null caso 
     não hajam mais referências */
  void *gc_unref(void *body) {
      GCHeader *header = gc_get_header(body);

      header->refcount--;

      if (header->refcount == 0) {
	  free(header);
	  body = NULL;
      }

      return body;
  }

É importante notar que a implementação apresentada aqui não é aconselhada para 
uso em programas multitarefa, pois não utiliza nenhum mecanismo de 
sincronização ou operações atômicas.



4 - Citações, links e referências
==============================================================================

- In managed code we trust, our recent battles with the .NET garbage collector
  http://samsaffron.com/archive/2011/10/28/in-managed-code-we-trust-our-recent-battles-with-the-net-garbage-collector

- Two-Phased Garbage Collection
  http://perldoc.perl.org/perlobj.html#Two-Phased-Garbage-Collection

- Arrays of Length Zero
  http://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html#Zero-Length

- Object memory management
  http://developer.gnome.org/gobject/stable/gobject-memory.html

- The Memory Management Reference: Beginners Guide: Recycling
  http://www.memorymanagement.org/articles/recycle.html
^ Topo