8 minutes read

You already know that garbage collector (or GC) is a part of JVM that frees up memory in runtime and generally makes your life happier. But now let's make things a bit more complicated: there are many different garbage collectors. What is more, developers have the power to apply settings and control garbage collectors. Today we'll start from the beginning and explore serial GC.

By JVM we mean HotSpot JVM, the most used JVM and maintained by Oracle.

The first and the simplest

Serial GC was introduced with Java 1.3 back in 1999 and became the first garbage collector JVM ever had. Collectors are constantly improving, and new ones appear from time to time, for example, the seventh GC was introduced with Java 12. But why was it called serial garbage collector, you may ask? Look at its approach: the program runs until GC starts cleaning, then it's frozen while GC is working, and only when the cleaning is finished, your program is back in action.

Serial garbage collector: cleaning approach

When serial GC is executing, it's using only one thread while all other application threads are waiting. As a consequence, the collector isn't used in applications with high requirements for response time because cleaning can take quite a while. However, serial GC still can be useful for programs with really small heaps or on single processor machines.

A situation when the program is stopped is called Stop The World.

Cleaning details: the structure

But what really happens when serial GC starts acting? First, all garbage collectors are based on the same logic: find dead objects and delete them. Secondly, the object is considered dead when it's impossible to reach it from a running program. And thirdly, you remember that all objects created during runtime are stored in the heap.

Heap: divided by serial GC

Serial GC builds on a generational hypothesis that declares that most objects die young. By implementing this hypothesis, we can reduce the number of objects that need to be checked for deletion. We use generations (memory pools where one pool composes of objects close in age) for that.

Look at the picture above. This is how serial GC divides the heap. As you see, there are only two generations: young and tenured (old).

The young generation consists of:

  • Eden (as in the place where the beginning of humanity took place)
  • Survivor 1
  • Survivor 2
  • Virtual space

The tenured generation only consists of:

  • Tenured
  • Virtual space

For starters, let's say that virtual spaces are used at initialization: maximum address space is virtually reserved and allocated to physical memory when needed.

Now you are ready to trace how serial GC interacts with the heap.

Cleaning details: the process

Once upon a time, during the execution of your program, an object was created. Not a huge one, but a most ordinary object.

Usual sized object creation

All objects created next will be placed in Eden too. But when Eden becomes full, serial GC starts acting. It has two stages of cleaning: minor and major. The first stage is minor.

Minor stage cleaning: Eden is filled

The minor stage starts with identifying dead objects and deleting them. After that, the copying mechanism happens: all surviving objects will be copied to Survivor 1:

Minor stage cleaning: copying objects to S1

As you see, Eden is empty again and ready for new objects.

So, now you can successfully create new objects and fill Eden again:

Minor stage cleaning: Eden is filled again

When Eden fills up, the minor stage of serial GC continues. It deletes dead objects and copies the surviving ones from Eden and Survivor 1 to the second Survivor:

Minor stage cleaning: copying objects to S2

Next time Eden becomes full, the copying will happen from Eden and Survivor 2 to Survivor 1.

The idea is that one of the Survivors is always empty: when Eden is full, surviving objects will be copied to an empty Survivor, and the other Survivor will become unoccupied.

Meanwhile, serial GC is constantly watching how old objects are doing. If an object becomes too old, serial GC will put it into Tenured instead of copying it into Survivor.

Let's now look at the last case that may happen: Tenured becomes full. If that happens, serial GC starts major cleaning. The major stage uses the so-called "Mark-sweep-compact" algorithm for the old generation, and the same algorithm "Copying collectors" as the minor stage for the young generation.

Major stage cleaning: Tenured is filled

As a step-by-step, it looks like this:

  1. Find and delete dead objects in both generations.
  2. Compact objects in Tenured.
  3. Copy objects between Survivors (and transfer old objects to Tenured).

Major stage cleaning: compacting and copying objects

The last thing we would like to point out is when you create a huge object, it won't be placed in Eden but stored directly in Tenured.

Becoming a pro

Are you now wondering how to actually use serial GC? Let's lift the veil of secrecy and learn how to adjust serial GC!

To activate serial GC, just use the simple option: -XX:+UseSerialGC.

