Legacy Documentclose button

Important: The information in this document is obsolete and should not be used for new development.

Previous Book Contents Book Index Next

Inside Macintosh: Memory /
Chapter 1 - Introduction to Memory Management


Using Memory

This section describes how you can use the Memory Manager to perform the most typical memory management tasks. In particular, this section shows how you can

The techniques described in this section are designed to minimize fragmentation of your application heap and to ensure that your application always has sufficient memory to complete any essential operations. Many of these techniques incorporate the heap memory cushion and emergency memory reserve discussed in "Low-Memory Conditions," beginning on page 1-36.

Note
This section describes relatively simple memory-management techniques. Depending on the requirements of your application, you might want to manage your heap memory differently.

Setting Up the Application Heap

When the Process Manager launches your application, it calls the Memory Manager to create and initialize a memory partition for your application. The Process Manager then loads code segments into memory and sets up the stack, heap, and A5 world (including the jump table) for your application.

To help prevent heap fragmentation, you should also perform some setup of your own early in your application's execution. Depending on the needs of your application, you might want to

The following sections describe in detail how and when to perform these operations.

Changing the Size of the Stack

Most applications allocate space on their stack in a predictable way and do not need to monitor stack space during their execution. For these applications, stack usage usually reaches a maximum in some heavily nested routine. If the stack in your application can never grow beyond a certain size, then to avoid collisions between your stack and heap you simply need to ensure that your stack is large enough to accommodate that size.
If you never encounter system error 28 (generated by the stack sniffer when it detects a collision between the stack and the heap) during application testing, then you probably do not need to increase the size of your stack.

Some applications, however, rely heavily on recursive programming techniques, in which one routine repeatedly calls itself or a small group of routines repeatedly call each other. In these applications, even routines with just a few local variables can cause stack overflow, because each time a routine calls itself, a new copy of that routine's parameters and variables is appended to the stack. The problem can become particularly acute if one or more of the local variables is a string, which can require up to 256 bytes of stack space.

You can help prevent your application from crashing because of insufficient stack space by expanding the size of your stack. If your application does not depend on recursion, you should do this only if you encounter system error 28 during testing. If your application does depend on recursion, you might consider expanding the stack so that your application can perform deeply nested recursive computations. In addition, some object-oriented languages (for example, C++) allocate space for objects on the stack. If you are using one of these languages, you might need to expand your stack.

Note
If you are programming in LISP or another language that depends extensively on recursion, your development system might allocate memory for local variables in the heap rather than on the stack. If so, expanding the size of the stack is not helpful. Consult your development system's documentation for details on how it allocates memory.
To increase the size of your stack, you simply reduce the size of your heap. Because the heap cannot grow above the boundary contained in the ApplLimit global variable, you can lower the value of ApplLimit to limit the heap's growth. By lowering ApplLimit, technically you are not making the stack bigger; you are just preventing collisions between it and the heap.

By default, the stack can grow to 8 KB on Macintosh computers without Color QuickDraw and to 32 KB on computers with Color QuickDraw. (The size of the stack for a faceless background process is always 8 KB, whether Color QuickDraw is present or not.) You should never decrease the size of the stack, because future versions of system software might increase the default amount of space allocated for the stack. For the same reason, you should not set the stack to a predetermined absolute size or calculate a new absolute size for the stack based on the microprocessor's type. If you must modify the size of the stack, you should increase the stack size only by some relative amount that is sufficient to meet the increased stack requirements of your application. There is no maximum size to which the stack can grow.

Listing 1-3 defines a procedure that increases the stack size by a given value. It does so by determining the current heap limit, subtracting the value of the extraBytes parameter from that value, and then setting the application limit to the difference.

Listing 1-3 Increasing the amount of space allocated for the stack

PROCEDURE IncreaseStackSize (extraBytes: Size);
BEGIN
   SetApplLimit(Ptr(ORD4(GetApplLimit) - extraBytes));
END;
You should call this procedure at the beginning of your application, before you
call the MaxApplZone procedure (as described in the next section). If you call IncreaseStackSize after you call MaxApplZone, it has no effect, because the SetApplLimit procedure cannot change the ApplLimit global variable to a value lower than the current top of the heap.

Note
Some compilers add to the beginning of your application some default initialization code that automatically calls MaxApplZone. You might need to specify a compiler directive that turns off such default initialization if you want to increase the size of the stack. Consult your development system's documentation for details.

Expanding the Heap

