Important: The information in this document is obsolete and should not be used for new development.
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.
- set up your application heap at application launch time
- determine how much free space is available in your application heap
- allocate and release blocks of memory in your heap
- define and install a grow-zone function
- 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.
- change the size of your application's stack
- expand the heap to the heap limit
- allocate additional master pointer blocks
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.
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
- 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.
ApplLimit
global variable, you can lower the value ofApplLimit
to limit the heap's growth. By loweringApplLimit
, 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 theMaxApplZone
procedure (as described in the next section). If you call IncreaseStackSize after you callMaxApplZone
, it has no effect, because theSetApplLimit
procedure cannot change theApplLimit
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 theMaxApplZone
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 callingMoveHHi
) and locked them (by callingHLock
). 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 callingMaxApplZone
, you should call theMoreMasters
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 callMoreMasters
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 theMoreMasters
should be enough. It's better to callMoreMasters
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 callMoreMasters
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 toMoreMasters
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 calledMoreMasters
. 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, callMoreMasters
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. TheDoSetUpHeap
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 callDoSetUpHeap
in a code segment that you never unload (possibly the main segment) rather than in a special initialization code segment. This is becauseMoreMasters
allocates a nonrelocatable block. If you callMoreMasters
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;TheIsMemoryAvailable
function calls the Memory Manager'sPurgeSpace
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 thecontig
parameter. If the size of the potential memory request together with the size of the memory cushion is less than the value returned incontig
,IsMemoryAvailable
is set toTRUE
, indicating that it is safe to allocate the specified amount of memory; otherwise,IsMemoryAvailable
returnsFALSE
.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 theIsMemoryAvailable
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 callIsMemoryAvailable
.)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 theIsMemoryAvailable
function for a nonessential request, essential requests might have already dipped into the memory cushion. In that case,IsMemoryAvailable
returnsFALSE
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 callingNewHandle
,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
orNewPtr
directly. Instead call a function such asNewHandleCushion
, defined in Listing 1-6, orNewPtrCushion
, 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;TheNewHandleCushion
function first callsIsMemoryAvailable
to determine whether allocating the requested number of bytes would deplete the memory cushion.
If so,NewHandleCushion
returnsNIL
to indicate that the request has failed. Otherwise,
if there is indeed sufficient space for the new block,NewHandleCushion
callsNewHandleClear
to allocate the relocatable block. Before callingNewHandleClear
, 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;Listing 1-8 illustrates a typical way to call
- Note
- The functions
NewHandleCushion
andNewPtrCushion
allocate prezeroed blocks in your application heap. You can easily modify those functions if you do not want the blocks prezeroed.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 theDisposeHandle
andDisposePtr
procedures. When you allocate memory indirectly by calling a Toolbox routine, there is always a corresponding Toolbox routine to release that memory. For example, theDisposeWindow
procedure releases memory allocated with theNewWindow
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
- When your application starts up, you need to allocate a block of reserve memory. Because you allocate the block, it is no longer free in the heap and does not enter into the free-space determination done by
IsMemoryAvailable
.- When your application needs to fulfill an essential memory request and there isn't enough space in your heap to satisfy the request, you can release the reserve. This effectively ensures that you always have the memory you request, at least for essential operations. You can use a grow-zone function to release the reserve when necessary; see "Defining a Grow-Zone Function" on page 1-48 for details.
- Each time through your main event loop, you should check whether the reserve has been released. If it has, you should attempt to recover the reserve. If you cannot recover the reserve, you should warn the user that memory is critically short.
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;TheInitializeEmergencyMemory
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 theIsMemoryAvailable
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 functionIsEmergencyMemory
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 functionIsMemoryAvailable
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;TheRecoverEmergencyMemory
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 toNIL
. This is preferable to disposing of the space (by calling theDisposeHandle
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, thenMyGrowZone
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 functionMyGrowZone
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
.)