17 Advanced Progressive Jvm How to Complete Garbage Collection

17 Advanced Progressive JVM- How to Complete Garbage Collection #

Starting from this lesson, we will focus on explaining the basics of the JVM (Java Virtual Machine). Some code optimization techniques in Java are closely related to the JVM, such as escape analysis for optimizing non-capturing lambda expressions.

Before performing these optimizations, you need to have a deep understanding of the running principles of the JVM so that you can optimize in a targeted manner.

In addition, the knowledge in this lesson is all high-frequency interview questions, which also shows the importance of understanding JVM theory.

JVM Memory Area Partitioning #

When learning about the JVM, memory area partitioning is an unavoidable topic, and it is almost always asked in interviews. As shown in the following diagram, the memory area partitioning mainly includes five parts: the heap, the Java virtual machine stack, the program counter, the native method stack, and the metaspace. I will introduce each of them one by one.

Drawing 1.png

JVM Memory Area Partitioning Diagram

1. Heap #

As shown in the JVM memory area partitioning diagram, the area that occupies the largest memory in the JVM is the heap. Most of the objects that we create during coding are allocated on the heap, and it is also the main target area for garbage collection.

2. Java Virtual Machine Stack #

The JVM’s interpretation process is based on the stack, and the execution process of the program is also a process of push and pop, which is how the name “Java Virtual Machine Stack” comes about.

The Java virtual machine stack is thread-specific. When you start a new thread, Java allocates a virtual machine stack for it, and all the operations of this thread will be performed on the stack.

The Java virtual machine stack is actually a double-layer stack structure, from the method entry to the execution of specific bytecode, as shown in the diagram below.

Drawing 3.png

Java Virtual Machine Stack Diagram

In the Java virtual machine stack, each element is called a stack frame. Each stack frame contains four areas: local variable table, operand stack, dynamic linking, and return address.

Among them, the operand stack is the stack area operated by specific bytecode instructions. Considering the following code:

public void test(){
    int a = 1;
    a++;
}

The JVM will generate a stack frame for the test method and push it onto the stack. When the test method is completed, the corresponding stack frame will be popped out. When performing the increment operation on the variable a, the relevant bytecode instructions will be applied to the operand stack in the stack frame.

3. Program Counter #

Since it is a thread, it needs to be scheduled by the operating system. However, there are times when some threads cannot obtain CPU time slices. When such a thread resumes execution, how does it ensure that it can find the position where it left off before the context switch? This is the function of the program counter.

Similar to the Java virtual machine stack, the program counter is also thread-specific. The program counter only needs to record an execution position, so it does not require too much space. In fact, the program counter is the only area in the JVM specification that does not specify the occurrence of an OutOfMemoryError.

4. Native Method Stack #

Similar to the Java virtual machine stack, the native method stack is specific to native methods. In HotSpot, which is commonly used, the Java virtual machine stack and the native method stack are combined into one, which is actually a native method stack. It’s important to note the differences in the specifications.

5. Metaspace #

Metaspace is an area that is easily confused because it has gone through multiple iterations before taking its current form. Let’s explain two interview questions about this area, and then everyone will understand.

  • Is metaspace allocated on the heap?

Answer: Metaspace is not allocated on the heap, but rather in the non-heap space. Its size is not limited by default. The method area, which is commonly referred to as the permanent generation, is located within the metaspace.

  • In which area is the string constant pool located?

Answer: This depends on the JDK version.

Prior to JDK 1.8, there was no concept of metaspace. At that time, the method area was located in a space called the permanent generation.

Before JDK 1.7, the string constant pool was also located in this permanent generation space. However, in JDK 1.7, the string constant pool was moved from the permanent generation to the heap.

Therefore, starting from version 1.7, the string constant pool has always existed in the heap.

6. Direct Memory #

Direct memory refers to memory that is operated on using Java’s direct memory API. This portion of memory can be controlled by the JVM, such as the memory allocated by the ByteBuffer class, which can be controlled using specific parameters.

It is important to note that direct memory and native memory are not the same concept.

  • Direct memory is more specific, with its own API (such as ByteBuffer), and its size can be controlled using the -XX:MaxDirectMemorySize parameter.
  • Native memory is a general term, such as memory operated on using native functions, which cannot be restricted by the JVM. Caution must be exercised when using native memory.

GC Roots #