Near the beginning of your application's execution, before you allocate any memory,
you should call the MaxApplZone procedure to expand the application heap immediately to the application heap limit. If you do not do this, the Memory Manager gradually expands your heap as memory needs require. This gradual expansion can result in significant heap fragmentation if you have previously moved relocatable blocks to the top of the heap (by calling MoveHHi) and locked them (by calling HLock). When the heap grows beyond those locked blocks, they are no longer at the top of the heap. Your heap then remains fragmented for as long as those blocks remain locked.

Another advantage to calling MaxApplZone is that doing so is likely to reduce the number of relocatable blocks that are purged by the Memory Manager. The Memory Manager expands your heap to fulfill a memory request only after it has exhausted other methods of obtaining the required amount of space, including compacting the heap and purging blocks marked as purgeable. By expanding the heap to its limit, you can prevent the Memory Manager from purging blocks that it otherwise would purge. This, together with the fact that your heap is expanded only once, can make memory allocation significantly faster.

Note
As indicated in the previous section, you should call MaxApplZone only after you have expanded the stack, if necessary.

Allocating Master Pointer Blocks

After calling MaxApplZone, you should call the MoreMasters procedure to allocate as many new nonrelocatable blocks of master pointers as your application is likely to need during its execution. Each block of master pointers in your application heap contains 64 master pointers. The Operating System allocates one block of master pointers as your application is loaded into memory, and every relocatable block you allocate needs one master pointer to reference it.

If, when you allocate a relocatable block, there are no unused master pointers in your application heap, the Memory Manager automatically allocates a new block of master pointers. For several reasons, however, you should try to prevent the Memory Manager from calling MoreMasters for you. First, MoreMasters executes more slowly if it has to move relocatable blocks up in the heap to make room for the new nonrelocatable block of master pointers. When your application first starts running, there are no such blocks that might have to be moved. Second, the new nonrelocatable block of master pointers is likely to fragment your application heap. At any time the Memory Manager is forced to call MoreMasters for you, there are already at least 64 relocatable blocks allocated in your heap. Unless all or most of those blocks are locked high in the heap (an unlikely situation), the new nonrelocatable block of master pointers might be allocated above existing relocatable blocks. This increases heap fragmentation.

To prevent this fragmentation, you should call MoreMasters at the beginning of your application enough times to ensure that the Memory Manager never needs to call it for you. For example, if your application never allocates more than 300 relocatable blocks in its heap, then five calls to the MoreMasters should be enough. It's better to call MoreMasters too many times than too few, so if your application usually allocates about 100 relocatable blocks but sometimes might allocate 1000 in a particularly busy session, you should call MoreMasters enough times at the beginning of the program to cover the larger figure.

