JVM basics (three): garbage collection mechanism

Preface

Nice to meet you~ Welcome to read my article.

There is a high wall between Java and C++ surrounded by dynamic memory allocation and garbage collection technology. People outside the wall want to go in, but people inside the wall want to come out.

Garbage Collection (Garbage Collection, also known as GC) is an eternal topic in virtual machines. The above sentence is a classic, and it also points out the importance of garbage collection. In c/c++, developers have the supreme right to memory, and they also need to be responsible for the object in the end: creation and release. After each piece of memory is used up, the free method needs to be called to release the memory. While JVM uses automation technology, developers do not need to care about memory allocation and release. When an object is no longer used, the garbage collection mechanism will reclaim the object. However, all these seemingly beautiful garbage collection mechanisms have "hidden murderous intent" behind them. The garbage collection mechanism is not perfect. When memory leaks or memory overflow problems occur due to improper use by the developer or other reasons, it is difficult to troubleshoot the problem if you do not understand the garbage collection mechanism of the JVM. Therefore, learning the garbage collection mechanism is not only to deal with interviews, but more importantly, to be able to write more robust code and solve the problems caused by the garbage collection mechanism.

There are four key issues with the garbage collection mechanism:

  1. What is the garbage collection mechanism?
  2. Which area is garbage collected?
  3. When will it be recycled?
  4. How to recycle?

These four issues can be said to be the core of the garbage collection mechanism. To understand these four issues, you also understand the garbage collection mechanism. The first question has been described above. In general, it is a mechanism in the virtual machine that can automatically reclaim and release memory for objects that are no longer in use . Then, the following will focus on the other three questions to explain.

What memory needs to be reclaimed

In the JVM basics (2): runtime data area , it is mentioned that the runtime data area of ​​JVM mainly includes thread-private method stack and program counter and heap area and method area shared by threads.

Method stacks and program counters are born as threads are born, and they die as threads die. Each method will be pushed into a stack frame when it is called. The size of each stack frame has also been determined during the compilation stage. With the method call and return, the stack frame will execute the stacking and popping operations in an orderly manner. This part of the memory Allocation and collection are deterministic , because garbage collection is not required. In contrast, the Java heap and method area are very uncertain. At runtime, the classes that need to be loaded in the method area, the addition of constants, and the creation of objects are all full of uncertainty and cannot be determined at the compilation stage. Therefore, the program will continue to create objects in these two areas when the program is running. It is very important to manage the memory of these two areas.

In these two areas, the management of the heap area is more important and common than the method area. In fact, there are very few garbage collections in the method area, and the timing of the collection is very demanding, such as class unloading. The heap area is the storage area for object instances, and the program will frequently create and then discard objects during runtime, so I will mainly focus on the heap area to talk about garbage collection.

Is it necessary to recycle the method area?

Have.

The data stored in the method area seems to be data that must exist during the entire program operation: class information, constants, various symbol references, etc. But in fact it is not . However, these recovery scenarios are relatively special and rare, and it is difficult to attract attention, and this is often the hidden danger of memory leaks. There are few objects to be recycled in the method area, and the recycling conditions are so harsh that the cost-effectiveness of recycling is low. In some virtual machines, the method area's recycling mechanism is not implemented, and the Java virtual machine specification does not explicitly require the implementation of garbage collection in the method area. But being difficult does not mean that you can not do it. In some scenarios, it is still necessary to recover the method area.

There are two main types of data recovered in the method area: class information and constants. Different class loaders load the same class from the perspective of the virtual machine. They belong to two completely different classes . In some scenarios where bytecode operations, a large amount of reflection, dynamic proxy, etc. are frequently used , the loaded class information There are many but usually only need to create an object and no longer use it, so it is necessary to recycle these class information to reduce the pressure in the method area, that is, class unloading. Similarly, constants are not required to be used during the entire program run, and some constants that are no longer used can also be recycled. So how to judge that a class or a constant needs to be recycled?

