Prabal Tripathi

Garbage Collection

July 5, 2026

We all have heard and have been familiar with the concept of what a garbage collector does, it frees up memory by clearing up unreferenced objects, In some languages this is automatic like in Golang and Java, and in some there is an explicit need of calling the GC like in c++

Now there are some things we should definitely think about while discussing GC. Let's start from the basics :

Why do programming languages need GC ?

So whenever we write a programme, we define variables, functions and what not, Now these obv. would be getting stored somewhere, yes fine, so the memory available to these mutating threads or programmes are of two kinds, one is the stack memory and second is the Heap, Now variables and functions (along with the locally declared variables) get stored in the stack as a stack frame ( just a unit of a stack ), This is temporary memory and the data gets cleared, as soon as the function returns and value is popped out.

And then what about the objects that get created, like arrays and linked lists, so first these are dynamically allocated, by which mean, at run time, so there size can increase during the coarse of programme running. and so they can be big as well. and since stack memory is very limited, so we store such objects in heap, the object storage.

Or else if you would store such big object in a stack , you will very soon get a Stack Overflow error. Now heap memory contains the object and the pointer variable hold the address of these objects basically the starting memory address. One of the other reason of using heap which i would like to mention is, globally accessible objects, suppose you want an object to not be passed across functions, rather all the functions accessing the single object globally, like in case the object is gigantic, so you will try and keep it in heap, a better idea !!! and objects present in the heap are always passed by reference.

so some languages provide you with functionalities Of explicit deallocation of objects, like free function and delete function in C++. The problem with it is that sometimes the engineers and developers can forget when to deallocate, and that can lead to problems like memory leak. Memory leak is one of the important problems that you should definitely know about, and it is basically that if you don't deallocate the objects correctly, sometimes the useful pool of memory can leak out completely. That is why it is called a memory leak. It should be called a useful memory leak, because there are certain objects and certain object spaces which you can use, but you are not deallocating the object. Those memory spaces are still in use.

If you keep allocating new objects and don't deallocate objects from the heap, at one point in time the heap memory will be 100% full. At that point, the process using the heap will actually crash. It will be a matter of process crash, which will happen due to the useful memory leak, and not sticking to the term of memory leak, because it sometimes creates confusion.

And one of those second situations is a kind of dangling pointer problem. What happens in a dangling pointer problem is: suppose there is a pointer B which was referencing an address X, Y, Z. Now there is a pointer C also which was referencing that same object X, Y, Z. Now somebody D allocated C, but these are still pointing to that address. This is one of the situations where somebody freed the memory space because of C, but B is still pointing to that. That is one of the situations that nobody wants to get into.

Suppose there is a pointer D which got allocated a memory space at the same space where B is pointing. Now B will be pointing to some value, but it's a value that B is not programmatically programmed to handle. That's why it can result in some unpredictable situations and some really garbage value. It can still work out, and the process might not crash, but it will lead to something unpredictable. In most of the cases, if there is a dangling pointer kind of a problem, the programmers or developers wish that the process should crash rather than having some implicit, unpredictable results.

This is why most of the languages' runtimes provide automatic garbage collection that actually diminishes the chances of these human errors and can actually save your memory and maintain it in the heap memory efficiently.

java -XX:+UseG1GC -jar myapp.jar           # Use G1GC
java -XX:+UseSerialGC -jar myapp.jar       # Use Serial (single-threaded, tiny heap)
java -XX:+UseParallelGC -jar myapp.jar     # Use Parallel (multi-threaded, throughput-focused)
java -XX:+UseConcMarkSweepGC -jar myapp.jar  # Use CMS (low-latency, now deprecated)
java -XX:+UseZGC -jar myapp.jar            # Use ZGC (ultra-low-pause, newer)
java -XX:+UseShenandoahGC -jar myapp.jar   # Use Shenandoah (low-pause, experimental in some versions)

