Introduction | modern high-level programming language points and automatic and manual two kinds of memory management way. Typical examples of manual memory management are C and C++, which require active memory allocation or release during code writing. Languages such as PHP, Java, and Go use automatic memory management systems that allocate and reclaim memory on their behalf through memory allocators and garbage collectors, often referred to as GC. In the “automatic memory management system practical manual – Java garbage collection” and “automatic memory management system practical manual – Golang garbage collection” to share the Java and Golang garbage collection algorithm, today Tencent background development engineer Wang Hui to summarize and compare the two algorithms.

I. Garbage collection area

Each part of Java memory runtime area, including program counter, virtual machine stack, local method stack three areas with the thread, with the thread; How much memory is allocated to each stack frame is basically known when the class structure is determined. The Java heap and method area are different. Multiple implementation classes in an interface may need different memory, and multiple branches in a method may need different memory. We don’t know which objects will be created until the program is running. The Java heap and method areas are the primary areas managed by the Java garbage collector.

Go memory is divided into two parts: Heap and Stack. During execution, the program can actively request memory space from the Heap, which is allocated by the memory allocator and reclaimed by the garbage collector. The memory in the stack area is automatically allocated and released by the compiler. The stack area stores the parameters and local variables of the function, which are created with the creation of the function and destroyed upon the return of the function. If you only apply and allocate memory, memory will eventually run out. Go uses garbage collection to collect unused spans and release them to the MHEAP. The Mheap merges the span and adds the merged span to the SCAV tree. When memory is reallocated, the MHeap reallocates memory. Therefore, the Go heap is the primary area managed by the Go garbage collector.

Second, trigger garbage collection timing

Java GC is called when the application is idle, that is, when there are no application threads running. Because THE GC occurs in the lowest priority thread, the GC thread is not called when the application is busy, except for the following conditions.

GC is called when the Java heap is out of memory. However, in this case, because Java is a generational collection algorithm and there are many types of garbage collectors, the GC timing of the various garbage collectors may not be exactly the same, and we are talking about the general case here.

  1. Minor GC occurs when Eden space is insufficient.

  2. When the object age increases to a certain extent, Young GC;

  3. When the new generation object is transferred to the Old age or created as a large object or array, the Old age space is insufficient and the Old GC is triggered.

  4. The system.gc () call triggers the Full GC;

  5. Block usage exceeds the threshold.

Go triggers based on the following conditions:

  • Runtime. mallocGC triggers GC based on heap size when requesting memory.

  • Runtime.gc The user program triggers GC manually;

  • Runtime. forcegchelper Background running timed checks trigger GC.

Third, collection algorithm

The garbage collection of Java virtual machines adopts generational collection algorithm, which divides the memory into several blocks according to different object life cycles. For example, in the new generation, a large number of objects die in each collection, so a “mark-copy” algorithm can be used to complete each garbage collection at a fraction of the cost of copying objects. Older objects have a higher chance of survival, and there is no extra space for allocation guarantees, so we must choose the “mark-clean” or “mark-clean” algorithm for garbage collection.

The current Go is garbage collection based on the tag removal algorithm.

Iv. Garbage debris disposal

Because the Java memory management division, so prone to garbage objects, the JVM’s continuous improvement and update the GC algorithm in recent years, the JVM in dealing with memory fragments on more by adopting the idea of space compression and generational collection, such as “tag – copy” is used in new generation algorithm, the G1 collector supports the object move to cut long running fragments of the memory problem, The region design makes it easier to return free memory to the OS.

Because of the memory management implementation of Go, it is difficult to implement generational implementation, and moving objects can make the Runtime larger and more complex, so Go has a different approach to memory fragmentation than Java.

1.Go language SPAN memory pool design, reduce a lot of memory fragmentation problems.

The Go memory is released as follows: If a large number of free spans exist in the McAche, the memory is returned to McEntral. When there are more free spans in McEntral, they are returned to mheap. The Mheap is then returned to the operating system. This design has the following advantages:

  • Memory allocation is mostly done in user mode and does not require frequent access to kernel mode.

  • Each P has an independent SPAN cache. Multiple cpus do not concurrently read and write to the same memory, thus reducing the dirty state of L1 cache lines and increasing the CPU cache hit ratio.

  • As for the problem of memory fragmentation, Go is managed by itself in the user mode, but there is no fragmentation at the OS level, which reduces the pressure of fragmentation management at the operating system level.

  • McAche allows memory allocation without locks.

2. Tcmalloc allocation mechanism, Tiny object and large object allocation optimization, also leads to almost no memory fragmentation to some extent.

Sizeclass =1 spans are used for objects <=8B, so int32, byte, bool, and small strings will all use sizeclass=1 spans. However, most of the 8B space allocated to them will not be used. And these types are used very often, resulting in a lot of internal fragmentation.

So Go tries not to use spans with sizeclass=1, instead treating objects <16B as tiny objects uniformly. When allocating, get a 16B object from span sizeclass=2 for allocation. If the size of the object is smaller than 16B, the space is temporarily saved (the McAche. Tiny field) and reused for the next allocation until the object is used up.

For example, in this way, the utilization rate of space is (1+2+8)/16100%= 68.75%, while in accordance with the original management mode, the utilization rate is (1+2+8)/(83)=45.83%. Notes in the source code describe that special handling of Tiny objects will save about 20% of memory on average. If the data to be stored has Pointers, even <= 8B will not be treated as tiny objects, instead using the normal span of sizeclass=1.

In Go, the largest sizeclass can only hold up to 32K objects. If more than 32K of memory is requested at once, the system bypasses McAche and McEntral and fetches it directly from the Mheap, where a Freelarge field manages a large span.

3.Go objects (struct types) can be allocated on the stack.

Go does static Escape Analysis at compile time, and if an object is not found to have escaped the current scope, it is allocated on the stack rather than on the heap, reducing GC memory fragmentation collection.

For example:

func F() {
  temp := make([]int, 0.20) // This is a temporary variable applied to the internal function. It is not returned as a value. It is applied to the stack by the compiler.
  temp = append(temp, 1)
}

func main() {
  F()
}
Copy the code

The temp variable is allocated on the stack and not on the heap:

hewittwang@HEWITTWANG-MB0 rtx % go build -gcflags=-m
# hello
./new1.go:4:6: can inline F
./new1.go:9:6: can inline main
./new1.go:10:3: inlining call to F
./new1.go:5:14: make([]int, 0.20) does not escape
./new1.go:10:3: make([]int, 0.20) does not escapeh
Copy the code

When we change the above code:

package main
import "fmt"

func F() {
  temp := make([]int, 0.20)
  fmt.Print(temp)
}

func main() {
  F()
}
Copy the code

The temp variable is allocated to the heap. This is because temp is passed into the print function and the compiler assumes that the variable will be used later. Therefore, the application to the heap, the application to the memory on the heap will cause garbage collection, if this process (especially if garbage collection is constantly triggered) too often will cause the GC to become overstressed and the application performance problems.

hewittwang@HEWITTWANG-MB0 rtx % go build -gcflags=-m
# hello
./new1.go:9:11: inlining call to fmt.Print
./new1.go:12:6: can inline main
./new1.go:8:14: make([]int, 0.20) escapes to heap
./new1.go:9:11: temp escapes to heap
./new1.go:9:11: []interface {}{... } does notescape
<autogenerated>:1: .this does not escape
Copy the code

5. Object selection of “GC Roots”

In Java, due to the division of memory runtime regions, the following are commonly selected as objects for “GC Roots” :

  • Objects referenced in the virtual machine stack (the local variable table in the stack frame);

  • Objects referenced in the Native method stack;

  • The object referenced by the class static attribute in the method area;

  • The object referenced by the constant in the method area;

  • Java VIRTUAL machine internal references;

  • All objects held by the synchronization lock.

In Java, unreachable objects can escape. Even in the reachability analysis, unreachable objects are not necessarily dead. At this time, they are temporarily in the “probation stage”. In order to truly declare an object dead, at least two marking processes must be experienced. In addition, the runtime constant pool and classes in Java also need to be cleaned up from the runtime constant pool and method area classes.

The selection of Go is relatively simple, that is, the reference pointer in the global variable and G Stack, simply speaking, the reference pointer in the global quantity and Go procedure. Because there is no encapsulation concept of classes in Go, GC Root selection is also relatively simple.

Write barriers

In order to solve the problem of hanging pointer in concurrent tricolor reachambility analysis, two solutions are presented, namely “Dijkstra inserting write barrier” and “Yuasa deleting write barrier” respectively.

In Java, both of the above two methods are applied, for example, CMS is based on “Dijkstra insert write barrier” to do concurrent marking, G1, Shenandoah is using “Yuasa delete write barrier” to achieve.

Before Go v1.7, the runtime used Dijkstra insertion write barrier to ensure strong tri-color immutability. Go combined Dijkstra insertion write barrier and Yuasa delete write barrier in V1.8 to form a hybrid write barrier. The hybrid write barrier combined with the characteristics of the two to achieve concurrent stable GC in the following ways:

1. Scan all objects on the stack and mark them black.

2. During GC, any new objects created on the stack are black.

3. The deleted object is marked in gray.

4. The added object is marked in gray.

To ensure stack efficiency, hybrid write barriers are used for the heap area. That is, the stack area does not trigger the write barrier, only the heap area does. Since the reachable nodes initially marked by the stack area are black nodes, there is no need for the second STW scan. In essence, it combines the features of insert barrier and delete barrier, and solves the problem that insert barrier needs second scan. At the same time, different strategies are adopted for heap area and stack area to ensure that the running efficiency of stack is not damaged.

Seven,

From the perspective of garbage collection, Java garbage collection mechanism is relatively perfect after the development of multiple generations. Java is divided into the new generation and the old generation to store objects. Usually in Cenozoic allocated memory object, multiple objects of survival will be moved to the old age, because of the new generation survival rate low, produce the possibility of space debris is high, usually choose “tag – copy” as a recovery algorithm, high survival rate, and the old s usually choose “tag – clear” or “mark – up” as a recovery algorithm, compression space.

Go is a non-generational, concurrent garbage collector based on tricolor tag and sweep. Its advantages can be realized in combination with its TCMALloc memory allocation strategy. Since the allocation of small and micro objects has its own memory pool, all fragments can be perfectly reused, so GC does not have to worry about space fragmentation.

reference

1. Design and Implementation of Go Language

(draveness. Me/golang/docs…).

2. Go vs. Java Garbage Collection In an Expert’s Eyes

(blog.csdn.net/u011277123/…).

3. Go Language Problems

(www.bookstack.cn/read/qcrao-)…

4. CMS Garbage Collector

(juejin. Cn/post / 684490…).

5. Golang V 1.16

(github.com/golang/go)

6. Golang– Memory Management (Memory Allocation)

(t.zoukankan.com/zpcoding-p-…).

7. “Understanding the Java Virtual Machine in Depth: Advanced JVM Features and Best Practices (3rd edition)” — China Machine Press

Author’s brief introduction

Wang Hui

Tencent background development engineer, responsible for Tencent hotspot related back-end business, graduated from Software College of Nanjing University.