Constants are relatively simple. Obsolete constants or constants that are no longer used can be recycled . How to judge? This constant is not referenced anywhere in the virtual machine, then it is an obsolete constant.

The class information is more complicated and needs to meet three conditions:

  1. All object instances of this class, including subclasses, are recycled.
  2. The class loader that loaded the class is recycled.
  3. The Class object of this class is not referenced.

At this time, you can uninstall this class. In fact, many of our classes are loaded using application class loader or system class loader, so the second condition is difficult to meet. Therefore, under normal circumstances, the classes loaded by the custom class loader are more likely to be recycled. I'll talk about the content of the class loader later.

How to identify garbage

As mentioned earlier, the main area of ​​garbage collection is the Java heap area, and this area mainly stores object instances. To perform garbage collection, we must first determine whether an object is garbage, so how to determine whether an object is garbage? There are two very common solutions: reference counting and reachability analysis.

Reference counting

The citation counting method may be the first type of spam notation that readers recognize, and it is also the most widely recognized algorithm. The concept of reference counting is relatively simple:

Use an extra memory to count the references of the object. When a reference is made to this object, the reference count is +1, and when a reference becomes invalid, the reference count is -1. If the reference count of an object is 0, it means that the object is not referenced and is marked as garbage.

The reference counting method has two advantages: the principle is simple, and the judgment efficiency is high. It is also adopted and learned for these two reasons. However, he has a fatal shortcoming: unable to solve the circular reference problem .

For example: A holds B, B holds A, then if A and B are no longer used, but the two cannot be released. You may think that there are very few mutual references between these two objects. However, when there are more and more objects and the logic becomes more and more complicated, the references between the objects may cause the objects in the entire heap area to be unable to be released. , Which caused a serious memory leak.

Therefore, the reference counting method needs a lot of extra processing to ensure correct work . At the same time, each object needs extra space to store the counter . Although not much, it is also a memory cost.

Accessibility analysis

The reachability analysis method solves the circular reference problem of the reference counting method very well, and it is also the algorithm used by most virtual machines at present. The concept of accessibility analysis is:

Starting from a series of objects called GCRoots, search downwards based on the references they hold, and the route taken by the search is called the "reference chain". If there is no reference chain between an object and GCRoots, then the object is marked as garbage.

The advantage of this algorithm is that it solves the problem of circular references. But it brings two problems: how to determine GCRoots and reference bloat.

The first is the determination of GCRoots. What objects can be used as GCRoots? Objects that exist and can be directly accessed by the virtual machine (The author did not find the most rigorous definition of GC Roots. This conclusion may not be accurate. In fact, there are many factors considered to determine the GCRoots object, and this is just my summary. Easy to understand). Give the most obvious example. The object in the stack frame refers to the object pointed to. These objects are currently in use, indicating that they must be alive. At the same time, the virtual machine can access these objects through the stack frame. Then the objects referenced by these objects may also be used, so they are not garbage. The objects that can be used as GCRoots have the following types:

  • The objects referenced in the method stack include the virtual machine stack and the local method stack.
  • The object referenced by the static property of the method area.
  • The object referenced by the constant.
  • Objects referenced inside the virtual machine.
  • The object held by the synchronization lock.
  • JMXBean that reflects the internal situation of the virtual machine, callbacks registered in JCMTI, local code cache, etc.
  • Objects temporarily added according to specific areas are mainly the addition of cross-generation references.

There are many types. Remember my summary that may not be rigorous: the object must exist and can be directly accessed by the virtual machine, then the object can be used as GCRoots . These objects will definitely exist at runtime, so they can be used as GC Roots.

The second problem is citation bloat. When there are more and more objects in memory, the reference chain of a GC Root will become a huge tree at this time, so the performance consumption of traversing this tree will be higher. Then some additional operations are needed to optimize the problem of reference expansion, such as generational collection, etc. This problem will be slowly discussed later.

Java four major reference types

