Why Does Gc() Not Free Memory

Why does gc() not free memory?

How do you check memory usage? Normally virtual machine allocates some chunk of memory that it uses to store its data. Some of the allocated may be unused and marked as free. What GC does is discovering data that is not referenced from anywhere else and marking corresponding chunks of memory as unused, this does not mean that this memory is released to the OS. Still from the VM perspective there's now more free memory that can be used for further computation.

As others asked did you experience out of memory errors? If not then there's nothing to worry about.

EDIT:
This and this should be enough to understand how memory allocation and garbage collection works in R.

From the first document:

Occasionally an attempt is made to release unused pages back to the
operating system. When pages are released, a number of free nodes
equal to R_MaxKeepFrac times the number of allocated nodes for each
class is retained. Pages not needed to meet this requirement are
released. An attempt to release pages is made every R_PageReleaseFreq level 1
or level 2 collections.

EDIT2:

To see used memory try running gc() with verbose set to TRUE:

gc(verbose=T)

Here's a result with an array of 10'000'000 integers in memory:

Garbage collection 9 = 1+0+8 (level 2) ... 
10.7 Mbytes of cons cells used (49%)
40.6 Mbytes of vectors used (72%)
used (Mb) gc trigger (Mb) max used (Mb)
Ncells 198838 10.7 407500 21.8 350000 18.7
Vcells 5311050 40.6 7421749 56.7 5311504 40.6

And here's after discarding reference to it:

Garbage collection 10 = 1+0+9 (level 2) ... 
10.7 Mbytes of cons cells used (49%)
2.4 Mbytes of vectors used (5%)
used (Mb) gc trigger (Mb) max used (Mb)
Ncells 198821 10.7 407500 21.8 350000 18.7
Vcells 310987 2.4 5937399 45.3 5311504 40.6

As you can see memory used by Vcells fell from 40.6Mb to 2.4Mb.

dotnet C# - Why doesn't the GC free memory?

The GC is in principle free to run whenever it wants, or never at all for that matter. In practice I would not expect it to run unless you are actually trying to allocating something. So after StressMem returns you might not see any GC unless you do some more work that require memory. If you ran StressMem in a loop I would expect frequent GCs.

Note that that does not necessarily mean that the memory usage for the process will decrease. The garbage collector may return memory to the OS if physical memory runs low, but is you have free memory, why not use it?

If you are investigating how your application uses memory I would recommend a memory and/or performance profiler. They should reveal how much time you are using for GC, what you are using memory for, how much you are allocating etc.

C# Memory leak query - Why GC is not releasing memory of the local scope?

GC doesn't start automatically at the end of scope or function call. According to MS documentation:

Garbage collection occurs when one of the following conditions is true:

•The system has low physical memory. This is detected by either the low memory notification from the OS or low memory indicated by the host.

•The memory that is used by allocated objects on the managed heap surpasses an acceptable threshold. This threshold is continuously adjusted as the process runs.

•The GC.Collect method is called. In almost all cases, you do not have to call this method, because the garbage collector runs continuously. This method is primarily used for unique situations and testing.

Fundamentals of Garbage Collection

Garbage Collection does not reduce current memory usage - in release mode. Why?

The garbage collector is not guaranteed to run when you call Collect(). It simply flags the object for collection. The next time the GC runs it will "collect" the flagged object.

There is no way in .NET to force the GC to collect at a specific point in time - if you need this functionality you'll need to go to native code.

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()?.

Java: calling to GC takes all free memory

Mind the documentation of freeMemory():

Returns the amount of free memory in the Java Virtual Machine.

and compare to totalMemory():

Returns the total amount of memory in the Java virtual machine. The value returned by this method may vary over time, depending on the host environment.

So, change your program to

public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
log("before gc", runtime);
runtime.gc();
log("after gc", runtime);
}

static void log(String point, Runtime runtime) {
System.out.println("Free memory " + point + ": "
+ runtime.freeMemory() / (1024 * 1024) + " MB of "
+ runtime.totalMemory() / (1024 * 1024) + " MB");
}

On my machine, it prints