Different options allow you to modify the heap to your own needs, but here we will consider only two of them:

  • -XX:MinHeapFreeRatio=?
  • -XX:MaxHeapFreeRatio=?

Both of these options take a percentage as a parameter and automatically adjust free space in young and old generations.

Let's set -XX:MinHeapFreeRatio=20 and -XX:MaxHeapFreeRatio=60. The first option says: if free space in a generation is less than 20%, give more space to the generation (so that at least 20% becomes free). The second option says: if free space in a generation is more than 60%, reduce free space in the generation (so that no more than 60% becomes free). The diagrams below demonstrate it:

Adjusting serial GC

It is best to make a detailed performance investigation before adjusting serial GC.

No one forgets their first... garbage collector

Now it's time to see how serial GC acts with a real program. We will take a simple code that saves 100_000 elements in a collection:

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100_000; i++) {
            String str = "abc";
            list.add(str);
        }
    }
}

Let's run our code:

java -XX:+UseSerialGC -Xlog:gc*:/yourPath/gc.log Main

Here, /yourPath/gc.log is the path to the file where you would like your logs to be written.

The -Xlog:gc* option will give information about our garbage collector and save it to the provided file. The * symbol indicates that we want to receive detailed information (without it, there will be just general info).

The gc.log file will look like this:

[0.021s][info][gc] Using Serial
[0.021s][info][gc,heap,coops] Heap address: 0x000000060de00000, size: 7970 MB, Compressed Oops mode: Zero based, Oop shift amount: 3
[0,086s][info][gc,heap,exit ] Heap
[0,086s][info][gc,heap,exit ]   def new generation   total 153600K, used 10926K [0x000000060de00000, 0x00000006184a0000, 0x00000006b3ea0000)
[0,086s][info][gc,heap,exit ]     eden space 136576K,   8% used [0x000000060de00000, 0x000000060e8aba98, 0x0000000616360000)
[0,086s][info][gc,heap,exit ]     from space 17024K,   0% used [0x0000000616360000, 0x0000000616360000, 0x0000000617400000)
[0,086s][info][gc,heap,exit ]     to   space 17024K,   0% used [0x0000000617400000, 0x0000000617400000, 0x00000006184a0000)
[0,086s][info][gc,heap,exit ]   tenured generation   total 341376K, used 0K [0x00000006b3ea0000, 0x00000006c8c00000, 0x0000000800000000)
[0,086s][info][gc,heap,exit ]      the space 341376K,   0% used [0x00000006b3ea0000, 0x00000006b3ea0000, 0x00000006b3ea0200, 0x00000006c8c00000)
[0,086s][info][gc,heap,exit ]   Metaspace       used 3513K, capacity 4486K, committed 4864K, reserved 1056768K
[0,086s][info][gc,heap,exit ]    class space    used 303K, capacity 386K, committed 512K, reserved 1048576K

So, what do we see here? Here's a quick explanation:

  • [0.021s]: time since the start of the JVM;
  • [info]: info debugging level;
  • Using Serial: the type of GC is serial;
  • gc, heap, coops, exit: so-called tags, which specify what the logging message contains;
  • def new generation: info regarding the young generation;
  • tenured generation: info regarding the tenured generation.

The command java -Xlog:help will give you detailed information about the -Xlog option. What you see in gc.log file is the default output, but configuring logs is worthy of a separate topic.

Logs are the base of any investigation. They can show section memory sizes:

  • When the garbage collector started acting;
  • How much memory it freed;
  • How long it was working.

One last question: is it possible to run out of memory in a heap? Unfortunately, yes. As a result, java.lang.OutOfMemoryError happens, and the program crashes. Technically, it occurs if cleaning takes 98% of the time and the collector frees no more than 2% of the memory.

Conclusion

Our adventure with serial GC is nearing the end. Here are some key facts about it for you to remember:

  • It is the oldest and the simplest GC, introduced with Java 1.3.
  • It uses only one thread for cleaning while all other threads are waiting (Stop The World).
  • Heap consists of two generations: young and tenured (old).
  • Heap is limited: if it's full you will have java.lang.OutOfMemoryError, and the program crashes.
29 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo