Most software that runs out of memory simply crashes. On modern operating systems, this happens when programs require more virtual memory than is available. A program that reserves or commits too much virtual memory can run out of free space. When that happens, it can misbehave: Heap allocations may fail, new threads may not start, stacks may fail to grow, and so on. It might exit politely or crash at that point, often without so much as a complaint about what's really gone wrong.
Ideally, your programs gracefully handle out-of-memory conditions and keep running. At the least, they might provide some detailed diagnostic output, or find ways to cope with the situation and survive until resources become available again. Such positive outcomes are possible if the program can identify virtual memory that is not actually required at the time when the low-memory problem occurs. Given modern component-based programming techniques, the program's components typically don't have enough information about each other to understand and comply with one another's virtual memory requirements as the "memory pressure" builds. While the information-hiding aspect of good component-based design is useful in many ways, it can hamper the components' ability to share limited virtual memory resources.
There are two kinds of unused virtual memory that become interesting when your program is memory-starved:
- Reserved virtual memory.
- Unused committed virtual memory.
Reserved virtual memory occupies part of your program's virtual address space, most likely because a component has proactively set it aside, yet it is clearly not being used. Committed, but otherwise unused, virtual memory can be found when components aren't making prudent choices regarding their memory footprints. There's nothing that prevents a component from appropriating either reserved or unused committed virtual memory to prevent a crash, if your components have enough intelligence to recognize suitable ranges that might be appropriated. The appropriated ranges can be used to provide space for the preparation and presentation of diagnostic output, or for any other purpose required to keep your program running when it could not otherwise continue.
Some components proactively reserve virtual memory and don't use it. By reserving virtual memory, a component typically renders it unusable by other components of the same program that are running in the same processunless the other components are "smart" enough to realize they could commit that memory for their own use, rather than cause a crash by running out of memory. You can build this sort of intelligence into your program, making your components smart enough to not die from out-of-memory conditions when reserved, uncommitted memory is available. To do that, you need a small virtual memory analyzer/watchdog component that records information about how virtual memory is used by your program. This information may include a set of timestamps recorded as the program runs, each time virtual memory is reserved. When virtual memory runs low, your other components might need your analyzer/watchdog component to grab the reserved memory region that has the oldest timestamp. The region is then available to be committed for whatever purpose necessary for the program to stay alive.
Some components proactively commit virtual memory and never use it. It may be left in an uninitialized state, or filled with a simple patternusually all NULLs. On most modern platforms, virtual memory is committed at least a page at a time (for example, a page occupies four kilobytes of virtual memory on most versions of Windows). Pages of memory left uninitialized can be decommitted by any component in a process, and no harm results unless the committed memory is actually needed by the component that originally committed it. The same virtual memory analyzer/watchdog component that monitors reserved virtual memory may be extended to make software components smart enough to not die from an out-of-memory condition whenever committed, uninitialized memory is available. The analysis can involve tracking the pages of memory as "in use" or "not in use." One way to tell whether a page is in use is to run a compression routine, such as the LZW algorithm, on the contents of the page. This can be done for each page tracked by your analyzer/watchdog component when an out-of-memory condition occurs and when reserved, uncommitted pages are unavailable. When all or part of a page is found to be initialized, the page can be considered in use. The analysis can also involve recording a timestamp, during the run, each time virtual memory pages are committed. When virtual memory runs low, your other components might need your analyzer/watchdog component to decommit the uninitialized page with the oldest timestamp, so that the page can be reused as needed to keep your program running.