You can determine empirically how many times to call MoreMasters by using a low-level debugger. First, remove all the calls to MoreMasters from your code and then give your application a rigorous workout, opening and closing windows, dialog boxes, and desk accessories as much as any user would. Then, find out from your debugger how many times the system called MoreMasters. To do so, count the nonrelocatable blocks of size $100 bytes (decimal 256, or 64 4). Because of Memory Manager size corrections, you should also count any nonrelocatable blocks of size $108, $10C, or
$110 bytes. (You should also check to make sure that your application doesn't allocate other nonrelocatable blocks of those sizes. If it does, subtract the number it allocates from the total.) Finally, call MoreMasters at least that many times at the beginning of your application.

Listing 1-4 illustrates a typical sequence of steps to configure your application heap
and stack. The DoSetUpHeap procedure defined there increases the size of the stack by 32 KB, expands the application heap to its new limit, and allocates five additional blocks of master pointers.

Listing 1-4 Setting up your application heap and stack

PROCEDURE DoSetUpHeap;
CONST
   kExtraStackSpace = $8000;              {32 KB}
   kMoreMasterCalls = 5;                  {for 320 master ptrs}
VAR
   count:   Integer;
BEGIN
   IncreaseStackSize(kExtraStackSpace);   {increase stack size}
   MaxApplZone;                           {extend heap to limit}
   FOR count := 1 TO kMoreMasterCalls DO
      MoreMasters;                        {64 more master ptrs}
END;
To reduce heap fragmentation, you should call DoSetUpHeap in a code segment that you never unload (possibly the main segment) rather than in a special initialization code segment. This is because MoreMasters allocates a nonrelocatable block. If you call MoreMasters from a code segment that is later purged, the new master pointer block is located above the purged space, thereby increasing fragmentation.

Determining the Amount of Free Memory

Because space in your heap is limited, you cannot usually honor every user request that would require your application to allocate memory. For example, every time the user opens a new window, you probably need to allocate a new window record and other associated data structures. If you allow the user to open windows endlessly, you risk running out of memory. This might adversely affect your application's ability to perform important operations such as saving existing data in a window.

It is important, therefore, to implement some scheme that prevents your application from using too much of its own heap. One way to do this is to maintain a memory cushion that can be used only to satisfy essential memory requests. Before allocating memory for any nonessential task, you need to ensure that the amount of memory that remains free after the allocation exceeds the size of your memory cushion. You can do this by calling the function IsMemoryAvailable defined in Listing 1-5.

Listing 1-5 Determining whether allocating memory would deplete the memory cushion

FUNCTION IsMemoryAvailable (memRequest: LongInt): Boolean;
VAR
   total:   LongInt;    {total free memory if heap purged}
   contig:  LongInt;    {largest contiguous block if heap purged}
BEGIN
   PurgeSpace(total, contig);
   IsMemoryAvailable := ((memRequest + kMemCushion) < contig);
END;
The IsMemoryAvailable function calls the Memory Manager's PurgeSpace procedure to determine the size of the largest contiguous block that would be available if the application heap were purged; that size is returned in the contig parameter. If the size of the potential memory request together with the size of the memory cushion is less than the value returned in contig, IsMemoryAvailable is set to TRUE, indicating that it is safe to allocate the specified amount of memory; otherwise, IsMemoryAvailable returns FALSE.

Notice that the IsMemoryAvailable function does not itself cause the heap to be purged or compacted; the Memory Manager does so automatically when you actually attempt to allocate the memory.

Usually, the easiest way to determine how big to make your application's memory cushion is to experiment with various values. You should attempt to find the lowest value that allows your application to execute successfully no matter how hard you try to allocate memory to make the application crash. As an extra guarantee against your application's crashing, you might want to add some memory to this value. As indicated earlier in this chapter, 40 KB is a reasonable size for most applications.

CONST
   kMemCushion =  40 * 1024;           {size of memory cushion}
You should call the IsMemoryAvailable function before all nonessential memory requests, no matter how small. For example, suppose your application allocates a new, small relocatable block each time a user types a new line of text. That block might be small, but thousands of such blocks could take up a considerable amount of space. Therefore, you should check to see if there is sufficient memory available before allocating each one. (See Listing 1-6 on page 1-44 for an example of how to call IsMemoryAvailable.)

You should never, however, call the IsMemoryAvailable function before an essential memory request. When deciding how big to make the memory cushion for your application, you must make sure that essential requests can never deplete all of the cushion. Note that when you call the IsMemoryAvailable function for a nonessential request, essential requests might have already dipped into the memory cushion. In that case, IsMemoryAvailable returns FALSE no matter how small the nonessential request is.

Some actions should never be rejectable. For example, you should guarantee that there is always enough memory free to save open documents, and to perform typical maintenance tasks such as updating windows. Other user actions are likely to be always rejectable. For example, because you cannot allow the user to create an endless number of documents, you should make the New Document and Open Document menu commands rejectable.

Although the decisions of which actions to make rejectable are usually obvious, modal and modeless boxes present special problems. If you want to make such dialog boxes available at all costs, you must ensure that you allocate a large enough memory cushion to handle the maximum number of these dialog boxes that the user could open at once. If you consider a certain dialog box (for instance, a spelling checker) nonessential, you must be prepared to inform the user that there is not enough memory to open it if memory space become low.

Allocating Blocks of Memory

As you have seen, a key element of the memory-management scheme presented in this chapter is to disallow any nonessential memory allocation requests that would deplete the memory cushion. In practice, this means that, before calling NewHandle, NewPtr, or another function that allocates memory, you should check that the amount of space remaining after the allocation, if successful, exceeds the size of the memory cushion.

An easy way to do this is never to allocate memory for nonessential tasks by calling NewHandle or NewPtr directly. Instead call a function such as NewHandleCushion, defined in Listing 1-6, or NewPtrCushion, defined in Listing 1-7.

Listing 1-6 Allocating relocatable blocks

FUNCTION NewHandleCushion (logicalSize: Size): Handle;
BEGIN
   IF NOT IsMemoryAvailable(logicalSize) THEN
      NewHandleCushion := NIL
   ELSE
      BEGIN
         SetGrowZone(NIL);          {remove grow-zone function}
         NewHandleCushion := NewHandleClear(logicalSize);
         SetGrowZone(@MyGrowZone);  {install grow-zone function}
      END;
END;
The NewHandleCushion function first calls IsMemoryAvailable to determine whether allocating the requested number of bytes would deplete the memory cushion.
If so, NewHandleCushion returns NIL to indicate that the request has failed. Otherwise,
if there is indeed sufficient space for the new block, NewHandleCushion calls NewHandleClear to allocate the relocatable block. Before calling NewHandleClear, however, NewHandleCushion disables the grow-zone function for the application heap. This prevents the grow-zone function from releasing any emergency memory reserve your application might be maintaining. See "Defining a Grow-Zone Function" on page 1-48 for details on grow-zone functions.

You can define a function NewPtrCushion to handle allocation of nonrelocatable blocks, as shown in Listing 1-7.

Listing 1-7 Allocating nonrelocatable blocks

FUNCTION NewPtrCushion (logicalSize: Size): Handle;
BEGIN
   IF NOT IsMemoryAvailable(logicalSize) THEN
      NewPtrCushion := NIL
   ELSE
      BEGIN
         SetGrowZone(NIL);          {remove grow-zone function}
         NewPtrCushion := NewPtrClear(logicalSize);
         SetGrowZone(@MyGrowZone);  {install grow-zone function}
      END;
END;
Note
The functions NewHandleCushion and NewPtrCushion allocate prezeroed blocks in your application heap. You can easily modify those functions if you do not want the blocks prezeroed.
Listing 1-8 illustrates a typical way to call NewPtrCushion.

Listing 1-8 Allocating a dialog record

FUNCTION GetDialog (dialogID: Integer): DialogPtr;
VAR
   myPtr: Ptr;                {storage for the dialog record}
BEGIN
   myPtr := NewPtrCushion(SizeOf(DialogRecord));
   IF MemError = noErr THEN
      GetDialog := GetNewDialog(dialogID, myPtr, WindowPtr(-1))
   ELSE
      GetDialog := NIL;       {can't get memory}
END;
When you allocate memory directly, you can later release it by calling the DisposeHandle and DisposePtr procedures. When you allocate memory indirectly by calling a Toolbox routine, there is always a corresponding Toolbox routine to release that memory. For example, the DisposeWindow procedure releases memory allocated with the NewWindow function. Be sure to use these special Toolbox routines instead of the generic Memory Manager routines when applicable.

Maintaining a Memory Reserve

A simple way to help ensure that your application always has enough memory available for essential operations is to maintain an emergency memory reserve. This memory reserve is a block of memory that your application uses only for essential operations and only when all other heap space has been allocated. This section illustrates one way to implement a memory reserve in your application.

To create and maintain an emergency memory reserve, you follow three distinct steps:

To refer to the emergency reserve, you can declare a global variable of type Handle.

VAR
   gEmergencyMemory: Handle;  {handle to emergency memory reserve}
Listing 1-9 defines a function that you can call early in your application's execution (before entering your main event loop) to create an emergency memory reserve. This function also installs the application-defined grow-zone procedure. See "Defining a Grow-Zone Function" on page 1-48 for a description of the grow-zone function.

Listing 1-9 Creating an emergency memory reserve

PROCEDURE InitializeEmergencyMemory;
BEGIN
   gEmergencyMemory := NewHandle(kEmergencyMemorySize);
   SetGrowZone(@MyGrowZone);
END;
The InitializeEmergencyMemory procedure defined in Listing 1-9 simply allocates a relocatable block of a predefined size. That block is the emergency memory reserve.
A reasonable size for the memory reserve is whatever size you use for the memory cushion. Once again, 40 KB is a good size for many applications.

CONST
   kEmergencyMemorySize =  40 * 1024;  {size of memory reserve}
When using a memory reserve, you need to change the IsMemoryAvailable function defined earlier in Listing 1-5. You need to make sure, when determining whether a nonessential memory allocation request should be honored, that the memory reserve has not been released. To check that the memory reserve is intact, use the function IsEmergencyMemory defined in Listing 1-10.

Listing 1-10 Checking the emergency memory reserve

FUNCTION IsEmergencyMemory: Boolean;
BEGIN
   IsEmergencyMemory := 
      (gEmergencyMemory <> NIL) & (gEmergencyMemory^ <> NIL);
END;
Then, you can replace the function IsMemoryAvailable defined in Listing 1-5 (page 1-43) by the version defined in Listing 1-11.

Listing 1-11 Determining whether allocating memory would deplete the memory cushion

FUNCTION IsMemoryAvailable (memRequest: LongInt): Boolean;
VAR
   total:   LongInt;    {total free memory if heap purged}
   contig:  LongInt;    {largest contiguous block if heap purged}
BEGIN
   IF NOT IsEmergencyMemory THEN {is emergency memory available?}
      IsMemoryAvailable := FALSE
   ELSE
   BEGIN
      PurgeSpace(total, contig);
      IsMemoryAvailable := ((memRequest + kMemCushion) < contig);
   END;
END;
As you can see, this is exactly like the earlier version except that it indicates that memory is not available if the memory reserve is not intact.

Once you have allocated the memory reserve early in your application's execution, it should be released only to honor essential memory requests when there is no other space available in your heap. You can install a simple grow-zone function that takes care of releasing the reserve at the proper moment. Each time through your main event loop, you can check whether the reserve is still intact; to do this, add these lines of code to your main event loop, before you make your event call:

IF NOT IsEmergencyMemory THEN
   RecoverEmergencyMemory;
The RecoverEmergencyMemory function, defined in Listing 1-12, simply attempts to reallocate the memory reserve.

Listing 1-12 Reallocating the emergency memory reserve

PROCEDURE RecoverEmergencyMemory;
BEGIN
   ReallocateHandle(gEmergencyMemory, kEmergencyMemorySize);
END;
If you are unable to reallocate the memory reserve, you might want to notify the user that because memory is in short supply, steps should be taken to save any important data and to free some memory.

Defining a Grow-Zone Function

The Memory Manager calls your heap's grow-zone function only after other attempts to obtain enough memory to satisfy a memory allocation request have failed. A grow-zone function should be of the following form:

FUNCTION MyGrowZone (cbNeeded: Size): LongInt;
The Memory Manager passes to your function (in the cbNeeded parameter) the number of bytes it needs. Your function can do whatever it likes to free that much space in the heap. For example, your grow-zone function might dispose of certain blocks or make some unpurgeable blocks purgeable. Your function should return the number of bytes, if any, it managed to free.

When the function returns, the Memory Manager once again purges and compacts the heap and tries again to allocate the requested amount of memory. If there is still insufficient memory, the Memory Manager calls your grow-zone function again, but only if the function returned a nonzero value when last called. This mechanism allows your grow-zone function to release memory gradually; if the amount it releases is not enough, the Memory Manager calls it again and gives it the opportunity to take more drastic measures.

Typically a grow-zone function frees space by calling the EmptyHandle procedure, which purges a relocatable block from the heap and sets the block's master pointer to NIL. This is preferable to disposing of the space (by calling the DisposeHandle procedure), because you are likely to want to reallocate the block.

The Memory Manager might designate a particular relocatable block in the heap as protected; your grow-zone function should not move or purge that block. You can determine which block, if any, the Memory Manager has protected by calling the GZSaveHnd function in your grow-zone function.

Listing 1-13 defines a very basic grow-zone function. The MyGrowZone function attempts to create space in the application heap simply by releasing the block of emergency memory. First, however, it checks that (1) the emergency memory hasn't already been released and (2) the emergency memory is not a protected block of memory (as it would be, for example, during an attempt to reallocate the emergency memory block). If either of these conditions isn't true, then MyGrowZone returns 0 to indicate that no memory was released.

Listing 1-13 A grow-zone function that releases emergency storage

FUNCTION MyGrowZone (cbNeeded: Size): LongInt;
VAR
   theA5:   LongInt;             {value of A5 when function is called}
BEGIN
   theA5 := SetCurrentA5;        {remember current value of A5; install ours}
   IF (gEmergencyMemory^ <> NIL) & (gEmergencyMemory <> GZSaveHnd) THEN
      BEGIN
         EmptyHandle(gEmergencyMemory);
         MyGrowZone := kEmergencyMemorySize;
      END
   ELSE
      MyGrowZone := 0;           {no more memory to release}
   theA5 := SetA5(theA5);        {restore previous value of A5}
END;
The function MyGrowZone defined in Listing 1-13 saves the current value of the A5 register when it begins and then restores the previous value before it exits. This is necessary because your grow-zone function might be called at a time when the system is attempting to allocate memory and value in the A5 register is not correct. See the chapter "Memory Management Utilities" in this book for more information about saving and restoring the A5 register.

Note
You need to save and restore the A5 register only if your grow-zone function accesses your A5 world. (In Listing 1-13, the grow-zone function uses the global variable gEmergencyMemory.)


Previous Book Contents Book Index Next

© Apple Computer, Inc.
3 JUL 1996