Baixe o app para aproveitar ainda mais
Prévia do material em texto
1 ACH2024 Aulas 11 e 12 – Gerenciamento de Memória e Coleta de Lixo Profa. Ariane Machado Lima 2 Programa de ACH2024 Algoritmos para classificação externa em disco e fita. Arquivos, consultas, organizações seqüênciais, técnicas de indexação, indexação cilindro-superfície, árvores-B, tries e hashing. Organização de arquivos: seqüêncial, aleatória e invertida. Estruturas de dados para alocação dinâmica de memória, coleta e compactação de lixo. Estruturas de dados para representação de grafos, algoritmos de busca em grafos. 3 Memória principal Memória utilizada por um programa: Em parte conhecida pelo compilador (estruturas estáticas declaradas no programa, alocadas em tempo de compilação) Em parte não conhecida pelo compilador (caso a linguagem de programação permita alocação DINÂMICA de memória, com estruturas alocadas em tempo de execução por meio de funções como malloc, calloc, new, ...) 4 Memória principal Memória utilizada por um programa: Segmento de texto (ou de código): código de um programa e variáveis alocadas estaticamente Segmento de pilha: alocação de espaço necessário à execução de sub-rotinas (variáveis locais, parâmetros, etc…) Segmento de heap: alocação dinâmica de memória • Obs: nada tem a ver com a estrutura de dados “heap” Segm. de texto Pilha Memória disponível Heap Sentido de crescimento 5 Alocação dinâmica de memória Exemplos: malloc/calloc x free: linguagem C new x delete: Linguagem C++ new x dispose: Linguagem Pascal new: Linguagens Java, Smalltalk Vantagens: Alocar/desalocar à medida que for necessário (economia de memória) Quando o tamanho da memória necessária só é conhecida em tempo de execução Desvantagens: Tempo (de execução) gasto no gerenciamento de memória Implementação do gerenciamento de memória pode ser complicado (para linguagens em que isso não é feito de forma transparente, como C, C++, Pascal) 6 Gerenciador de memória Parte do sistema operacional responsável por manter os blocos de memória disponíveis, alocar a programas e reintegrar blocos liberados, além de gerenciar o acesso compartilhado, transferência de dados de/para o disco, etc. Heap é uma parte da memória dedicada à alocação dinâmica (nada tem a ver com a “estrutura de dados” heap) 7 Outras questões a serem consideradas pelo gerenciador de memória Fragmentação externa: Com o tempo (após alocações/desalocações sucessivas), o heap pode ficar fragmentado, isto é, com vários trechos de memória disponível intercortados por trechos de memória utilizada 8 Outras questões a serem consideradas pelo gerenciador de memória Fragmentação interna: Parte dos blocos alocados não são inteiramente utilizados 9 Gerenciamento da memória: mapa de bits ou listas ligadas [TANENBAUM, A. S. Modern Operating Systems, 4th ed.] 10 Gerenciamento da memória por listas ligadas: procedimento de liberação de um trecho 11 Alocação dinâmica de memória Como o gerenciador irá alocar a memória solicitada no heap terá impacto sobre a fragmentação Trechos consecutivos de memória (de tamanho variável) normalmente organizados em uma lista duplamente ligada Cada trecho tem, além do espaço para armazenamento propriamento dito, quatro campos: dois ponteiros para estar na lista duplamente ligada (anterior e próximo), tamanho e flag disponível/reservado Duas principais estratégias de “VER” a memória e gerenciá-la: Como um vetor sequencial contendo trechos contíguos de memória utilizada e disponível (intercalados): métodos de ajuste sequencial Como várias listas de blocos: métodos de ajuste não sequencial 12 Métodos de ajuste sequencial Vetor sequencial contendo trechos contíguos de memória utilizada e disponível (intercalados em qualquer ordem) Se um trecho de tamanho R for requisitado (por exemplo 8 KB), onde ele será alocado? Depende de qual variação do método de ajuste sequencial está sendo utilizado... utilizadosdisponíveis Posição inicial dos trechos (em KB) 13 Métodos de ajuste sequencial Vetor sequencial contendo trechos contíguos de memória utilizada e disponível (intercalados em qualquer ordem) Se um trecho de tamanho R for requisitado (por exemplo 8 KB), onde ele será alocado? Depende de qual variação do método de ajuste sequencial está sendo utilizado... utilizadosdisponíveis Posição inicial dos trechos (em KB) 14 Métodos de ajuste sequencial variações Primeiro ajuste: seleciona o primeiro trecho encontrado (a partir do início da lista) grande o suficiente Próximo ajuste: seleciona o próximo trecho grande o suficiente (a partir do índice “corrente”, ajustado após a última alocação) Melhor ajuste: seleciona o menor trecho dentre os trechos grandes o suficiente Pior ajuste: seleciona o maior trecho de todos 15 Métodos de ajuste sequencial Vetor sequencial contendo trechos contíguos de memória utilizada e disponível (intercalados em qualquer ordem) Se um trecho de tamanho R for requisitado (por exemplo 8 KB), onde ele será alocado? Depende de qual variação do método de ajuste sequencial está sendo utilizado... utilizadosdisponíveis Posição inicial dos trechos (em KB) 16 Métodos de ajuste sequencial variações 17 Métodos de ajuste sequencial: variações Qual vocês acham que é melhor? Melhor ajuste pode ser o pior: gasta tempo analisando tudo e, a menos que o ajuste seja perfeito, deixa sobrar normalmente trechos pequenos que não podem ser reutilizados Pior ajuste tenta evitar esse desperdício, deixando sobrar trechos maiores que podem ser ainda utilizados, e assim posterga a criação de blocos pequenos Primeiro ajuste é o mais eficiente: balanço entre tempo de achar um bloco de tamanho suficiente (retorna assim que achar o primeiro) e fragmentação (não deixa sobrar sistematicamente o menor ou maior trecho) Próximo ajuste, similar ao primeiro ajuste, mas chega mais rápido ao fim do heap. Nesse caso: Ou gastará mais tempo tendo que rever todo o heap para ver se há trecho disponível Ou considera-se que acabou a memória, e portanto pode ter fragmentado mais. 18 Métodos de ajuste sequencial: variações Qual vocês acham que é melhor? Melhor ajuste pode ser o pior: gasta tempo analisando tudo e, a menos que o ajuste seja perfeito, deixa sobrar normalmente trechos pequenos que não podem ser reutilizados Pior ajuste tenta evitar esse desperdício, deixando sobrar trechos maiores que podem ser ainda utilizados, e assim posterga a criação de blocos pequenos Primeiro ajuste é o mais eficiente: balanço entre tempo de achar um bloco de tamanho suficiente (retorna assim que achar o primeiro) e fragmentação (não deixa sobrar sistematicamente o menor ou maior trecho) Próximo ajuste, similar ao primeiro ajuste, mas chega mais rápido ao fim do heap. Nesse caso: Ou gastará mais tempo tendo que rever todo o heap para ver se há trecho disponível Ou considera-se que acabou a memória, e portanto pode ter fragmentado mais. 19 Métodos de ajuste NÃO sequencial Mais adequados para grandes volumes de memória Memória organizada em várias listas de trechos de memória 20 Métodos de ajuste não sequencial: Estratégia 1: listas tamanho-específicas Várias listas, cada lista contendo trechos de um mesmo tamanho Número de listas potencialmente grande → listas organizadas em uma estrutura de árvore 21 Métodos de ajuste não sequencial: Estratégia 2: ajuste exato adaptativo Baseada na ideia de que alguns tamanhos de trechos solicitados são mais “populares” → manter listas de trechos para esses tamanhos (número menor de listas) Essaslistas armazenam trechos DESALOCADOS durante as T últimas alocações. Quando um trecho de tamanho b é solicitado, se b for um tamanho “popular”, procura na lista dos trechos de tamanho b (ajuste exato). Se não encontrar (ou se b não for um tamanho “popular”) procura na memória usando um método de ajuste sequencial Se após T alocações não foi feita nenhuma solicitação para um trecho de tamanho considerado “popular”, esse tamanho deixar de ser “popular” e sua lista é liberada (ajuste exato adaptativo) 22 Métodos de ajuste não sequencial: Estratégia 2: ajuste exato adaptativo 23 Métodos de ajuste não sequencial: Estratégia 2: ajuste exato adaptativo Algoritmo de alocação: (ERRATA DO LIVRO) bi bi > 25 Comentários finais A fragmentação, em algum grau, é um problema em qualquer algoritmo Para lidar com isso é necessário tomar providências extras. Ex: Compactar memória após certo número de alocações e desalocações Reconstruir a lista de tamanhos após certo tempo (no método de ajuste exato adaptativo) Outra estratégia de alocação que diminua a fragmentação, ex: buddy systems (método sofisticado de vai particionando em dois os trechos disponíveis, utilizando apenas o que for necessário) 26 Referência bibliográfica DROZDEK, Adam. Data Structures and Algorithms in C++. Ed. Cengage Learning. 4a ed. 2013. Cap. 12 (seções 12.1 e 12.2) 27 Coleta de Lixo 28 O problema do vazamento de memória (memory leak) Vazamento de memória: um trecho de memória, ainda não liberado, torna-se inacessível Ex: int* vertices = (int*) calloc(tamanho, sizeof(int)); … /* sequência de comando que NÃO incluem um free(vertices) */ vertices = novo_conjunto_de_vertices; 29 Gerenciamento de memória alocada dinamicamente Em linguagens SEM coleta de lixo (garbage collection): Para cada comando de alocação, um de desalocação quando a memória não for mais necessária Exemplos: malloc/calloc x free: linguagem C new x delete: Linguagem C++ new x dispose: Linguagem Pascal 30 Gerenciamento de memória alocada dinamicamente CUIDADO! Certifique-se de não acessar posições de memória que foram desalocadas! Se não terá o famoso “Segmentation fault”! Ex: int* vertices = (int*) calloc(tamanho, sizeof(int)); … free(vertices); … if (vertices[3] == ….) XXXXX errado!!! não existe mais “vertices” dangling pointer (ponteiro pendente) 31 Gerenciamento de memória alocada dinamicamente BOA PRÁTICA DE PROGRAMAÇÃO: Sempre que for usar um ponteiro que você não acabou de atribuir valor a ele ou usá-lo, certificar-se antes se ele é válido : if (vertices == null) { printf “vertices == null\n”; exit(1); } Sempre que for desalocar: free(vertices); vertices = null; vale a dica de cima também antes do free!!! 32 Gerenciamento de memória alocada dinamicamente Em linguagens orientadas a objeto, cuidar para que os destrutores façam a desalocação corretamente Ex: Objeto que tem atributos alocados dinamicamente (no construtor ou em durante a “vida” do objeto), precisa ter esses atributos desalocados explicitamente (no destrutor ou em outro método). Cuidado especial quando tiver uma hierarquia de classes…. Ao utilizar herança, os destrutores devem “virtual” para garantir uma correta desalocação 33 Coleta de lixo Há linguagens nas quais o usuário não precisa se preocupar em desalocar a memória (ex: LISP, Java, Smalltalk) Isso é feito automaticamente pelo “coletor de lixo” da linguagem Duas tarefas necessárias (que podem ser feitas sequenciamente ou integradas) Marcação: para identificar o que está sendo utilizado (vivo) Regeneração: para liberar o que não está sendo utilizado (morto) e compactar o heap Há vários possíveis algoritmos de coleta de lixo. Ex: Contagem de referências Marcar e varrer (Mark-and-Sweep) Pare e copie Incremental Basedo em gerações 34 Contagem de referências Cada célula de memória (trecho alocado do heap) precisa ter um campo inteiro que seja um contador de quantas variáveis apontam para ela Quando uma variável deixa de apontar para ela, o contador deve ser decrementado Quando o contador chega a zero, a célula é identificada como lixo: se ela tem ponteiros para outras células, os contadores dessas células são também decrementados a célula é reintegrada à parte disponível do heap 35 Contagem de referências - Exemplo b 36 Contagem de referências - Exemplo 37 Contagem de referências - Exemplo Contadores de referência a 38 Contagem de referências - Exemplo b 39 Contagem de referências - Exemplo a X 40 Contagem de referências - Exemplo Xa 41 Contagem de referências Vantagem É uma coleta de lixo “on the fly”, realizada ao longo da execução dos programas, não necessitando parar a execução dos programas para executar o coletor de lixo 42 Contagem de referências Desvantagens Sobrecarga no uso da memória (um contador de referências para cada trecho/célula) Sobrecarga de tempo, durante a execução do programa, atualizando as referências (pode haver a necessidade de várias atualizações recursivas) e reintegrando à memória livre Pode falhar na detecção de estruturas circulares: Contadores de referência 43 Marcar e varrer Executado em algum momento específico, por exemplo quando a memória disponível está abaixo de um limiar Executa as tarefas de marcação e regeneração em duas fases distintas (nas fases marcar e varrer) Ao invés de precisar de um campo inteiro, precisa só de um bit para indicar se a célula está marcada (viva, usada) ou não marcada (morta, não usada) 44 Marcar e varrer Inicialização: marca todas as células do heap como “morta” Fase de marcação: A partir dos ponteiros fora do heap (da pilha e variáveis globais do segmento de texto), marque cada célula alcançada como “viva” e percorra recursivamente os ponteiros dessas células Fase de varredura: Percorra novamente o heap movendo cada célula “morta” para a lista livre (de memória disponível) 45 Marcar e varrer Capaz de identificar estruturas circulares não acessíveis Tempo de execução depende do tamanho do heap, já que todo ele será percorrido pelo menos 2 vezes Desvantagem: precisa de espaço em memória (que já está baixo) para a pilha que faz o percurso recursivo das referências 46 Pare e copie Para não ter que percorrer todo o heap: Divide o heap em duas partes (dois semi- espaços), sendo somente um ativo em cada instante Alocações são feitas no semi-espaço ativo Quando acaba o espaço disponível no semi-espaço ativo: seus trechos vivos são identificados como nos métodos anteriores (a partir das variáveis fora do heap, mas percorrendo os ponteiros em uma busca em largura) mas agora a ação é copiá-los, contiguamente, no segundo semi-espaço (não-ativo) Os dois semi-espaços trocam de papel (ativo ↔ não ativo) 47 Pare e copie Desvantagem: Somente metade do heap está disponível Vantagens: Tempo de execução depende do volume de dados vivos ao invés do tamanho total do heap Executa compactação da memória (isto é, memória disponível está contígua) Promove localização: trechos distintos de valores estruturados podem estar próximos fisicamente na memória, aumentando a chance de estarem na mesma página (usa busca em largura para fazer a cópia) Não usa memória adicional para marcações Não é recursivo (consome menos memória) 48 Coleta de lixo incremental As estratégias “Marcar evarrer” e “pare e copie” vistas até agora baseiam-se em, algum momento, parar a execução do programa corrente e executar a coleta de lixo O tempo de coleta pode ser inaceitável em sistemas de tempo real, nos quais o tempo é uma questão crítica (ex: sistemas de navegação aérea, sistemas de sensores de usinas nucleares, etc…) Para esses casos, a coleta de lixo deve ser feita aos poucos, intercalada com a execução do programa, antes da memória disponível cair a níveis críticos O método de contagem de referências é incremental, mas suas desvantagens o tornam desaconselhável 49 Coleta de lixo incremental Necessidade de um programa mutador para arbitrar quem vai executar a cada instante: o programa ou o coletor Problema: nessa intercalação de execuções, o programa pode alterar estados das variáveis que já foram analisadas na últimas execuções do coletor O coletor então precisa saber o que o programa fez desde sua última execução → maior sobrecarga de execução total 50 Marcar e varrer incremental O que devemos adaptar no marcar e varrer tradicional (abaixo) para torná-lo incremental? Inicialização: marca todas as células do heap como “morta” Fase de marcação: A partir dos ponteiros fora do heap (da pilha e variáveis globais do segmento de texto), marque cada célula alcançada como “viva” e percorra recursivamente os ponteiros dessas células Fase de varredura: Percorra novamente o heap movendo cada célula “morta” para a lista livre (de memória disponível) 51 Marcar e varrer incremental Fase de inicialização: Nada precisa ser alterado Fase de marcação: Todas as células alocadas durante a última interrupção precisam ser marcadas vivas Obs: e se células marcadas como vivas tiverem sido inutilizadas durante a última interrupção? • Serão coletadas apenas na rodada seguinte. Fase de varredura: Como o heap é varrido sequencialmente, basta garantir que alocações sejam feitas sempre em regiões anteriores ao ponto de reinício de varredura 52 Marcar e varrer incremental Fase de inicialização: Nada precisa ser alterado Fase de marcação: Todas as células alocadas durante a última interrupção precisam ser marcadas vivas Obs: e se células marcadas como vivas tiverem sido inutilizadas durante a última interrupção? • Serão coletadas apenas na rodada seguinte. Fase de varredura: Como o heap é varrido sequencialmente, basta garantir que alocações sejam feitas sempre em regiões anteriores ao ponto de reinício de varredura 53 Os semi-espaços ativos e inativos são agora chamados from_space e to_space Sempre que uma célula do from_space é referenciada durante o programa, ela primeiro é copiada para o to_space (no fim) Alocações são feitas no to_space (no topo), e a cada alocação são copiadas k células do from_space para o to_space (no fim) Quando todas as células do from_space são copiadas, os dois semi-espaços trocam de papéis (from ↔ to) Pare e copie incremental 54 Algoritmos baseados em gerações Dados empíricos: a maioria dos dados se tornam lixo pouco tempo depois de criados Ideia: dividir o heap em áreas baseadas na idade das células (gerações) Gerações mais novas sofrem coleta de lixo com mais frequência Novas alocações são feitas na geração mais nova Células que sobrevivem a um certo número de coletas são promovidas a uma geração mais velha 55 Observações A maioria das técnicas de coleta de lixo são conservadoras: só assume-se que uma célula é lixo se ela for inacessível (em muitos casos não dá para prever se uma célula nunca mais será utilizada no programa se ainda houver uma referência a ela) Para cada trecho de memória analisado, é preciso diferenciar o que contém valores propriamente ditos (inteiro, strings, etc) e o que contém ponteiros. Por ex: utilizando alguns bits do trecho para essa rotulação 56 Referência bibliográfica DROZDEK, Adam. Estruturas de Dados e Algoritmos em C++. Ed. Cengage Learning. 2a ed. 2002. Seção 12.3 (voltado à implementação em Lisp) TANENBAUM, A. S. Modern Operating Systems.Pearson Education 4 ed. Seção 3.2.3 Tese: http://www.eecs.berkeley.edu/Pubs/TechRpts/1989/CSD- 89-544.pdf Parte inicial do cap 9 do livro Développement d'applications avec Objective Caml by Emmanuel Chailloux, Pascal Manoury and Bruno Pagano: http://caml.inria.fr/pub/docs/oreilly-book/pdf/chap9.pdf (15 primeiras páginas que tratam do tema em geral) Slide 1 Slide 2 Slide 3 Slide 4 Slide 5 Slide 6 Slide 7 Slide 8 Slide 9 Slide 10 Slide 11 Slide 12 Slide 13 Slide 14 Slide 15 Slide 16 Slide 17 Slide 18 Slide 19 Slide 20 Slide 21 Slide 22 Slide 23 Slide 25 Slide 26 Slide 27 Slide 28 Slide 29 Slide 30 Slide 31 Slide 32 Slide 33 Slide 34 Slide 35 Slide 36 Slide 37 Slide 38 Slide 39 Slide 40 Slide 41 Slide 42 Slide 43 Slide 44 Slide 45 Slide 46 Slide 47 Slide 48 Slide 49 Slide 50 Slide 51 Slide 52 Slide 53 Slide 54 Slide 55 Slide 56
Compartilhar