So now, when you are thinking about choosing a garbage collector, there is not even a single parameter where you can evaluate it on, and there are no best garbage collectors. There is not even a concept of a best garbage collector. It always depends on the parameter that you want the garbage collector to be best at. Any garbage collector is best at some parameter, and if that parameter is most important to you, then you should select that garbage collector, because other garbage collectors will suck at that parameter. There is no garbage collector which is equally best in all the parameters that are there. You should evaluate the parameters which are important for you, and that's how you should select the GC. It's very important that sometimes you start using a GC, and over the period of time you change the garbage collector according to your needs, because your needs change. These parameters are very simple:

  1. Safety: This is something that actually removes the problem of dangling pointers, wherein two pointers referencing the same object and you somehow deallocate that object. That should not happen, and that dangling pointer kind of a problem should not come.
  2. Throughput: As we all know, GC runs along with the programme, or sometimes it may pause the programme and then run. It has a direct relation with the throughput of the mutating threads or the main programme that we are dealing with. This throughput should be as high as possible, and that's why GC should take as little course as possible. For sure, this will affect the performance of the GC itself, but sometimes GC affects its own performance to actually contribute to the system's overall throughput.
  3. Defragmentation: Sometimes GC also does defragmentation in between programmes, just to free up some space. What is defragmentation? Just helping you with a simple example. Since the memory is contiguous, we remove objects in an unconjuice manner. For example, I removed an object which was referencing the memory frame 3, then the memory frame 7, and then the memory frame 8. Suppose all the frames were of 1 GB. Now I have to locate a new object of 2 GB. I have to, let's say, till the 30th frame of memory was full. Even though I did locate 2 GB of memory from the third frame and seventh frame, I can't put a 2 GB object there because I don't have 2 GB of contiguous memory between 1 and 30. I'll have to use the space of 30 and 32, which is not right, because suppose 30 was the only set of memory available for me. Then I need to use some memory in that space, though 2 GB is available, but I cannot use it. That's why reshuffling of objects happens, and that's what I call defragmentation. Sometimes GC runs this defragmentation step to sort out the memory and clear the space and free up the space for new allocation of objects.This defragmentation also leads to pause time. Sometimes this is the kind of algorithm that leads to stop-the-world GCs, which I'll talk about later. 4.Completeness. It basically says that my heap should be completely cleared. This cannot happen in one cycle; it actually requires many cycles. Completeness means that all the unreferenced objects should not exist at all. This is what completeness means.
  4. The data structures that GCs use. They use different kinds of data structures for better access to objects, for finding the unreferenced objects and somehow deallocating the space of those objects. One of the most famous data structures that gets used is a graph because object dependency can be represented easily in graphs. We'll later study Mark and sweep kind of algorithms, which actually do a DFS traversal or a DFS graph kind of a traversal. Since object dependency can be easily related to a graph, that's why most of the time GCs use a graph kind of an algorithm. 6.Scalability : so scalability becomes one of the major questions because when you are working at scale, you have a huge amount of heap memory. You obviously cannot use some kind of a pause-time garbage collector or a stop-the-world garbage collector, because then that garbage collector would need a lot of time for defragmentation or object shuffling in that heap, and that won't be a good thing to do. Most of the garbage collectors in today's time have become pause-free, so yes, you should use some pause-free garbage collector if you are working at scale.

Let's discuss the mark-and-sweep algorithm. Mark-and-sweep works on the famous concept of graph traversal called depth-first search. DFS travels the entire graph or tree of nodes in the object relationship model and checks all the objects that can be referenced from the root node, which can be a global variable, a global function, or some kind of global thread. All the objects which are reachable are considered live objects, because some pointers are referencing them, and if they're live, then those are all marked. At the end of the marking phase, it's checked what all is unmarked, and then those objects are swept out. Mark-and-sweep works in majorly these two, or actually three, phases:

  1. Find the global root-level nodes.
  2. Traverse all the nodes through DFS, go inside the depth of it, mark all the nodes which are connected, and consider them live.
  3. At the end of the marking phase, find out what are the objects that are not live and that are not marked, and sweep them out. This is the mark-and-sweep algorithm, and this is done by the collector thread itself. This can be done in a concurrent way, or in a stop-the-world manner. You can read more about that, but yes, this is basic mark-and-sweep.

So the question comes: when is the collector thread actually called? You call the collector thread when the mutator thread is unable to do an object allocation. Then you call the collector thread to free out some memory. Again, if you are unable to allocate an object after even calling the collector thread, then that is what we call an out-of-memory error, that is OOM. It is one of the most famous errors, and I've been facing that in my company server lately. We solved it by switching from one GC to the other, actually.

Then there is something called tricolour abstraction also for current garbage collection. It was introduced by Dijkstra, and he said that all the objects in the memory can be divided into three states:

  • Live state
  • A state where the object has been seen but is not processed yet
  • A state where the object has not even been seen yet There are these three colours: black, grey, and white, which we use to depict the object. He says that, if you can visualise it, the initial object tree would have been white, and then a layer of grey comes over and starts marking the object grey. Beyond that, all the objects are black, and the lower objects are still white. White gets converted to grey, grey gets converted to black. Mutating threads can currently move the objects from the white panel to the grey panel and from the grey panel to the black panel. The best idea about this is that the black nodes can never be connected to the white nodes. This actually promises you correctness, that whatever you are removing is for sure unreferenced, and you can keep on removing the white objects continuously or the objects which are not even being seen yet. Yes, this is one of the things that you should definitely read. I think that would make much more sense. It is proposed by that same person who introduced the minimum shortest path in a weighted graph, who is Dijkstra. You can definitely read about such stuff. This will give you much more clarity about things.

Okay Good night or Good moring, Enjoy the sunday , Byeeee

  • Prabal