Monday, December 20, 2010

VisualWorks 7.7.1 Memory Policy and Shrinking Memory Usage

VisualWorks 7.7.1 has LargeGrainMemoryPolicy as its default memory policy. If you allocate a large amount of memory, let’s say 300 MB, and then drop the referenced objects, the memory allocated from the operating system does not drop.

Even when the image is left unused for a long time, the allocation stays. Running additional allocations, which consumes less memory than the first allocation, does not free any memory allocated from the operating system. However, evaluating garbage collect frees up the used memory. Earlier versions of VisualWorks did not have this behavior; they would free memory when additional allocations were made.

I used the following script on Windows 7 64 bit to test this behavior:

| arrays |
arrays := List new.
1 to: 300 do: [:each |
       arrays addLast: (ByteArray new: 1024 * 1024 withAll: 1)].
arrays := List new.
1 to: 100 do: [:each |
       arrays addLast: (ByteArray new: 1024 * 1024 withAll: 1)].

After the script finished, all initial 300 MB of data are still allocated from the OS, when using VisualWorks 7.7.1. This might not represent a problem; the memory is not “lost”. It is still there, available for new allocations made by the image. But if you have multiple images on one server (Citrix hosting a desktop application, or a server application with multiple images), this memory usage can cause problems.

To fix this problem I make my own memory policy, add instance variable #lastGarbageCollectTimestamp, add getter/setter for the variable and override #idleLoopAction to to perform a garbage collection every 60 second:

                super idleLoopAction.
                (self lastGarbageCollectTimestamp isNil or: [
                               (self lastGarbageCollectTimestamp differenceFromTimestamp: Timestamp now)
                                               > (Duration fromSeconds: 60)]) ifTrue: [
                               ObjectMemory globalCompactingGC.
                               self lastGarbageCollectTimestamp: Timestamp now]

Note that a global, compacting garbage collect can be CPU intensive. However, we prefer this over using too much memory.


Andrés said...

I think it would have been easier to reduce the value of freeMemoryUpperBound. Did you try that?

Andrés said...

Also, note that the idleLoop will run the IGC. Eventually, the IGC will catch up with that garbage and collect it. When it does, it will check the current free memory against the freeMemoryUpperBound, and release unwanted free memory.

Note that there is a balance between performance and how aggressively you want to keep memory usage down. I am not sure how to reconcile a memory upper bound of 3GB and being concerned about 300MB of unused memory. Is 3GB a realistic memory upper bound? If not, shouldn't it be reduced to e.g.: 512MB? In that way, you know no image will start using more memory than you know your app will need.

If memory consumption is a concern because you have potentially many images running, will the machine support e.g.: 4 images using the 3GB they have been told is available to them? If not, does that memory upper bound make sense?

Andrés said...

Also, what might be going on is that, since the memory policy knows the upper memory bound is 3GB, it will assume you also mean a rather large growth regime boundary. In other words, the memory policy is thinking "you told me the app will use up to 3GB, so I won't bother doing a GC until the image grows enough because it is expected the image will grow a lot before stabilizing".

In other words, the memory policy is favoring growth over reclamation. Did you try setting a lower growth regime upper bound?

Runar Jordahl said...

Please note that this post deals with VisualWorks 7.7.1 using the Cincom default memory policy. I run the script after starting a Cincom base image which is limited to using 515 MB of memory.

When the script is run, the image jumps from using 56 MB to using 356 MB. Those additional 300 MB created in the script are garbage when the script is finished, but the image does not reduce its memory consumption, even when being idle for a long time. If you later manually perform a garbage collect, it gets down to 120 MB (which is OK).

This behavior indicates that for some scenarios, it is wise to run garbage collection at regular intervals.

I did present another memory policy in another blog post. We use this 3 GB memory policy, since our application needs more than 512 MB. For our application 3 GB is actually too little. We do large Monte Carlo simulations of oil/gas fields, and customers will, from time to time, run as large models as our application allows. But usually the system uses around 200 MB.

Many customers run on Citrix, therefore we want to reduce the memory used. Out of for example 30 users, typically only one or two users will do simulations requiring more than 1 GB of memory. We want to ensure that all images free memory when they no longer need it.

I notice that for VisualWorks 7.7.1, ObjectMemory currentMemoryPolicy growthRegimeUpperBound is 322122547, meaning that beyond 322 MB usage, garbage collection will be performed before growing memory usage. I tried reducing it (ObjectMemory currentMemoryPolicy growthRegimeUpperBound: 10000000), but that did not seem to have any effect. (It could be that simply setting it when the policy is being used does not work.) Anyway, growthRegimeUpperBound does not take into account (as far as I know) the case where an image has allocated a large object, then drops it and goes into being idle. So even with growthRegimeUpperBound tuned, a regular garbage collect will reduce memory usage that could stay for a very long time.

Andrés said...

Ahh, I think I found a couple problems. The memory policy can run the IGC by calling the primitive directly, but that will skip the call to updateAfterGC, which checks to see if memory should be shrunk. The uses are in idleLoopAction and incrementalGC.

Looking at the old memory policy code, it looks like these problems were originally there as well. Sigh... oh well, easy enough to fix. Thanks for letting me know!

Andrés said...

For now, I just changed the idle loop to shrink memory if required after it calls the IGC. I didn't change the IGC action due to low space because, in that context, the implication is that the image is busy and growing.