Garbage Collector not freeing "trash memory" as it should in an Android application

Garbage collection is complicated, and different platforms implement it differently. Indeed, different versions of the same platform implement garbage collection differently. (And more ... )

A typical modern collector is based on the observation that most objects die young; i.e. they become unreachable soon after they are created. The heap is then divided into two or more "spaces"; e.g. a "young" space and an "old" space.

  • The "young" space is where new objects are created, and it is collected frequently. The "young" space tends to be smaller, and a "young" collection happens quickly.
  • The "old" space is where long-lived objects end up, and it is collected infrequently. On "old" space collection tends to be more expensive. (For various reasons.)
  • Object that survive a number of GC cycles in the "new" space get "tenured"; i.e they are moved to the "old" space.
  • Occasionally we may find that we need to collect the new and old spaces at the same time. This is called a full collection. A full GC is the most expensive, and typically "stops the world" for a relatively long time.

(There are all sorts of other clever and complex things ... which I won't go into.)


Your question is why doesn't the space usage drop significantly until you call System.gc().

The answer is basically that this is the efficient way to do things.

The real goal of collection is not to free as much memory all of the time. Rather, the goal is to ensure that there is enough free memory when it is needed, and to do this either with minimum CPU overheads or a minimum of GC pauses.

So in normal operation, the GC will behave as above: do frequent "new" space collections and less frequent "old" space collections. And the collections will run "as required".

But when you call System.gc() the JVM will typically try to get back as much memory as possible. That means it does a "full gc".

Now I think you said it takes a couple of System.gc() calls to make a real difference, that could be related to use of finalize methods or Reference objects or similar. It turns out that finalizable objects and Reference are processed after the main GC has finished by a background thread. The objects are only actually in a state where they can be collected and deleted after that. So another GC is needed to finally get rid of them.

Finally, there is the issue of the overall heap size. Most VMs request memory from the host operating system when the heap is too small, but are reluctant to give it back. The Oracle collectors note the free space ratio at the end of successive "full" collections. They only reduce the overall size of the heap if the free space ratio is "too high" after a number of GC cycles. There are a number of reasons that the Oracle GCs take this approach:

  1. Typical modern GCs work most efficiently when the ratio of garbage to non-garbage objects is high. So keeping the heap large aids efficiency.

  2. There is a good chance that the application's memory requirement will grow again. But the GC needs to run to detect that.

  3. A JVM repeatedly giving memory back to the OS and and re-requesting it is potentially disruptive for the OS virtual memory algorithms.

  4. It is problematic if the OS is short of memory resources; e.g. JVM: "I don't need this memory. Have it back", OS: "Thanks", JVM: "Oh ... I need it again!", OS: "Nope", JVM: "OOME".

Assuming that the Android collector works the same way, that is another explanation for why you had to run System.gc() multiple times to get the heap size to shrink.


And before you start adding System.gc() calls to your code, read Why is it bad practice to call System.gc()?.