Both of the above algorithms are inseparable from one concept: reference. According to different scenarios, Java has designed 4 different reference types. We should use strong references and weak references the most in daily life. A correct understanding of garbage mark algorithms and reference types can help us reduce memory leaks.

Strong citation

Strong references are also called direct references. For example Person p = new Person(), this pis a strong reference to the Person instance. In the case that the object has a strong reference, the object will never be recycled, and an OutOfMemoryError will be thrown if the memory is insufficient.

Soft reference

Soft references are a bit weaker than strong references. The method of use is SoftReference<Person> pS = new SoftReference<>(new Person);. When an object has only soft references, it will be recycled by the system when the memory is insufficient. If the memory is sufficient, it will not be recycled.

This reference is generally used for some resources held by the object, but these resources are not absolutely necessary. After the program crashes and the choice of releasing resources, the release of resources is chosen.

Weak reference

Weak references are one grade weaker than soft references. The method of use is WeakReference<String> s = new WeakReference<>("");. For an object with only weak references, it will be recycled regardless of whether the memory is sufficient.

Weak reference emphasizes that the relationship between two objects is very weak. The problem he solves is: A does need to be referenced to B, but when B needs to be recycled, it cannot be recycled because it is referenced by A. At this time, the use of weak references can solve the problem well. For example, in the MVP design pattern in Android, Activity holds a reference to the Presenter and can directly call the Presenter's interface to make a request. Similarly, the Presenter needs to asynchronously call back the Activity's interface to update the UI. However , if the Presenter directly holds the reference of the Activity, then when the interface is exited, the Activity cannot be recycled in time and memory leaks. So at this time, Presenter can only hold weak references to Activity . When he is there, I can find him at any time. When he wants to go, I can’t stop him . The reason is the same. Click it).

Phantom reference

It has no impact on the life cycle of the object, and it is not possible to obtain an object instance through a virtual reference, only to receive notification when the object is recycled.

summary

Different types of references have different applicable scenarios, and there is no better or worse, and there is no need to think that strong references will definitely cause memory leaks and deliberately use weak references. More importantly, pay attention to the applicable scenarios and the problems solved by these several references. Then when you encounter these scenarios, you can use the appropriate reference type.

How to collect garbage

Earlier we analyzed how to find objects that need to be recycled. There are two algorithms: reference counting and reachability analysis. Now the question is coming. Since these two algorithms can determine whether an object needs to be recycled, can it just traverse all the objects in the heap and mark all the objects that need to be recycled, and then reclaim the garbage objects? Of course not. The GC mechanism is to release memory and reduce memory pressure. At the same time, GC itself, as an automated tool for memory management, must not cause too high performance pressure . In the process of collecting these garbage, directly traversing all objects in the heap is a very low-performance approach, which will seriously affect the performance of the program. To understand how to improve the performance of garbage collection, we must start with a very important theory: generational collection theory.

Generational collection theory

The generational theory is based on two hypotheses:

  1. Weak generational hypothesis: the vast majority of objects are dying
  2. Strong generational hypothesis: the more difficult it is for the object to die out after more garbage collection

These two hypotheses point out that two objects of different ages have different characteristics: newly created objects are mostly collected, while objects that survive multiple collections are rarely collected later . Although these are two hypotheses, they have also been confirmed in practice. In fact, it is well understood that most of the created objects are temporary objects, and the longer they survive, the larger or more important the scope of this object is, and it will continue to survive in the future. Then we can implement different recycling algorithms for these objects with different characteristics to improve GC performance:

  1. For newly created objects, we need to GC them more frequently to release memory, and only need to record the objects that need to be left each time, instead of marking other large objects that need to be recycled to improve performance.
  2. For objects that have survived many GCs, GC can be performed on other gates at a lower frequency, and only a small number of objects that need to be recycled each time need to be paid attention to.

For these two different objects, we can divide the heap into two regions: one for storing newly created objects, and one for storing objects that survive multiple GCs. Then the former is called the new generation , while the latter is called the old generation . This theory of implementing different collection schemes for objects of different ages is called generational collection theory . It can be found that this solution has greatly improved the performance of traversing all objects in the heap directly. Of course, there are different and more specific collection algorithms for specific age objects, which we will talk about later. Here we want to talk about another topic: Is generational collection really just dividing the heap area into two parts?


