Dynamic Storage Management

Optional readings for this topic from Operating Systems: Principles and Practice: none.

How to manage a region of memory or storage to satisfy various needs?

Two basic operations in dynamic storage management:

  • Allocate a block with a given number of bytes
  • Free a previously allocated block

Two general approaches to dynamic storage allocation:

  • Stack allocation (hierarchical): restricted, but simple and efficient.
  • Heap allocation: more general, but more difficult to implement, less efficient.

Stack Allocation

A stack can be used when memory freeing is predictable: memory is freed in opposite order from allocation.

  • Very efficient

Example: procedure call. X calls Y calls Y again.

Stacks are also useful for lots of other things: tree traversal, expression evaluation, top-down recursive descent parsers, etc.

A stack-based organization keeps all the free space together in one place.

Heap Allocation

Heap allocation must be used when storage freeing is unpredictable

Memory consists of allocated areas and free areas (or holes). Ideally, all of the free space would be in a single contiguous region, but inevitably end up with lots of holes.

Goal: Keep the number of holes small, keep their size large.

Fragmentation: inefficient use of memory because of lots of small holes. Stack allocation has no fragmentation.

Heap allocators must keep track of the storage that is not in use: free list.

Best fit: keep linked list of free blocks

  • Scan the whole list on each allocation
  • Choose block that comes closest to matching the needs of the allocation
  • Put any extra space back on the list.
  • During release operations, merge adjacent free blocks.

First fit: similar to best fit, except

  • Stop scanning as soon as free region is found that is large enough.

Problem with both best fit and first fit: over time, holes tend to fragment, approaching the size of the smallest objects allocated

Bit map: alternate representation of the free list, useful if storage comes in fixed-size chunks (e.g. disk blocks).

  • Keep a large array of bits, one for each chunk.
  • If bit is 0 it means chunk is in use, if bit is 1 it means chunk is free.

Slabs: keep separate memory pools for each popular size.

  • Slab: a region of memory that is divided up into chunks of the same size
  • Separate free list for each slab
  • When allocating, go to the slab(s) for the desired size and allocate from their free lists
  • If no free objects of the desired size, allocate a new slab, chop it up into blocks and put them all on its free list
  • When freeing, return block to the free list of the slab containing it.
  • If all the blocks in a slab become free, then the slab can be freed.
  • Allocation and freeing are fast in the common case where there are free blocks in the slabs.
  • What's wrong with this?

Storage Reclamation

How do we know when dynamically-allocated memory can be freed?

  • Easy when a chunk is only used in one place.
  • Reclamation is hard when information is shared: it can't be recycled until all of the users are finished.
  • Usage is indicated by the presence of pointers to the data. Without a pointer, can't access (can't find it).

Two potential errors in reclamation:

  • Dangling pointers: better not recycle storage while it's still being used.
  • Memory leaks: storage gets "lost" because no one freed it even though it can't ever be used again.

Reference counts: keep count of the number of outstanding pointers to each chunk of memory. When this becomes zero, free the memory. Examples:

  • std::shared_ptr in C++
  • Early versions of JavaScript
  • Inodes in a file system

Garbage collection: storage isn't freed explicitly (no free operation), but rather implicitly: just delete pointers.

  • When the system needs storage, it searches through all of the pointers (must be able to find them all!) and collects things that can't be reached.
  • If structures are circular then this is the only safe way to reclaim space.
  • Garbage collectors typically compact memory, moving objects to coalesce all free space.

One way to implement garbage collection: mark and copy:

  • Must be able to find all objects.
  • Must be able to find all pointers to objects.
  • Pass 1: mark. Go through all statically-allocated and procedure-local variables (data outside the region being collected), looking for pointers (roots). Mark each object pointed to, and recursively mark all objects they point to. The compiler has to cooperate by saving information about where there are pointers within structures.
  • Pass 2: copy and compact. Go through all objects, copy live objects into contiguous memory (must update pointers!); then free any remaining space.

Garbage collection is expensive:

  • 10-20% of all CPU time in systems that use it.
  • Uses memory inefficiently: 2-5x overallocation.
  • Long pauses during garbage collection.