There is a wall of dynamic memory allocation and garbage collection between Java and C++
We know that managing memory manually means free, fine-grained control, but it relies heavily on the level and care of the developer.
Memory leaks can occur if you forget to free memory space after using it, and unexpected problems can occur if you free the wrong memory space or use dangling Pointers.
At this time, Java came along with the Garbage Collection (GC), which left the management of memory to GC, reducing the programmer’s programming burden and improving the development efficiency.
So it’s not that we don’t need memory management with Java, it’s just that GC is carrying the load for us.
However, GC is not a panacea, different scenarios apply different GC algorithms, and different parameters need to be set, so we can’t just leave it alone, only a deep understanding of it can make it work.
I believe that many people are familiar with GC content. I first learned about GC from Understanding the Java Virtual Machine, but that book alone is not enough to cover GC.
I thought I knew a lot at the time, but it took some education to understand what ignorance is fearless.
In addition, after a period of time, a lot of GC content can not be said. In fact, many students said that some knowledge will be forgotten after learning, and some content was understood at that time, but after a period of time, they can not remember anything.
Most of the time it’s because it’s not structured in my mind, not understanding the cause and effect, not connecting the dots.
Recently, I sorted out the knowledge points related to GC, and wanted to expand the content related to GC from points and aspects, and clarify my thoughts along the way, so I output this article, hoping to be helpful to you.
There are a lot of things about GC, but it doesn’t need to be too deep for our general development, so I just picked out the ones I thought were important, and I deleted some of the source code. I didn’t think it was necessary, but it was important to clarify the concept.
I was also going to analyze the various garbage collectors for the JVM, but the article is too long, so I’ll write it in two and post it in the next one.
The GC content of this article is not limited to the JVM but is generally jVM-biased, and the default implementation is HotSpot.
The body of the
First of all, according to the Java Virtual Machine Specification, Java virtual machine runtime data is divided into program counters, virtual machine stacks, local method stacks, heaps, and method areas.
The three areas of the program counter, virtual machine stack, and local method stack are thread private and are automatically reclaimed when a thread dies, so there is no need to manage them.
So garbage collection only needs to focus on the heap and method areas.
And the method area recycling, often low cost performance, because the judgment can be recycled conditions are more stringent.
Unloading a class, for example, requires that all instances of the class have been reclaimed, including subclasses. Then the class loader that needs to be loaded is also recycled, and the corresponding class object is not referenced so that it is allowed to be recycled.
As far as classloaders are concerned, they are almost impossible to recycle unless there is a scenario like OSGI designed to replace classloaders.
The high return for garbage collection is the collection of memory in the heap, so we focus on garbage collection in the heap.
How do I determine if an object is garbage?
Since it is garbage collection, we need to determine which objects are garbage first, and then see when and how to clean them up.
There are two common garbage collection strategies: one is direct collection, that is, reference counting; The other is indirect recovery, that is, tracing recovery (accessibility analysis).
It is also well known that reference counting has a fatal flaw – circular references, so Java uses reachability analysis.
So why do so many languages adopt counting references that are obviously flawed?
For example, CPython, reference counting is a bit useful, so let’s start with reference counting.
Reference counting
Reference counting is basically setting a counter for each memory location. When it is referenced, the counter increases by one. When the counter decreases to zero, it means that the location can no longer be referenced, so memory can be freed immediately.
As shown in the figure above, the clouds represent references, and object A has one reference, so the counter has A value of 1.
Object B has two external references, so the counter value is 2, whereas object C is not referenced, so the object is garbage and can be freed immediately.
This tells you that reference counting takes up extra storage space, which becomes obvious if the memory unit itself is small.
Second, the memory release of the reference count is equivalent to spreading this overhead over the daily running of the application, because the moment the count reaches zero is the moment the memory is freed, which is actually suitable for memory-sensitive scenarios.
In the case of collection for reachability analysis, objects that become garbage are not cleared immediately and will have to wait for the next GC to be cleared.
Reference counting is a relatively simple concept, but the flaw is the circular reference mentioned above.
How does something like CPython solve the problem of circular references?
First we know that things like integers and strings don’t refer to other objects inside them, so there is no problem with circular references, so reference counting is not a problem.
That is why container objects such as List, dictionaries, and Instances can create circular dependencies, so Python has introduced mark-clear backup processing in addition to reference counting.
But the specific approach and the traditional mark – clear is not the same, it takes to find unreachable objects, rather than reachable objects.
Python uses a bidirectional linked list to link container objects. When a container object is created, it is inserted into the list and removed when it is deleted.
Add a field gc_refs to the container object. Now let’s see how circular references are handled:
- For each container object, set gC_refs to the reference count for that object.
- For each container object, find the container object it refers to and reduce the gC_refs field of the container object it finds referenced.
- Move container objects whose GC_refs is greater than 0 to a different collection because gC_refs is greater than 0 means that there is an external reference to it and therefore these objects cannot be freed.
- Then find the objects referenced by the container object whose GC_refs is greater than 0. They cannot be cleared either.
- The last remaining object description is only referenced by objects in the list, no external references, so it is garbage to remove.
In the following example, objects A and B are referenced in A loop, and objects C refer to objects D.
In order to make the picture clearer, I have taken screenshots of steps 1-2 in the picture above and steps 3-4 in the picture below.
Eventually both A and B referenced in the loop can be cleaned up, but there is no such thing as A free lunch, and one of the biggest overheads is that each container object requires additional fields.
There is also the overhead of maintaining container lists. According to PyBench, this overhead accounts for about 4% of the deceleration.
At this point, we know that reference counting has the advantage of being simple to implement and clean up memory in a timely manner. The disadvantage of reference counting is that it cannot handle circular references. However, it can be combined with mark-clean schemes to ensure the integrity of garbage collection.
So Instead of solving the circular reference problem of reference counting, Python combines the unconventional mark-erase scheme to save the curve.
And in extreme cases reference counting isn’t that timely, you know, if you have one object referring to another object, and another object referring to another object, and so on.
So when the first object is about to be recycled, there will be a chain recycling reaction, and the delay will be highlighted if there are many objects.
Accessibility analysis
Reachable analysis actually uses mark-sweep, which marks reachable objects and clears unreachable objects. As to using what means clear, clear after whether to arrange this is later.
Mark-sweep is done periodically or when memory is low, starting with GC Roots, traversing the scan, marking all scanned objects as reachable, and then reclaiming all unreachable objects.
Root references include global variables, on-stack references, registers, and so on.
I don’t know if it feels a little bit like this, but we do GC when we’re out of memory, and when we’re out of memory we have the most objects, the most objects and therefore it takes a long time to scan tokens.
Therefore, mark-sweep means that garbage is accumulated and then removed once, which consumes a lot of resources during garbage collection and affects the normal operation of the application.
Therefore, generational garbage collection and the method of marking objects directly from the root node and then tracing can be introduced.
But this can only be mitigated, not eradicated.
I think this is the biggest difference between the idea of mark-sweep and reference counting, one that saves the processing, and one that spreads the consumption evenly over the daily run of the application.
Whether it’s token-clear or reference counting, you only care about reference types, not things like integers.
So the JVM also needs to determine what type of data is on the stack, which can be classified as conservative GC, semi-conservative GC, and exact GC.
Conservative type GC
Conservative GC refers to the type of data that the JVM does not record, that is, it is impossible to tell whether data at a location in memory is of reference type or non-reference type.
So you can only guess if there are Pointers based on some criteria. For example, when scanning on the stack, we can determine whether this is a pointer to the GC heap based on whether the address is within the upper and lower bounds of the GC heap, whether the bytes are aligned, etc.
It’s called conservative GC because the one that doesn’t meet the criteria is definitely not a pointer to the GC heap, so that memory is not referenced, while the one that does is not necessarily a pointer, so it’s a conservative guess.
Let me draw another picture to explain it, and it should be pretty clear when I look at the picture.
We know you can tell by pointing to an address, whether it’s byte aligned, whether it’s in the range of the heap, but it’s possible that a value that happens to have a value is the value of the address.
That’s confusing, so you can’t be sure it’s a pointer, you can just assume it’s a pointer.
Therefore, there can be no cases of accidental killing. An object is dead, but a suspected pointer points to it, mistook it for being alive and let it go.
So conservative GC lets some garbage out and is not very memory friendly.
And because of the suspected pointer, we cannot confirm whether it is a real pointer, so we cannot move the object, because to move the object, we need to change the pointer.
One way to do this is to add an intermediate layer, called the handle layer, where references refer to the handle and then to the handle list to find the actual object.
So the direct reference does not need to change, if you want to move the object just need to modify the handle table. But then you have one more layer of access, and it becomes less efficient.
Semi-conservative GC
Semi-conservative GC, where the type information is recorded on the object but not anywhere else, so scanning from the root is still a guesswork.
However, after the objects in the heap are obtained, the information contained in the objects can be accurately known. Therefore, tracing is always accurate and thus called semi-conservative GC.
You can now see that only objects scanned directly from the root cannot be moved by semi-conservative GC. Objects traced back from the direct object can be moved, so semi-conservative GC can use either a partial object moving algorithm or a mark-clear algorithm that does not move objects.
Conservative GC can only use mark-sweep algorithms.
Accurate type GC
As you already know, accuracy means that the JVM needs to know the type of the object clearly, including references on the stack.
You could conceivably tag Pointers to indicate types, or record type information externally to form a mapping table.
HotSpot uses a mapping table called OopMap.
In HotSpot, the object type information will record its own OopMap, which records the type of data at which offsets within that type of object, and methods executed in the interpreter can automatically generate OopMap for GC to use through the interpreter function.
A JIT-compiled method also generates an OopMap at a specific location, recording which locations on the stack and in registers are referenced when an instruction to that method is executed.
These specific locations are mainly in:
- The end of the loop (not counted loop)
- Method before returning/after calling the method’s call instruction
- Location where an exception may be thrown
These locations are called safepoints.
So why do you choose these places to insert? Because it would be too expensive to record an OopMap for every instruction, these key locations are selected to record.
So in HotSpot GC is not accessible anywhere, only at safe points.
Now that we know that oOPMaps in object types can be computed at class load time, oOPMaps generated by the interpreter, and OOPMaps generated by the JIT, we have sufficient conditions to determine the object type accurately at GC time.
So it’s called exact GC.
There are also JNI calls that are not executed in the interpreter and are not JIT compiled, so OopMap is missing.
In HotSpot, accuracy is addressed through handle wrapping, such as JNI’s entry arguments and return value references, which access the real object through the handle.
Instead of scanning the JNI stack frame during GC, you can scan the handle table to see which objects in the GC heap are referenced by JNI.
safer
We have already mentioned the safe point, which of course is not only for logging OopMap, because GC requires a consistency snapshot, so the application thread needs to pause, and the choice of pause point is the safe point.
So let’s go through our thoughts. Start with a GC noun and call the application mutator in a garbage collection scenario.
An object that can be accessed by the mutator is alive, meaning that the mutator’s context contains data that can be accessed from the live object.
This context really refers to the data on the stack, registers, etc. For the GC it only cares about where the reference is on the stack, registers, etc., because it only cares about the reference.
But the context is constantly changing as the Mutator runs, so the GC needs to take a consistent context snapshot to enumerate all the root objects.
In order to obtain a snapshot, all mutator threads need to be stopped. Otherwise, there will be no consistent data and some live objects will be lost. The consistency here is actually similar to the consistency of transactions.
The points in all of the mutator threads that have a chance to become pause points are called safepoints.
The openJDK website defines security points as follows:
A point during program execution at which all GC roots are known and all heap object contents are consistent. From a global point of view, all threads must block at a safepoint before the GC can run.
However, SafePoint is not only useful for GC, such as DeOptimization, Class Redefinition, but GC SafePoint is better known.
Let’s think about where we can put this safety point.
Every bytecode boundary can be a safe point for the interpreter, and there are many safe points that can be inserted in JIT-compiled code, but only in specific places.
Because the security point needs to check, and check requires overhead. If there are too many security points, the overhead will be too large, which is equal to the need to check whether the security point needs to be entered every few steps.
Secondly, the OopMap is recorded as we mentioned above, so there is an extra space overhead.
So how did mutator know it needed to pause at a safe point?
Check () check (); check ();
The check at the time of the interpretation execution is to polling a flag bit at the safe point, which will be set if GC is to be entered at this point.
While the compile execution is polling page unreadable, when the need to enter SafePoint, the memory page is set to inaccessible, and then compile the code access will occur an exception, and then catch the exception suspend or pause.
Some of you might say, well, what about the thread that’s blocked? It won’t make it to safety. Can’t wait for it?
This is where the concept of a security zone is introduced. A zone in a code segment where the reference relationship does not change is called a security zone.
It is safe to start GC anywhere in the zone, and threads executing into the zone will identify themselves as entering the zone,
So there is no need to wait for GC, and these threads will check to see if GC is going on when they exit the safe zone, block if so, and continue if GC is finished.
Some of you might have some doubts about the counted loop, like for (int I…) There’s not going to be a counted loop here.
So let’s say you have some counted loop and it’s doing something really slow, so there’s a good chance that all the other threads are going to the safe point of blocking and waiting for the loop to finish, and that’s going to be stuck.
Generational collection
We mentioned earlier that a mark-and-sweep GC is simply a collection of garbage, so centralized collection can have an impact on application performance, so the idea of generational collection is adopted.
Because research has found that some objects are basically immortal and live for a long time, while other objects are snapped soon after they come out. This is the weak generation hypothesis and the strong generation hypothesis.
Therefore, the heap is divided into new generation and old age, so that different areas can be treated according to different recycling strategies to improve recycling efficiency.
For example, the new generation of objects have the characteristics of life and death, so the return rate of garbage collection is very high, and there are few living objects that need to be traced, so the collection is also fast, and garbage collection can be arranged more frequently.
If there are only a few surviving objects in the new generation garbage collection, many objects need to be cleared each time with the mark-clean algorithm. Therefore, the mark-copy algorithm can be used to copy the surviving objects to a region each time, and the rest can be directly cleared.
But the naive mark-copy algorithm splits the heap in half, which is a poor memory utilization of only 50%.
So the HotSpot virtual machine has one Eden zone and two Survivor zones, with a default size ratio of 8:1:1, which gives 90% utilization.
Each collection copies the surviving objects to a Survivor region and then empties the other regions. If the Survivor region does not fit, it is put into the old age. This is the allocation guarantee mechanism.
However, the objects of the old age are basically not garbage, so the time of tracing marks is longer and the return rate of collection is lower, so the collection frequency is arranged lower.
This area due to the clear object every time very few, so you can use tags – clear algorithm, but just remove without moving objects will have a lot of memory chips, so there is a call tag – sorting algorithm, after every time is equal to remove the need to memory neat neat, need to move the object, the more time-consuming.
So you can use a combination of mark-clean and mark-clean to collect old ages. For example, mark-clean is used everyday, and mark-clean is used when too much memory fragmentation is detected.
There are probably a lot of people who don’t have a very clear mark-clear, mark-tidy, mark-copy algorithm, but let’s do a little bit of a review.
Mark-clear
There are two stages:
Marking stage: Tracing stage, tracing the graph of objects from the root (stack, registers, global variables, etc.), marking each object encountered.
Cleanup phase: Objects in the heap are scanned and marked as garbage collected.
This is basically the process shown below:
Clearing does not move or defragment memory space, and is usually marked by a free linked list (bidirectional linked list) to indicate which chunk of memory is free and available, thus leading to one condition: space fragmentation.
This makes it clear that the total memory is sufficient, but the requested memory is insufficient.
And in the memory application is also a bit of trouble, need to traverse the linked list to find the appropriate memory block, will be more time-consuming.
Therefore, there will be multiple free linked lists, that is, different linked lists are formed according to the size of memory blocks, such as large and small block linked lists. In this way, different linked lists are traversed according to the size of memory blocks to speed up the application efficiency.
Of course, there are more linked lists.
And then there’s the tag, which we think of as the tag on the object, like the tag bit in the header of the object, but that’s not compatible with copy-on-write.
This means that every time a GC is forked, the object must be replicated.
So there’s a bitmap notation, where you actually label a block of memory in the heap with a bit. Just as we have pages of memory, memory in the heap can be divided into chunks, and objects are in one or more chunks of memory.
Based on the address of the object and the starting address of the heap, we can calculate the number of blocks on which the object is located, and then set the number of bits in a bitmap to 1 to indicate that the object at that address is marked.
And not only can bitmap tables take advantage of copy-on-write, but cleaning is also much more efficient. If the tag is on the head of an object, then you need to traverse the heap to scan the object. Now with bitmaps, you can traverse the cleaning object quickly.
But whether you mark the object header or use a bitmap, the mark-cleared fragmentation problem is still not handled.
Hence the introduction of mark-copy and mark-tidy.
Mark-copy
First, the algorithm divides the heap into two parts, one is From and one is To.
Objects are only generated on From, and after GC occurs all live objects are found, copied To the To section, and then recycled From section as a whole.
Switch the identities of the “To” and “From” zones, that is, “To” becomes “From”, and “From” becomes “To”, and I’ll use a graph To explain another wave.
As you can see, the allocation of memory is compact and there is no memory fragmentation.
There is no need for the existence of free linked list. It is very efficient to move the pointer to allocate memory.
The affinity for CPU cache is high, because a node is traversed from the root, which is depth-first. The associated objects are found, and the memory is allocated in a similar place.
Thus, according to the principle of locality, when an object is loaded, the object referenced by it is also loaded, so the access cache hits directly. ,
Of course, it has its drawbacks, because objects can only be allocated in the From area, which is only half the size of the heap, so memory utilization is 50%.
Secondly, if there are many living objects, the pressure of replication is still very large and will be slow.
However, the conservative GC mentioned above is not suitable because of the need to move objects.
Of course, what I described above is depth-first, which is recursive calls with stack overflow risks, and there is also a Cheney GC replication algorithm that uses iterative breadth-first traversal. I won’t do the analysis, but I’m interested in searching by myself.
Mark-tidy
In fact, mark-collation and mark-copy are similar, the difference is that the replication algorithm is divided into two parts of the replication, while collation is not partitioned, direct collation.
The idea of the algorithm is still very clear, will live objects to the border collation, there is no memory fragmentation, also do not need to copy the algorithm to free up half of the space, so the memory utilization is also high.
The downside is that you need to search the heap multiple times. After all, the heap is marked and moved in one space, so overall it takes a lot of time, and if the heap is large, the time consumption will be even more significant.
Now that you’re clear about mark-clean, mark-copy, and mark-tidy, let’s go back to generational collection.
Across generations reference
We have divided generations according to the survival characteristics of objects to improve the efficiency of garbage collection. However, when recycling the new generation, some old objects may reference the new generation object, so the old age also needs to be used as the root. However, if the whole old age is scanned, the efficiency will be reduced again.
So something called Remembered Set was created to record references across generations without scanning the entire non-collection area.
A memory set is therefore an abstract data structure for recording a collection of Pointers from a non-collection region to a collection region. According to the accuracy of the record
- Word length accuracy, each record accurate to machine word length.
- Object precision. Each record is accurate to the object.
- Card accuracy, each record accurate to a memory area.
The most common implementation of memory sets is card precision, called card tables.
Let me explain what a card is.
Taking object precision as the distance, suppose that the new generation object A is referenced by the old generation object D, then we need to record the address of the old generation object D is referenced by the new generation object.
That card means dividing the memory space into many cards. If the Cenozoic object A is referenced by the Cenozoic object D, it is necessary to record that the memory chip where the Cenozoic object D is located references the Cenozoic object.
That is, the heap is cut by cards. Suppose the card size is 2 and the heap is 20, then the heap can be divided into 10 cards.
Because of the large scope of the card, only one record is required if the object next to D in the same card also references the new generation object.
Card tables are usually implemented with byte arrays, and the range of cards is set to the NTH power of 2. Take a look at the picture below to make it clear.
Assuming the address starts at 0x0000, element 0 of the byte array represents 0x0000 to 0x01FF, element 1 represents 0x0200 to 0x03FF, and so on.
Then, when the new generation is recovered, it only needs to scan the card table and add the memory block where the dirty table is identified as 1 to GC Roots for scanning, so there is no need to scan the whole old age.
The card table occupies less memory, but the relative word length, object accuracy is not accurate, need to scan a piece. So it’s a trade-off, how much card do you want?
There is also a multi-card table, simply said that there are multiple card table, here I draw two cards to show the meaning.
The card table above represents a larger address range, so you can scan the larger table and find a dirty one in the middle, and then subscript it directly to a more specific address range.
This kind of multi-card table can be scanned efficiently when the heap memory is large and cross-generation references are few.
Card tables are generally maintained through write barriers, which are essentially an AOP equivalent to adding code to update card tables when object reference fields are assigned values.
This is actually very easy to understand, in plain terms, when the reference field is assigned to determine the current object is the old age object, the reference object is the new generation object, so the old age object corresponding to the card table position is set to 1, indicating dirty, later need to add root scan.
However, scanning the old age as the root will lead to floating garbage, because the object of the old age may have become garbage, so the object of the new generation scanned by taking the garbage as the root is also likely to be garbage.
But that’s the sacrifice generational collection has to make.
Incremental GC
The so-called incremental GC is actually a piece-by-piece completion GC interspersed with application thread execution, which is clear from the diagram
So it looks like the GC time span is larger, but the mutator pause time is shorter.
For incremental GC, Dijkstra et al. abstracted the three-color labeling algorithm to represent three different conditions of objects in GC.
Three color marking algorithm
White: indicates the object that has not been searched. Gray: Searching for unfinished objects. Black: indicates the object that has been searched.
The following image is from Wikipedia. It’s not the right color, but it means the right color (black is blue, grey is yellow).
Let me summarize the three-color conversion in words.
All objects are white before GC starts, and once GC starts all objects that roots can reach are pushed onto the stack for searching, the color is gray.
The gray object is then removed from the stack to search for the child objects, which are also painted gray and pushed onto the stack. The object is colored black after all of its children are colored gray.
When the GC ends, all the gray objects are gone, leaving the black ones as living objects and the white ones as garbage.
There are three stages of incremental mark-clearing:
- Root lookup, which requires suspending the application thread to find the object referenced directly by the root.
- Phases are marked and executed concurrently with the application thread.
- Cleanup phase.
Here’s what the two nouns in GC mean.
Concurrency: The application thread executes with the GC thread. Parallelism: Multiple GC threads execute together.
Looks like there’s nothing wrong with the tricolor, right? Take a look at the picture below.
The first phase searches for A’s child object B, so A is colored black and B is gray. So we need to search for B.
However, when B started the search, the reference from A was changed by mutator to C, and then the reference from B to C was deleted.
And then we start searching for B, and then B has no reference so the search is over, and then C is garbage, so A is already black, so we can’t search for C anymore.
This is the case of missing marks, the object is still in use as garbage, very serious, this is not allowed by GC, would rather let go, can not kill the wrong.
If A is marked black and the root reference is deleted by the mutator, then A is garbage, but it is marked black and will have to be cleared by the next GC.
This is the result of not pausing the Mutator during markup, but it is also intended to reduce the impact of GC on application execution.
In fact, multiple bids are acceptable. If the mark is missed, it must be dealt with. We can summarize why the missing bid occurs:
- Mutator inserts A reference from black object A to white object C
- Mutator removes a reference from gray object B to white object C
As long as either of these two conditions is broken, there will be no missing mark.
Two conditions can be broken by the following means:
Incremental updates use write barriers to make white objects gray when black references white objects.
Write barriers are used to remove references to white objects, and white objects are set to grey, in effect preserving the old reference relationship. It’s called snapshot-at-the-beginning.
conclusion
The key points and ideas for garbage collection are pretty much there, and I’ll look at garbage collectors for the JVM in the next article.
Now let’s sum it up again.
Garbage collection is about finding garbage first, and finding garbage is divided into two schools, one is reference counting, one is reachability analysis.
Reference counting garbage collection is timely and memory friendly, but circular references cannot be handled.
Reachability analysis is basically the core choice of modern garbage collection, but due to the need for unified collection is time-consuming, it is easy to affect the normal operation of the application.
Therefore, the research direction of reachability analysis is how to reduce the impact on application operation, namely reduce STW(stop the world) time.
Therefore, according to the object generation hypothesis, the generation collection is studied. According to the characteristics of objects, the new generation and the old age are divided, and different collection algorithms are adopted to improve the efficiency of recycling.
Find ways to break up the GC steps so that it can be concurrent with the application thread, and take parallel collections to speed up the collection.
There is also deferred collection in the direction of assessment or recycling part of the garbage to reduce STW time.
In general, garbage recycling is very complicated, because there are many details, this article is a shallow armchair, don’t be fooled by my title hahaha.
The last
This article has been written for a long time, mainly because the content is a lot of how to arrange a little difficult, I also cut a lot of selective, but still nearly 1W words.
I have also consulted a lot of information during this period, but my personal ability is limited. If there is any mistake, please contact me immediately.
Shoulders of giants
Arctrix.com/nas/python/…
Openjdk.java.net/groups/hots…
The Garbage Collection Handbook
www.iteye.com/blog/user/r… Big blog
www.jianshu.com/u/90ab66c24… Wolf’s blog
Wechat search [yes training guide], pay attention to yes, from a little bit to a billion little bit, we will see you next.