Let's first look at a situation, as shown in the figure:

If we need to recycle the young generation at this time, then traverse all the objects in the young generation. Finally, object 1 and object 3 are alive, and object 4 needs to be recycled. However, the problem is that in the memory area of ​​the new generation, object 2 is unreachable by GC Root, but object 5 is alive and references object 2, then object 2 should be alive. If GC is only performed on the young generation, then it is very likely to "manslaughter" many cross-generation referenced objects, such as object 2. So what can we do? The easiest way is to traverse the old generation once and find out all the cross-generation references. As we said earlier, this kind of efficiency is too low. Every GC new generation has to traverse the old generation, so the performance without generational generation The difference is not much. The reason for this "manslaughter" problem is that, in the final analysis, cross-generation references , if there is no cross -generation reference , there would be no such problems. How to solve this problem involves another hypothesis:

Hypothesis of inter-generational citations: Inter-generation citations are only a minority compared to the same-generation citations.

So, does this hypothesis mean that we can ignore these few objects? Of course not~, the most obvious problem is that if object 2 is killed by mistake, then when object 5 calls the method of object 2, a null pointer exception will occur. The few keywords illustrate two problems: we don’t need to spend a lot of time to traverse the old age for him; we can solve this problem by allocating some memory resources to him, and a few indicate that the memory occupied is small, and a small amount of space is used. It is definitely worthwhile to exchange time. Here is an algorithm:

We can maintain a "memory set" in the new generation. The memory set divides the old age into different areas and puts objects with cross-generation references in the old age in a small memory. Then when the next generation GC is next time, only this small part of the memory needs to be scanned, without traversing all the old generation objects.

This can improve a lot of performance compared to the previous brute force scanning of the old generation, but it also has a price: a certain space is stored in the "memory set", and the correctness of the data needs to be maintained when the object changes reference. The first one is easy to understand, and the second cost, such as the object 5 in the above figure, if he abandons the reference to the object 2, then the "object set" should take him out of this small piece of memory.


As mentioned earlier, different ages require different algorithms to collect data and cannot be generalized. For example, there are few surviving objects each time in the new generation, the workload that needs to be collected for each GC marking is much more than the workload of marking surviving objects. So let's introduce different collection algorithms below.

Mark-sweep algorithm

This algorithm is simple and rude, and it is also the most basic collection algorithm. The algorithm concept is as follows:

Each time it traverses and marks the objects that need to be recycled or the surviving objects, and then recycles the marked or unmarked objects one by one.

Is not it simple? But there are big problems:

  1. Performance is unstable. When the young generation needs to reclaim a large number of objects each time, it is obviously very inefficient to perform a large number of markings and then release the memory one by one.
  2. Space fragmentation. This is easy to understand. After recycling, there will be a lot of discontinuous space left, which will cause large objects to fail to be created.

Mark-copy algorithm

This algorithm is designed for the new generation:

Divide the memory into two equal parts A and B, and use only one part at the same time, for example, use part A.
When garbage collection, all the living objects in part A are copied to another part B.
The current memory of part A is all released, and then part B is regarded as the main memory.

This has two advantages: ** Only a small number of surviving objects need to be transferred, and then a whole piece of memory is recovered uniformly; the transferred objects are concentrated, and there will be no space fragmentation problems. **But there is a big price: only half of the memory can be used . According to a study by IBM, 98% of the objects in each new generation GC are recycled. Although we are skeptical about this data, it is certainly not cost-effective to divide the memory into equal halves. Then there is a new solution: Appel recycling .


The design background of Appel-style recycling is that 98% of the objects are recycled every new generation GC. He divided the memory into three parts: Eden and two Survivors. Eden occupies 80% of memory, and each Survivor occupies 10% of memory. Appel-style recycling algorithm is also not complicated:

Mainly use Eden part and a Survivor part.
When a GC occurs, all the surviving objects are copied to another Survivor, and Eden and the original Survivor are released.
At this time, the main memory used becomes the Survivor area of ​​the Eden part and the surviving object storage.

Draw a picture to help understand:

On the left, Eden and Suivivor1 are used. After the GC, object 1 and object 3 are alive, then they are copied to Survivor2. Then the memory area used becomes Survivor2 and Eden.

The advantage of this is that we only need to take up 10% of the memory to copy, and 90% of the memory space can be used. But there is always an unexpected moment. If the surviving objects exceed the space of Survivor, then the unsupported objects will be hosted in the old generation first. Wait until there is enough space for recycling to move the object back, or directly upgrade it to the old generation object. If there is no extra space in the old age, a GC in the old age will be triggered to make enough space.

Mark-up algorithm

The previous mark-copy method is for the new generation, but for the old generation, it is completely unsuitable. There are many surviving objects in the old age. If copying is performed, a large number of copy operations are required each time, which is very inefficient; and if all the objects are alive, the other half of the space may not be enough to store, which will directly cause problems. , Because there is no other space for temporary storage. So there needs to be another scheme that is more in line with the characteristics of the old age: the mark-organization

Mark the surviving objects, and then move all to the memory on one side, and release all the memory outside the boundary

This is actually easier to understand. There are more objects in the old age, so there are not many objects to be cleaned each time. If only the mark-sweep method is used, fragmented space will be generated, and the mark-up method does not immediately reclaim the objects after marking. , But move the surviving object to one side, and then release the memory outside the boundary, so that the fragmented space can be put together. Attentive readers may have discovered a problem: In this way, would it not be very inefficient to move all or most of the objects almost every time in the old generation of GC ? This is the drawback of this method. In fact, his drawbacks are more than that. When the object moves, the entire application must stop and wait for the GC to complete , otherwise the reference address error may occur, which not only reduces the performance problem, but also may cause intermittent freezes.

Can we solve the problem of fragmented space without moving objects? Yes, use the fragmented space through a concept similar to hard disk partitioning. The new problem that this brings is: the need for more complex memory allocators and memory accessors. And this is not only a problem that the code is complex and difficult to implement, but more importantly, it will lead to a decrease in the performance of accessing memory . Therefore, whether it is moving or not, it will cause performance overhead. In fact, the frequency of program access to memory is much higher than the frequency of GC, so under comprehensive consideration, the overall performance of the mark-organization method is better. Note that what is written here is the overall performance. If you pay attention to the low latency of GC, then the marking-organization method that needs to stop the application for GC is not suitable. Therefore, the GC collection algorithm used by each virtual machine is the result of a trade-off under the specific requirements of a specific scenario.

But there is another optimized marking-organization scheme: the "he thin mud method".

Only use the mark-clear method when the memory is sufficient, and execute the mark-clean method again when the fragmentation program affects the creation of the object.

In this way, the number of moving objects can be significantly reduced, and the performance of the marking-organization algorithm can be improved.

Will it be recycled if it is marked as garbage?

When studying this problem, the author felt that our daily development was not available. "In-depth understanding of the Java virtual machine" book also emphasizes not to use related methods. So why did you write this part? Two reasons: interview and broaden knowledge . "Interview makes a rocket," of course, will not let go of any questions, especially such questions, interviewers like to ask. Secondly, I think it is more important to be able to have a more comprehensive study of the knowledge of the GC mechanism.

Since I asked this question, it must not be. Let's review the process of GC first:

  1. First determine the area to be GC. Here is more subdivision, the new sound generation or the entire heap area? (In fact, GC is rarely performed on the old generation alone)
  2. Find out all objects that need to be recycled or surviving objects, and mark them
  3. The GC collector starts collecting. Use different methods to release memory according to different algorithms

There is a missing step between 2 and 3, which is the reason why the garbage marked as garbage will not necessarily be recycled.