Objects are mainly allocated on the heap, which can be thought of as a pool. Objects are continuously created, and the background garbage collection process continuously cleans up unused objects. When the rate at which memory is reclaimed cannot keep up with the rate at which objects are created, this object pool can experience overflow, which is commonly known as an OOM (Out of Memory) error.

One effective way to prevent OOM is to promptly remove unused objects from the heap space. So how does the JVM determine which objects should be cleaned up and which objects need to be continued to be used?

Here, we first emphasize a concept that is very helpful for understanding the process of garbage collection and can also demonstrate one’s knowledge during interviews.

Garbage collection does not involve finding unused objects and then removing them. Its process is just the opposite. The JVM finds objects that are being used, marks and traces these objects, and then indiscriminately considers the remaining objects as garbage to be cleaned up.

With an understanding of this concept, we can look at some basic derivative analysis:

  • The speed of garbage collection is related to the number of live objects in the heap, not the total number of objects in the heap.
  • The speed of garbage collection is not related to the size of the heap. Whether the heap is 32GB or 4GB, if the live objects are the same, the garbage collection speed will be similar.
  • Garbage collection does not have to clean up all the garbage every time. The most important thing is not to mistakenly consider objects being used as garbage.

So, how can we find these live objects, that is, which objects are being used? This becomes the core issue.

Think about when you write code, if you want to ensure that a HashMap can be continuously used, you can declare it as a static variable so that it will not be garbage collected. We call these entry points of the references that are being used GC Roots.

This method of using tracing to find live objects also has a nice name, called reachability analysis.

In summary, GC Roots include:

  • Reference types of the current method being called in the Java thread, such as parameters, local variables, temporary values, etc. These are all related to our stack frame.
  • All currently loaded Java classes.
    • Static variables of Java classes;
    • Reference type constants in the runtime constant pool (such as String or Class types);
    • Some references to JVM internal data structures, such as the sun.jvm.hotspot.memory.Universe class;
    • Monitor objects used for synchronization, such as objects on which the wait() method is called;
    • JNI handles, including global handles and local handles.

For this knowledge point, don’t just memorize it, you can compare it with the diagram of JVM memory areas. There are about three entry points: threads, static variables, and JNI references.

Strong, Soft, Weak, and Phantom References #

So, can objects that can be traced through GC Roots not be garbage collected? It depends.

There are four different levels of reference strength for Java objects: strong, soft, weak, and phantom references, from highest to lowest intensity.

  • Strong references are the default object relationships and are created by default when creating objects. This type of reference is the most common and strongest form of existence, and it will only be eliminated when it is disconnected from GC Roots.
  • Soft references are used to maintain objects that are optional. Soft reference objects will not be garbage collected when there is sufficient memory. Only when memory is low, the system will collect soft reference objects. If there is still not enough memory after collecting soft reference objects, an OutOfMemoryError will be thrown.
  • Weak references are of a lower level. When the JVM performs garbage collection, objects associated with weak references will be collected regardless of whether there is enough memory. Soft and weak references are frequently used in heap caching systems and can be prioritized for collection when memory is tight. (We introduced this feature of Guava Cache in “Chapter 07 | Case Study: Ubiquitous Caching, The Magic Weapon of High-Concurrency Systems”.)
  • Phantom references are a kind of reference that is almost fictional and not used very often in practical scenarios. Here is an obscure point: starting from Java 9.0, the Cleaner class was introduced to replace the finalizer method of the Object class, which is one application scenario for phantom references.

Generational Garbage Collection #

As mentioned earlier, the speed of garbage collection depends on the number of surviving objects. If there are too many objects, the JVM will be slow when marking and tracing.

In general, when the JVM performs these operations, it will stop all work of business threads and enter a SafePoint state, which is also what we usually call “Stop the World”. Therefore, the main goal of modern garbage collectors is to reduce the time of stop-the-world pauses.

One effective way to achieve this goal is by using generational garbage collection, which reduces the size of the garbage collection area. This is because most objects can be classified into two categories:

  • Most objects have a short lifespan.
  • Other objects are likely to live for a long time.

This assumption is called the weak generational hypothesis.

As shown in the figure below, the generational garbage collector logically divides the heap space into two parts: the young generation and the old generation.

Drawing 4.png

Heap space division: young generation and old generation

1. Young Generation #

