Mark-Clear (Mark and Sweep) is the most classic garbage collection algorithm. When the theory is used in production practice, there are many places that need to be optimized to adapt to the specific environment. Here's a simple example, let's take a step-by-step note to see how we can ensure that the JVM allocates objects safely and consistently.
You should have read the previous chapter: 1. Introduction to garbage Collection-GC reference manual
Defragmentation (fragmenting and compacting)
Each time the purge (sweeping) is performed, the JVM must ensure that the memory occupied by the unreachable object can be reused for recycling. However, this (and eventually) may result in memory fragmentation (similar to disk fragmentation), which can cause two problems:
Write operations are becoming more time-consuming because finding a large enough free memory can become cumbersome.
When a new object is created, the JVM allocates memory in contiguous blocks. If the fragmentation problem is serious, a memory allocation error (allocation error) occurs until there is no free fragment to hold the newly created object.
To avoid such problems, the JVM must ensure that the fragmentation problem is not out of control. Therefore, in the garbage collection process, it is not only marking and purging, but also the "memory defragmentation" process. This process allows all the available objects (reachable objects) to be arranged sequentially to eliminate (or reduce) fragmentation. As shown below:
Description
A reference in the JVM is an abstract concept, and if the GC moves an object, it modifies all references to that object (in the stack and in the heap).
Move/lift/compress is a STW process, so modifying an object reference is a safe behavior.
Generational hypothesis (generational hypothesis)
As we mentioned earlier, performing garbage collection requires stopping the entire application. Obviously, the more objects you have, the longer it takes to collect all the garbage. But is it possible to handle only a small area of memory? To explore this possibility, the researchers found that most of the recoverable memory in a program can be categorized into two categories:
Most objects soon cease to be used
A part of it will not be immediately useless, but it will not last (too) long
These observations form a weak-generation hypothesis (Weak generational hypothesis). Based on this assumption, the memory in the VM is divided into the younger generation (young Generation) and the older generation (old Generation). The old age is sometimes called the old Age Zone (tenured).
Split into such two separate areas that can be cleaned, allowing different algorithms to dramatically improve GC performance.
This approach is not without problem. For example, objects in different generations might refer to each other and become "de facto" GC root when a generational collection is collected.
Of course, it should be emphasized that the generational hypothesis does not apply to all programs. Because the GC algorithm is optimized for objects such as "Die Fast" or "otherwise live long", the JVM is embarrassed to collect objects that have survived for a long time.
Memory Pool (pools)
Memory pool Partitioning in heap memory is similar. What's not easy to understand is how garbage collection works in each memory pool. Note that different GC algorithms may differ in implementation details, but are consistent with the concepts described in this chapter.
New Generation (Eden, Eden)
Eden is an area in memory that is used to allocate newly created objects. Typically, multiple threads create multiple objects at the same time, so the Eden zone is divided into multiple threads that are allocated locally (thread local Allocation buffer, or Tlab). With this buffer partitioning, most objects are allocated directly by the JVM in the Tlab of the corresponding thread, avoiding synchronization with other threads.
If there is not enough memory space in the Tlab, it is allocated in the shared Eden space. If the shared Eden area does not have enough space, it triggers a young generation of GC to free up memory space. If the Eden area still does not have enough free memory area after the GC, the object is assigned to the old Generation.
When the Eden zone is garbage collected, the GC passes all the objects that are rooted to the root and marks them as surviving objects.
We have pointed out that there may be cross-generational references between objects, so you need a way to mark all references to Eden from other generations. In doing so, you will encounter a reference between generations again and again. The JVM implements some of the trick: the card tag (card-marking). Essentially, the JVM only needs to remember the rough location of the "dirty" objects in the Eden area, and there may be references to this section of the old age object reference. For more details, please refer to: Nitsan's blog.
Once the tagging phase is complete, all surviving objects in Eden will be copied into the survival zone (Survivor spaces). The entire Eden area can be considered empty, and then it can be used to assign new objects. This method is called "tag-copy": The surviving object is tagged and then copied to a survival area (note that it is copied, not moved).
Survival Zone (Survivor Spaces)
Next to the Eden area are two surviving areas, called from 空间
and to 空间
. It should be emphasized that at any one time there is always a survival area that is empty.
The empty survival zone is used to store the collected objects during the next young generation of GC. All surviving objects in the young generation (including the Edenq area and the non-empty "from" survival zone) are copied to the "to" survival area. When the GC process is complete, there are objects in the "to" area, and there are no objects in the ' from ' area. The roles of both are switched exactly.
The surviving objects are replicated multiple times between two surviving areas until certain objects survive for a certain threshold. Generational theory assumes that objects that survive longer than a certain period of time are likely to continue to survive for longer periods of time.
This kind of "old" objects are therefore promoted (promoted) to the old age. At the time of Ascension, the object of the surviving area is no longer copied to another surviving area, but is migrated to the old age and resides in the old age until it becomes unreachable.
In order to determine whether an object is "old enough" and can be promoted (Promotion) to the old age, the GC module tracks the number of times each surviving area object has survived. The age of the surviving objects increases after each generational GC is completed. When the age exceeds the elevation threshold (tenuring threshold), it is promoted to the old age region.
The specific elevation threshold is dynamically adjusted by the JVM, but you can also -XX:+MaxTenuringThreshold
specify the upper limit with parameters. If set -XX:+MaxTenuringThreshold=0
, the surviving objects are not copied between the surviving areas when the GC is promoted to the old age. This threshold is set to 15 GC cycles by default in a modern JVM. This is also the maximum value in the hotspot.
Ascension (Promotion) may also occur earlier if there is not enough space in the surviving area to hold the surviving objects in the young generation.
Old Age (Generation)
The GC implementations of the old age are much more complex. The memory space in the old age is usually larger, and the object is less likely to be garbage.
The frequency of GC occurred in the old age is much smaller than that of the younger generation. At the same time, the mark and copy algorithm is no longer used because most of the objects in the old age are expected to survive. Instead, a moving object is used to minimize memory fragmentation. The old-age space cleanup algorithm is usually built on a different basis. In principle, the following steps are performed:
Flags all objects that can be reached by GC roots through the flag bit (marked bit).
Delete all Unreachable objects
Organize the contents of the old age space by copying all the surviving objects, from the place where the old age space began, and storing them in turn.
As described above, the GC must be explicitly collated in the old age to avoid excessive memory fragmentation.
Permanent generation (PermGen)
Prior to Java 8 There was a special space called "Permanent generation" (Permanent Generation). This is where meta data (metadata) is stored, such as class information. In addition, this area also holds other data and information, including the internalized strings, and so on. This actually creates a lot of trouble for Java developers, because it's hard to figure out how much memory this area needs to occupy. The result of the prediction failure is that java.lang.OutOfMemoryError: Permgen space
this form of error occurs. Unless OutOfMemoryError It is true that a memory leak is the result, otherwise it can only increase the size of the PermGen, for example, the following example, is set PermGen maximum space is:
java -XX:MaxPermSize=256m com.mycompany.MyApplication
Meta Data area (METASPACE)
Since the space required for estimating metadata is so complex, Java 8 removes the permanent generation (Permanent Generation) directly and instead metaspace. Since then, many assorted in Java have been placed in normal heap memory.
Of course, information such as class definitions is loaded into metaspace. The metadata is located in local memory (native memories) and no longer affects normal Java objects. By default, the size of Metaspace is limited only by the local memory available to the Java process. This process is no longer caused by the extra load of several classes of/jar packets java.lang.OutOfMemoryError: Permgen space.
. Note that this unrestricted space is not without cost-if metaspace out of control, it can cause a very serious memory exchange (swapping), or cause local memory allocations to fail.
If you need to avoid this worst case scenario, you can limit the size of the metaspace in the following way, such as:
java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication
Minor GC vs Major GC vs full GC
Garbage collection events (garbage Collection events) are typically divided into: small GC (Minor GC)-Large GC (Major GC)-and full GC. This section describes these events and their differences. Then you will find that these distinctions are not particularly clear.
Most importantly, the application meets the service level agreement (Agreement, SLA) and monitors the response latency and throughput through the monitoring program. Only then can you see the results associated with GC events. It is important that these events stop the entire program and how long it lasts.
Although the terms Minor, Major and full GC are widely used, but there is no standard definition, let's take a closer look at the specifics.
Small GC (Minor GC)
Garbage collection events for young generations of memory are called small GC. This definition is both clear and widely shared. For small GC events, there are some interesting things you should know about:
- The Minor GC is always triggered when the JVM is unable to allocate memory space for new objects, such as when the Eden zone is full. Therefore, the higher the (new object) allocation frequency, the higher the frequency of the Minor GC.
- The Minor GC event actually ignores the old age. References to young generations from the old age are considered to be GC Root. References from the younger generation to the old age are all ignored in the mark phase.
- Contrary to general knowledge, the Minor GC causes a full-line pause (Stop-the-world) every time, suspending all application threads. For most programs, the duration of the pause is largely negligible, because the objects in the Eden area are basically garbage and are not copied to the surviving/old age. If this is not the case, most newly created objects cannot be cleaned up by garbage collection, and the Minor GC pauses for a longer period of time.
So the definition of Minor GC is simple--minor the GC cleans up the young generation.
Major GC vs Full GC
It is worth mentioning that these terms are not formally defined-whether in the JVM specification or in GC-related papers.
We know that the Minor GC cleans up the young space, and the other definitions are simple:
- The Major GC (large GC) cleans up the old space.
- Full GC (fully GC) cleans up the entire heap, including the young generation and the old age space.
The cup is more complicated than the situation arises. Many Major GC are triggered by the Minor GC, so in many cases the two are inseparable. On the other hand, garbage collection algorithms like G1 perform partial-area garbage collections, so the term "cleaning" is not very accurate.
It also makes us realize that we should not worry about whether it's called Major GC or full GC, and we should be concerned: if a GC event stops all threads or executes concurrently with other threads.
These confusions are even rooted in standard JVM tools. My meaning can be explained by an example. Let's compare the GC information output of the two tools in the same JVM. This JVM uses the concurrency token and purge collector (Concurrent mark and Sweep collector -XX:+UseConcMarkSweepGC
).
First we look jstat
at the output:
jstat -gc -t 4235 1s
Time s0c s1c s0u s1u EC EU OC OU MC MU ccsc CCSU ygc YGCT FGC fgct GCT 5.7 34048.0 34048.0 0.0 34048.0 272640.0 194699.7 1756416.0 181419.9 18304.0 17865.1 2688.0 2497.6 3 0.275 0 0.000 0.275 6.7 34048.0 34048.0 34048.0 0.0 272640.0 247555.4 1756416.0 263447.9 18816.0 18123.3 2688.0 2523.1 4 0.359 0 0.000 0.359 7.7 34048.0 34048.0 0.0 34048.0 272640.0 257729. 3 1756416.0 345109.8 19072.0 18396.6 2688.0 2550.3 5 0.451 0 0.000 0.451 8.7 34048.0 34048.0 34048.0 34048.0 272640.0 272640.0 1756416.0 444982.5 19456.0 18681.3 2816.0 2575.8 7 0.550 0 0.000 0.550 9.7 3 4048.0 34048.0 34046.7 0.0 272640.0 16777.0 1756416.0 587906.3 20096.0 19235.1 2944.0 2631.8 8 0.720 0 0.000 0.72010.7 34048.0 34048.0 0.0 34046.2 272640.0 80171.6 1756416.0 664913.4 20352.0 19495.9 2944.0 2657.4 9 0.810 0 0.000 0.81011.7 34048.0 34048.0 34048.0 0.0 272640.0 129480.8 1756416.0 745100.2 20608.0 19704.5 2944.0 2678.4 10 0.896 0 0.000 0.89612.7 34048.0 34048.0 0.0 34046.6 272640.0 164070.7 1756416.0 822073.7 20992.0 19937.1 3072.0 2702.8 11 0.978 0 0.000 0.97813.7 34048.0 34048.0 34048.0 0.0 272640.0 211949.9 175641 6.0 897364.4 21248.0 20179.6 3072.0 2728.1 12 1.087 1 0.004 1.09114.7 34048.0 34048.0 0.0 34047.1 2 72640.0 245801.5 1756416.0 597362.6 21504.0 20390.6 3072.0 2750.3 13 1.183 2 0.050 1.23315.7 34048.0 3 4048.0 0.0 34048.0 272640.0 21474.1 1756416.0 757347.0 22012.0 20792.0 3200.0 2791.0 15 1.336 2 0.050 1.38616.7 34048.0 34048.0 34047.0 0.0 272640.0 48378.0 1756416.0 838594.4 22268.0 21003.5 3200.0 2813.2 16 1.433 2 0.050 1.484
This fragment is intercepted from the first 17 seconds after the JVM starts. Based on this information, it is possible to know that 2 full GC triggers execution after 12 minor GC (YGC), which takes a total of 50ms of time. Of course, the same information can be obtained through tools with graphical interfaces, such as Jconsole or JVISUALVM (or the latest JMC).
Before jumping to conclusions, let's look at the GC logs for this JVM process. Obviously, configuration -XX:+PrintGCDetails
parameters are required, the contents of the GC log are more detailed, and the results are somewhat different:
java -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC eu.plumbr.demo.GarbageProducer
3.157: [GC (Allocation Failure) 3.157: [parnew:272640k->34048k (306688K), 0.0844702 secs] 272640k->69574k ( 2063104K), 0.0845560 secs] [times:user=0.23 sys=0.03, real=0.09 secs] 4.092: [GC (Allocation Failure) 4.092: [parnew:306 688k->34048k (306688K), 0.1013723 secs] 342214k->136584k (2063104K), 0.1014307 secs] [times:user=0.25 sys=0.05, real=0.10 secs] ... cut for brevity ... 11.292: [GC (Allocation Failure) 11.292: [parnew:306686k->34048k (306688K), 0.0857219 secs] 971599k->779148k ( 2063104K), 0.0857875 secs] [times:user=0.26 sys=0.04, real=0.09 secs] 12.140: [GC (Allocation Failure) 12.140: [Parnew:3 06688k->34046k (306688K), 0.0821774 secs] 1051788k->856120k (2063104K), 0.0822400 secs] [times:user=0.25 sys=0.03 , real=0.08 secs] 12.989: [GC (Allocation Failure) 12.989: [parnew:306686k->34048k (306688K), 0.1086667 secs] 1128760k- >931412k (2063104K), 0.1087416 secs] [times:user=0.24 sys=0.04, real=0.11 secs] 13.098: [GC (CMS Initial Mark) [1 cms-i Nitial-mark: 897364K (1756416K)] 936667K (2063104K), 0.0041705 secs] [times:user=0.02 sys=0.00, real=0.00 secs] 13.102: [Cms-concurre nt-mark-start]13.341: [cms-concurrent-mark:0.238/0.238 secs] [times:user=0.36 sys=0.01, real=0.24 secs] 13.341: [ CMS-CONCURRENT-PRECLEAN-START]13.350: [cms-concurrent-preclean:0.009/0.009 secs] [times:user=0.03 sys=0.00, real= 0.01 secs] 13.350: [cms-concurrent-abortable-preclean-start]13.878: [GC (Allocation Failure) 13.878: [parnew:306688k-& gt;34047k (306688K), 0.0960456 secs] 1204052k->1010638k (2063104K), 0.0961542 secs] [times:user=0.29 sys=0.04, real= 0.09 secs] 14.366: [cms-concurrent-abortable-preclean:0.917/1.016 secs] [times:user=2.22 sys=0.07, real=1.01 secs] 14.3 : [GC (CMS Final Remark) [YG occupancy:182593 K (306688 k)]14.366: [Rescan (parallel), 0.0291598 secs]14.395: [Weak re FS processing, 0.0000232 secs]14.395: [Class unloading, 0.0117661 secs]14.407: [Scrub symbol table, 0.0015323 secs]14.409: [Scrub string table, 0.0003221 secs] [1 Cms-remark:976591k (1756416K)] 1159184K (2063104K), 0.0462010 secs] [times:user=0.14 sys=0.00, real=0.05 secs] 14.412: [Cms-conc urrent-sweep-start]14.633: [cms-concurrent-sweep:0.221/0.221 secs] [times:user=0.37 sys=0.00, real=0.22 secs] 14.633: [cms-concurrent-reset-start]14.636: [cms-concurrent-reset:0.002/0.002 secs] [times:user=0.00 sys=0.00, real=0.00 secs
As you can see from the GC log, some "different things" have happened after 12 Minor GC. Instead of two full GC, it performed a GC in the old age, divided into several phases:
- The initial marking phase (Initial Mark Phase) takes 0.0041705 seconds (approximately 4ms). This phase is a full-line pause (STW) event, suspending all application threads so that the initial token can be executed.
- Mark and Pre-clean phase (Markup and Preclean phase). Execute concurrently with the application thread.
- The final marking phase (final Remark phase) takes 0.0462010 seconds (approximately 46ms). This phase is also a full-line pause (STW) event.
- The purge operation (Sweep) is executed concurrently and does not need to pause the application thread.
So from the actual GC log you can see that not two full GC operations were performed, but only one Major GC that cleans up the old-time space.
If you only care about the delay, you jstat
can get the correct result by the data shown later. It correctly lists two STW events, which take up to a total of five Ms. This period of time affects the latency of all application threads. If you want to optimize throughput, this result will be misleading--jstat only the initial and final marker stages of Stop-the-world are listed, and the output of jstat completely hides the GC phase of concurrent execution.
2. Garbage collection in Java-GC reference manual