After the object is marked as garbage for the first time, if the object overrides the finalize method and has not been executed , the object will be placed in a queue, waiting for the virtual machine to call. If the object assigns itself to another reference in the finalize method, then it will not be recycled.
If the object has executed the finalize method once, it will be recycled directly the next time it is marked, and the second finalize will not be executed. method.

So when the object is marked as garbage, if it overwrites finalize, it will enter a queue and wait for the virtual machine to call the finalize method. Make yourself alive by calling the finalize method to assign yourself to other references. However: the virtual machine uses a low-priority thread to perform this task and does not wait for the finalize method to finish . The reason is that if an infinite loop occurs in finalize, it will cause blockage, causing the entire GC to fail to run.

So you can see that the finalize method has two very big shortcomings: the execution time and completion are uncertain and costly. The author's point of view in the book "In-depth Understanding of the Java Virtual Machine" is: forget this method and don't use it . And we can just do some understanding. So a very similar problem is discussed below, System.gc()will it trigger the recovery after calling it?

System.gc()Will the call always trigger a recycling

The answer is definitely no. According to the official comment, this method only informs the virtual machine that garbage collection is required, and the virtual machine tries to do it for you, but it is uncertain when to do it or not . Therefore, it can be seen from the official comments that this recycling is uncertain, including Runtime.getRuntime.gc(), in essence, the former calls the latter. Regarding System.gc(), there are many opinions that are not recommended:

  1. After calling System.gc(), we are not sure what the virtual machine does. Not sure when, how, or even if the GC will run.
  2. This method will reduce the GC efficiency of the virtual machine. The virtual machine has its own garbage collection strategy, and our operations will destroy this strategy and reduce performance. For example, in the previous "and thin mud" method, the original virtual machine should perform GC when the fragmented space affects the creation of the object, and if we perform gc in advance, this strategy will be destroyed.
  3. The GC mechanism of the virtual machine knows how to perform GC better than we do. We don't need to manually GC at all.

Of course, there are also views that agree with the use:

  1. It is recommended not to rely on it to do anything right. Don't rely on its work, but it's perfectly okay to imply that now is an acceptable collection time.
  2. This method can also be used when trying to find out if a particular object is leaking.
  3. If the virtual machine's strategy is to GC after insufficient memory, and there are more important tasks in the future, then GC can be performed in advance.
  4. GC can be performed when the computer is idle.

I have only sorted out a part of the views above, and interested readers can go to search engines to search. Let me talk about my own views:

Only do it if you know exactly what you are doing and what the result is . We will find that the arguments against using the gc method revolve around uncertainty and performance degradation, and why these two problems are caused, in the final analysis, is that it is not clear what happened when using the gc() method. The gc() execution strategy is not only affected by the virtual machine GC algorithm, but also by the current operating conditions. When it is determined that the gc() method needs to be used in one place, and it is proved to be effective through a comprehensive test, then the gc() method is completely fine. When you are not sure what impact gc() will have on the system, it is obviously not good to use gc() indiscriminately.

to sum up

Well, here is almost the GC mechanism of JVM. Let's review it again. There are four most important questions about GC: what is GC, where is GC, when is GC, and how is GC. I believe that after reading this article, I have my own answers to these questions. The first part of the article talks about the memory area that needs to be recycled, and at the same time discusses whether the method area needs to be recycled; the second part discusses how to judge whether an object is garbage, that is, the garbage marking algorithm; the third part discusses how to improve garbage collection Based on the generation theory, different collection algorithms are discussed. Finally, two common problems are discussed.

So here is the end of the basic theoretical content about the JVM GC mechanism. Hope the article is helpful to you.

This is the full text. It is not easy to be original. If it is helpful, you can like it, bookmark it and forward it.
The author has limited ability. If you have any ideas, please feel free to communicate and correct in the comment area.
If you need to reprint, please private message.

Also welcome to my personal blog: Portal

Guess you like

Origin blog.csdn.net/weixin_43766753/article/details/109272135