The young generation is further divided into an Eden space and two survivor spaces. Objects are initially allocated in the Eden space in the young generation. When the Eden space is full, a garbage collection cycle is triggered in the young generation.

At this time, surviving objects will be moved to one of the survivor spaces (referred to as “from”). When the young generation undergoes garbage collection again, surviving objects, including those in the “from” space, will be moved to the “to” space. So, either the “from” or “to” space will always be empty.

The default ratio of Eden, “from”, and “to” is 8:1:1, which means there is only 10% space wastage. This ratio can be configured using the -XX:SurvivorRatio parameter (default is 8).

2. Old Generation #

The optimization of garbage collection aims to collect objects in the young generation as soon as possible to reduce the number of objects in the old generation. So, how do objects enter the old generation? There are mainly four ways.

  • Normal promotion The previous section mentioned the garbage collection of the young generation. If an object survives the garbage collection of the young generation, its age will increase by one. When the object’s age reaches a certain threshold, it will be moved to the old generation.

  • Allocation Prefetching

If the young generation’s space is not enough and new objects need to be allocated space, it relies on other memory (in this case, the old generation) for allocation prefetching, and objects are directly created in the old generation.

  • Large Objects Allocated Directly in the Old Generation

Objects that exceed a certain threshold will be allocated directly in the old generation. This threshold can be configured with the -XX:PretenureSizeThreshold parameter.

  • Dynamic Age Determination

Some garbage collection algorithms do not require the age to reach 15 before promotion to the old generation. Instead, they use dynamic calculation methods. For example, G1 dynamically adjusts the promotion threshold of objects through the TargetSurvivorRatio parameter.

The old generation’s space is generally larger, and the garbage collection takes longer. When the old generation’s space is full, a garbage collection of the old generation will occur.

Currently, the widely used garbage collector is the G1 garbage collector. G1 has the concept of both young and old generations. However, G1 divides the entire heap into many parts, treating each part as a small target, making it easier to achieve target completion.

Drawing 6.png

As shown in the above figure, G1 also has the concepts of Eden and Survivor regions, but they are not contiguous in memory. Instead, they are composed of small portions. When performing garbage collection, G1 dynamically selects a portion of the small regions for garbage collection based on the maximum pause time set by MaxGCPauseMillis.

The configuration of G1 is very simple. We only need to configure three parameters to obtain excellent performance in most cases:

① MaxGCPauseMillis sets the target for the maximum pause time. The G1 garbage collector will automatically adjust and select specific small regions.

② G1HeapRegionSize sets the size of the small regions.

③ InitiatingHeapOccupancyPercent starts the concurrent marking phase when a certain proportion (default is 45%) of the entire heap memory is used.

Summary #

In this lesson, we mainly introduced the memory area division of the JVM. When interviewing, students often confuse this concept with Java’s memory model (JMM). You need to pay special attention to this.

JMM refers to the contents related to the collaboration between multiple threads and the main memory and working memory. Make sure to confirm the questions with the interviewer.

In this lesson, we mainly introduced the concepts of Heap, Java Virtual Machine Stack, Program Counter, Native Method Stack, Metaspace, and Direct Memory.

Next, we looked at the concept of GC Roots, which is implemented using the tracing-based reachability analysis method. Regarding object reference relationships, there are also strong, soft, weak, and phantom differences, and heap caches mostly use soft or weak references.

Finally, we looked at the concept of generational garbage collection and learned about some garbage collection strategies in the young and old generations.

JVM’s garbage collectors are updated very quickly, and there are many JVM versions, such as Zing VM and Dalvik. Currently, the widely used one is Hotspot, which provides a large number of configuration parameters to support performance tuning.

As I just mentioned, the main goal of garbage collectors is to ensure garbage collection effects while improving throughput and reducing STW time.

From the CMS garbage collector to the G1 garbage collector, and now to the ZGC, which supports a size of 16TB, the evolution of garbage collectors is becoming more and more intelligent, and the number of configuration parameters is decreasing. They can achieve the effect of “out-of-the-box” usage. However, regardless of which garbage collector is used, our coding style still affects the garbage collection effect. It is important to reduce object creation and timely sever the connection with objects that are no longer in use in our daily coding.

Finally, here’s a thought-provoking question: Besides primitive data types, are all objects always allocated on the heap? Feel free to share and discuss with everyone in the comments, and I will provide feedback for each answer.