Free memory before gc: 253 MB of 256 MB
Free memory after gc: 13 MB of 14 MB

indicating that the garbage collection did not only free memory, but gave it back to the operating system.

The default configuration is designed to suite larger applications and performing a (full) garbage collection with such a low memory usage is rather unusual. So it’s not surprising that the heap had a larger initial size and got reduced by this forced garbage collection.

When I run the same program with the -Xms14M option, I get

Free memory before gc: 12 MB of 14 MB
Free memory after gc: 13 MB of 14 MB

GC1 does not release OS-Memory

Resource efficiency is a major concern when it comes to optimizing the performance and scalability of an application. In the Java world, one aspect that usually dominates resource concerns is memory usage - more specifically, the size of the Java heap.

In contrast to .NET and the CLR, the JVM is known for the need to size the heap at startup time without any chance of space reclamation, which would make resources available to other processes. Does that still hold true today? Let's find out.

The garbage collector is in charge of managing the heap and associated OS allocations. Since JDK 9, the G1 garbage collector has been the default GC. Generally speaking, the GC tries to avoid the deallocation of heap space (to give memory back to the OS) because that can lead to performance degradation if that very space is needed at a later point-in-time.

Heap shrinking depends on the collection cycle, which is comprised of the young-only and space reclamation phases.

The young-only phase happens if the associated eden space, where new objects are allocated initially, is exhausted. Reachable objects are evacuated (moved) to the survivor space. If the survivor space is exhausted, reachable objects are evacuated to another survivor space or the old generation (if objects are reachable or "survive" a given number of collections).

The space reclamation happens depending on the size of the old generation, controlled by the Initiating Heap Occupancy Percent (IHOP). The default (-XX:InitiatingHeapOccupancyPercent) is 45 percent, but the IHOP is adjusted by the GC to optimize the collection interval (known as "Adaptive IHOP"). This phase also handles the young generation, therefore it's called a "mixed collection".

According to the G1 source in JDK 9, shrinking is only done after full collections (explicit collection or not) and only in case specific thresholds are met. G1CollectedHeap::do_full_collection calls G1CollectedHeap::resize_if_necessary_after_full_collection, which handles the shrinking process and generates the following GC log output:

log_debug(gc, ergo, heap)(
"Shrink the heap. requested shrinking amount: "
SIZE_FORMAT "B aligned shrinking amount: "
SIZE_FORMAT "B attempted shrinking amount: "
SIZE_FORMAT "B",
shrink_bytes, aligned_shrink_bytes, shrunk_bytes);

The G1 GC aims to

[...] provide the best balance between latency and throughput [...]

G1 attempts to defer garbage collections for as long as possible and tries to avoid full garbage collections in general. For details, see Garbage Collection Cycle.

[...] if the application runs out of memory while gathering liveness information, G1 performs an in-place stop-the-world full heap compaction (Full GC) like other collectors [...]

The collector only performs full collections in case of space exhaustion unless triggered manually (e.g. using java.lang.System.gc()). That's when heap shrinking can happen.

According to the G1 source in JDK 15, shinking is done for full collections as well as concurrent mark-and-sweep collections. G1FullCollector::complete_collection calls G1CollectedHeap::prepare_heap_for_mutators which in turn calls G1CollectedHeap::resize_heap_if_necessary. The actual shrinking process and log message is identical to JDK 9. In contrast to JDK 9, JDK 12 and higher also performs shrinking during the so-called re-mark phase (G1ConcurrentMark::remark calls G1CollectedHeap::resize_heap_if_necessary), in the context of the normal collection cycle.

The change was introduced with 08041b0d7c08.

A straightforward means of monitoring is -verbose:gc. Use -Xlog:gc=debug to see the log output mentioned above in case of shrinking.

So, despite popular belief, the JVM does return memory to the OS. Until JDK 11, heap shrinking was only triggered after full collections, given the change is significant enough to warrant that action without affecting performance. With JDK 12+ (checked until 15), heap shrinking is also triggered during the normal collection cycle, making it more likely for applications that typically do not involve full collections to see heap shrinkage.



Related Topics



Leave a reply



Submit