Study Notes: Java Virtual Machine - JVM memory structure, garbage collection, class loading and bytecode technology

Learning video source: https://www.bilibili.com/video/BV1yE411Z7AP
Detailed explanation of Java class loading mechanism and ClassLoader Recommended article: https://yichun.blog.csdn.net/article/details/102983363

Study notes - JVM


Preface

Learning video address

Insert image description here


1. What is JVM?

Definition: Java Virtual Machine - Java virtual machine (the running environment of Java programs)

advantage:

  1. Compile once, run anywhere
  2. Automatic memory management, garbage collection function
  3. Array subscript out-of-bounds check
  4. Polymorphism

Compare JDK, JRE, JVM:
Insert image description here


2. JVM memory structure

1. Program Counter Register

Program counter (register, Program Counter Register)

Function: Record the execution address of the next JVM instruction.
Features: 1. Thread private; 2. No memory overflow will occur.

In the figure below, the left side is the binary bytecode and JVM instructions; the right side is the Java source code
Insert image description here

2. Stack

Java Virtual Machine Stacks

The memory required by each thread when running corresponds to the Java virtual machine stack. Each stack is composed of multiple stack frames (Frame), corresponding to the memory occupied by each method call. There can only be one activity in each stack. The stack frame corresponds to the currently executing method. It is thread private.

Q&A

  1. Does garbage collection involve stack memory?
    The stack frame memory will be released when the stack frame is popped off the stack, and garbage collection is not required; garbage collection is only responsible for recycling objects in the heap.
  2. Is larger stack memory allocation better?
    The larger the stack memory allocated, the more recursive calls can be made, but this may affect the number of threads running simultaneously. If there is 500MB of memory, and each stack memory is allocated 1MB, the ideal number of runnable threads is 500; each stack memory is 2MB, and the ideal number of runnable threads is 250. Generally, the default stack memory of Linux system is 1024KB.
  3. Are local variables within methods thread-safe?
    Local variables are private to each thread rather than shared. If the variable does not escape the scope of the method, there will be no thread safety issues; otherwise, there may be thread safety issues due to variable sharing.
  4. Causes of stack memory overflow (java.lang.StackOverflowError)
    : 1. Too many stack frames; 2. Too large a stack frame

Native Method Stacks

The native method stack implementation is similar to the Java virtual machine stack, but it serves native methods. The virtual machine specification does not mandate the language, usage, and data structure of methods in the local method stack, so specific virtual machines can implement it freely. Some virtual machines (such as the Sun HotSpot virtual machine) even directly combine the local method stack and the virtual machine stack into one. It is also thread private.

Q&A

  1. How to locate threads and codes with excessive CPU usage?
    First use topthe command to locate which process is occupying too much CPU, and then use ps H -eo pid,tid,%cpu | grep $pidthe command to locate which thread is occupying the CPU too high. Finally, convert the tid of the thread into hexadecimal to get its nid. Use the jstack $pidcommand to find the thread and locate the specific execution. classes and lines of code. At this point, you can see where the code has problems.

  2. The program runs for a long time without results.
    It is suspected that a deadlock has caused this problem. You can use jstack $pidthe command to analyze (or jconsole)


3. Heap

Heap

Objects created through the new keyword will be in the heap and use heap memory.
Features: 1. Thread sharing; 2. There is a garbage collection mechanism.

Heap memory overflow (java.lang.OutOfMemoryError: Java Heap Space)
The total number of used objects in the heap occupies memory is greater than the maximum heap memory limit.
Heap memory diagnostic tools and commands:

  • The jps tool
    checks which Java processes are in the current system.jps
  • The jmap tool
    checks the heap memory usage at this time.jmap -heap $pid
  • jconsole tool is
    a multifunctional graphical interface tool that dynamically monitors heap memory usage.jconsole
  • jvirsualvm tool
    is a super multifunctional graphical interface tool that dynamically monitors heap memory usage.jvirsualvm

The following code cases can be used as monitoring objects:

public static void main(String[] args) throws InterruptedException {
    
    
	System.out.println("step 1");
	Thread.sleep(30000);
	byte[] megaArray = new byte[10 * 1024 * 1024]; // 10MB
	
	System.out.println("step 2");
	Thread.sleep(20000);
	array = null;
	System.gc();
	
	System.out.println("step 3");
	Thread.sleep(1000000L);
}

4. Method Area

Method Area

The method area stores information related to the class structure, class member variables, method data, member methods, and constructor method code parts. Although it is logically part of the heap, this is not necessarily the case in each JVM's implementation. The method area is shared by threads (the method area is the specification of the permanent generation and metaspace, the permanent generation is the implementation of Hotspot before 1.8, and the metaspace belongs to the implementation of 1.8 and later)

Differences between JDK1.6, 1.7 and 1.8 JMM

JDK 1.6: Program counter, Java virtual machine stack, local method stack, heap, method area [permanent generation] (string constant pool, static variables, runtime constant pool, class constant pool) JDK 1.7: Program counter, Java virtual
machine Stack, local method stack, heap (string constants, static variables), method area [permanent generation] (runtime constant pool, class constant pool) JDK 1.8: program counter, Java virtual machine stack, local method stack, heap (
character String constants), metadata (static variables, runtime constant pool, class constant pool)
Insert image description here

Method area memory overflow

  • There was a permanent generation memory overflow before 1.8: java.lang.OutOfMemoryError: PermGen Space
    -XX:MaxPermSize=8m

  • There is a metaspace memory overflow in 1.8 and later: java.lang.OutOfMemoryError: Metaspace
    -XX:MaxMetaspaceSize=8m

Code example:

/**
 * 由于方法区在1.8后的元空间实现使用的是系统内存,
 * 而我的PC使用的是16G内存,很难触发该问题,所以需添加如下参数,以指定最大元空间内存
 * -XX:MaxMetaspaceSize=8m(如果是1.8之前,则需指定-XX:MaxPermSize=8m)
 */
public class Demo extends Classloader {
    
    
	public static void main(String[] args) {
    
    
		int n = 0;
		try {
    
    
			Demo demo = new Demo();
			for (int i = 0; i < 10000; i ++, n ++) {
    
    
				// ClassWriter可用作生成类的二进制字节码
				ClassWriter cw = new ClassWriter(0);
				// 版本号,访问权限,类名,包名,父类,接口
				cw.visit(Opcodes.V1_8, Opcodes.ACC_PLUBLIC, "Class" + i, null, "java/lang/Object", null);
				// 返回byte数组
				byte[] code = cw.toByteArray();
				// 执行加载类
				demo.defineClass("Class" + i, code, 0, code.length);
			}
		} finally {
    
    
			System.out.println(n);
		}
	}
}

5. Others

5.1 Constant pool

The constant pool can be understood as a table. The virtual machine instructions use this table to find the class name, method name, parameter type, literal and other information to be executed.

5.2 Runtime constant pool

When a class is loaded, its constant pool information is put into the runtime constant pool and its symbolic addresses are changed to real addresses.

Java program:

public class StringTableTest {
    
    
    public static void main(String[] args) {
    
    
        String a = "a";
        String b = "b";
        String ab = "ab";
    }
}

Decompile into bytecode file:

ziang.zhang@ziangzhangdeMacBook-Pro test % javap -v StringTableTest.class
Classfile /Users/ziang.zhang/dreamPointer/JavaSpace/JavaProjects/data-structures-and-algorithms/out/production/leetcode/test/StringTableTest.class
  Last modified 2022-10-3; size 496 bytes
  MD5 checksum 92426c26340f906a2a36f096f6c1fd33
  Compiled from "StringTableTest.java"
public class test.StringTableTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = String             #18            // a
   #3 = String             #20            // b
   #4 = String             #21            // ab
   #5 = Class              #25            // test/StringTableTest
   #6 = Class              #26            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Ltest/StringTableTest;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               a
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               b
  #21 = Utf8               ab
  #22 = Utf8               SourceFile
  #23 = Utf8               StringTableTest.java
  #24 = NameAndType        #7:#8          // "<init>":()V
  #25 = Utf8               test/StringTableTest
  #26 = Utf8               java/lang/Object
{
    
    
  public test.StringTableTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ltest/StringTableTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=4, args_size=1
         0: ldc           #2                  // String a
         2: astore_1
         3: ldc           #3                  // String b
         5: astore_2
         6: ldc           #4                  // String ab
         8: astore_3
         9: return
      LineNumberTable:
        line 6: 0
        line 7: 3
        line 8: 6
        line 10: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  args   [Ljava/lang/String;
            3       7     1     a   Ljava/lang/String;
            6       4     2     b   Ljava/lang/String;
            9       1     3    ab   Ljava/lang/String;
}
SourceFile: "StringTableTest.java"

When the program is running, the information in the constant pool will be loaded into the runtime constant pool . At this time, a, b, and ab exist in the constant pool as symbols. The bytecode instructions ldc # 2check whether the string constant pool contains a string object of the symbol, and if so, use it. If not, create a string object and put it in. Visible string creation is lazy

5.3 String constant pool (StringTable)

characteristic

  • Strings in the constant pool are only symbols and become objects when they are used for the first time
  • Use the string constant pool mechanism to avoid repeated creation of string objects
  • String variable splicing uses StringBuilder (1.8)
  • String constant splicing is based on compile-time optimization
  • You can use the intern() method to proactively put string objects that are not in the string constant pool into it.
    1.8: If there is no string object, put it in. If there is, then do not put it in. The return value is the object in the pool.
    1.6: If there is no string object, copy it. If there are any, they will not be put in. The return value is the object in the pool.

Location

JDK1.6: located in the constant pool, and the constant pool is located in the method area (permanent generation)
JDK1.8: located in the heap
Insert image description here

StringTable garbage collection

Case:

// 运行前添加JVM参数:-Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
    
    
	int i = 0;
	try {
    
    
		for (int j = 0; j < 10000; j ++) {
    
    
			String.valueOf(j).intern();
			i ++;
		}
	} catch (Exeception e) {
    
    
		e.printStackTrace();
	} finally {
    
    
		System.out.println(i);
	}
}

StringTable performance tuning

  • Adjust -XX:StringTableSize=Number of buckets
    StringTable is essentially a hash table. When there are more buckets, the probability of hash collision is smaller and the search speed is faster. You can use this parameter to adjust the number of buckets in the table.
  • Consider whether to put the string into the pool
    . When there are many duplicate strings, appropriate use of intern() can prevent duplicate string objects from being created, thereby reducing memory usage.

Q&A

  1. Determine the results of running the following code
    public static void main(String[] args) {
          
          
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString();
        String s5 = "a" + "b"; // javac在编译期的优化,在编译期就可以确定其值为"ab"
    
    	System.out.println(s3 == s4 + ", " + s3 == s5);
    }
    
    false, true
    because the creation of string s4 is based on StringBuilder as the underlying principle; the creation of string s5 is based on compile-time optimization. Since the string object "ab" has been placed in the string constant pool when s3 is created, s5 The referenced object is also "ab" located in the string constant pool.
  2. Determine the results of running the following code
    public static void main(String[] args) {
          
          
        // 动态拼接的字符串仅存在于堆中,字符串常量池中并不存在
        String s1 = new String("a") + new String("b");
        String s2 = "ab";
        // 尝试将此字符串对象放入字符串常量池,若已存在则不放入,不存在则放入。返回池中的对象
        String s3 = s1.intern();
        
        System.out.println(s1 == "ab" + ", " + s1 == s3 + ", " + s2 == s3);
    }
    
    false, false, true
    because s1 is dynamically spliced ​​from objects, and the final generated "ab" exists in the heap, but does not exist in the string constant pool; s2 is a string object created by the runtime and exists in the pool. s3 is taken from the string constant pool and is the same as the s2 object

5.4 Direct Memory (DirectMemory)

BIO: The Java program itself does not directly read the contents of the disk, but relies on the API of the operating system to execute it. At this time, the CPU switches from user mode to kernel mode, and a system buffer will be opened in the memory to buffer the disk file. The Java program uses the byte array to create the buffer again to read data in the system buffer.
Insert image description here

NIO: Create a direct memory for communication between Java heap memory and system memory. Data in disk files can be directly read from this direct memory, thus greatly improving efficiency.
Insert image description here

Features:

  • Commonly used in NIO operations and used for data buffers
  • Allocation and recovery costs are higher, but read and write performance is high
  • It is not managed by JVM memory recycling, and its release borrows the virtual reference mechanism.

Distribution and recycling principles

  • Use Unsafe objects to complete direct memory allocation and recycling. When recycling, you need to actively call the freeMemory method in the Unsafe class.
  • The implementation class of ByteBuffer uses Cleaner (virtual reference) to monitor the ByteBuffer object. Once the ByteBuffer object is garbage collected, the ReferenceHandler thread will call freeMemory through the Cleaner's clean method to release the direct memory.

OutOfMemoryError: Direct buffer memory error occurs when memory overflows


3. Garbage collection

1. How to determine if an object can be recycled

1.1 Reference counting method

The reference counting method is to add a reference counter to the object, and then use an additional memory area to store the number of times each object is referenced. When the object is referenced from a place, our reference count for the object will be increased by 1. , on the contrary, every time a reference fails, our reference count for the object will be reduced by 1. When the number of references to the object is 0, then we can think that the object will not be used again. In this way, we These recyclable objects can be quickly and intuitively located for cleaning.

Imperfections of reference counting

  1. Unable to solve the problem of circular references.
    Although the reference counting method is very intuitive and efficient, it is impossible to scan a "recyclable" object in a special case through the reference counting method. This special case is when an object has a circular reference, such as object A. B is referenced, and the B object refers to A. In addition, the two of them are not referenced by any other object. In fact, this part of the object is also a "recyclable" object, but it cannot be located through the reference counting method.
    Insert image description here

  2. Another aspect is that the reference counting method requires additional space to record the number of times each object is referenced, and this reference number also requires additional maintenance.

1.2 Reachability analysis

The reachability analysis method takes all "GC Roots" objects as the starting point. If the objects cannot be traced through the references of GC Roots, then we believe that these objects will not be used again. Now the mainstream programming languages ​​​​are Determine whether the object is alive through reachability analysis.

Insert image description here
Which objects do we call "GC Roots" objects? Of course, ordinary objects will definitely not work. If it is to be used as a GC Roots object, then it must meet a condition, that is, it must itself be used for a long period of time. It will not be recycled by GC. Then only objects that meet this condition can be used as GC Roots. The types of GC Roots are roughly as follows:

  1. Object referenced by local variables in the virtual machine stack.

  2. The object referenced by the static properties in the method area.

  3. The object referenced by the constant in the method area.

  4. The object referenced in the native method (Native method).

  5. Reference objects inside the virtual machine (class recorders, Class objects corresponding to basic data, exception objects).

  6. All objects held by synchronized locks (Synchronnized).

  7. Objects describing the internal conditions of the virtual machine (such as JMXBean, callbacks registered in JVMTI, local cache code).

  8. The object referenced by the garbage collector

1.3 Four kinds of references

  1. strong reference
    • Only when GC Roots objects do not reference the object through [strong references] can the object be garbage collected.
  2. SoftReference
    • When only soft references refer to the object, after garbage collection, if the memory is still insufficient, garbage collection will be triggered again to recycle the soft reference object.
    • You can use the reference queue to release the soft reference itself
  3. WeakReference
    • When only weak references refer to the object, during garbage collection, the weak reference objects will be recycled regardless of whether the memory is sufficient.
    • Can be used with reference queues to release weak references themselves
  4. PhantomReference
    • It must be used with the reference queue, mainly with the ByteBuffer. When the referenced object is recycled, the virtual reference will be enqueued, and the Reference Handler thread will call the virtual reference related method to release the direct memory.
  5. FinalReference
    • No manual coding is required, but it is used internally with the reference queue. During garbage collection, the finalizer reference is enqueued (the referenced object is not yet recycled), and then the Finalizer thread finds the referenced object by referencing the finalizer reference and calls its finalize method, the referenced object can be recycled only during the second GC.

Soft reference example

/**
 * 演示软引用,配合引用队列
 * -Xmx20M -XX:+PrintGCDetails -verbose:gc
 */
public static void main(String[] args) {
    
    
	List<SoftReference<byte[]>> list = new ArrayList<>();

	// 引用队列
	ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

	for (int i = 0; i < 5; i ++) {
    
    
		// 关联引用队列:当软引用所引用的byte[]被回收后,该软引用会自动进入queue
		SoftReference<byte[]> softRef = new SoftReference<>(new byte[4 * 1024 * 1024], queue);
		System.out.println(softRef.get());
		list.add(softRef);
	}

	// 在queue中poll出无用的软引用对象,并在list中remove掉
	Reference<? extends byte[]> poll = queue.poll();
	while (poll != null) {
    
    
		list.remove(poll);
		poll = queue.poll();
	}

	for (SoftReference<byte[]> softRef : list) {
    
    
		System.out.println(softRef.get());
	}
}

Weak reference example

/**
 * 演示弱引用
 * -Xmx20M -XX:+PrintGCDetails -verbose:gc
 */
public static void main(String[] args) {
    
    
	List<WeakReference<byte[]>> list = new ArrayList<>();

	for (int i = 0; i < 5; i ++) {
    
    
		WeakReference<byte[]> weakRef = new WeakReference<>(new byte[4 * 1024 * 1024]);
		list.add(weakRef);
		// 输出每次add后的list内容
		for (WeakReference<byte[]> one : list) {
    
    
			System.out.println(one.get() + " ");
		}
		System.out.println();
	}

	System.out.println("running over, list size: " + list.size());
}

Output result:

[B@7ea987ac 
[B@7ea987ac [B@12a3a380 
[B@7ea987ac [B@12a3a380 [B@29453f44 
[B@7ea987ac [B@12a3a380 [B@29453f44 [B@5cad8086 
[GC (Allocation Failure) --[PSYoungGen: 5632K->5632K(6144K)] 17920K->17957K(19968K), 0.0033787 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 5632K->0K(6144K)] [ParOldGen: 12325K->393K(8704K)] 17957K->393K(14848K), [Metaspace: 3153K->3153K(1056768K)], 0.0027499 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
null null null null [B@6e0be858 
null null null null [B@6e0be858 [B@61bbe9ba 
null null null null [B@6e0be858 [B@61bbe9ba [B@610455d6 
null null null null [B@6e0be858 [B@61bbe9ba [B@610455d6 [B@511d50c0 
[GC (Allocation Failure) --[PSYoungGen: 4380K->4380K(6144K)] 17061K->17061K(19968K), 0.0004614 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 4380K->0K(6144K)] [ParOldGen: 12681K->374K(13824K)] 17061K->374K(19968K), [Metaspace: 3158K->3158K(1056768K)], 0.0028986 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
null null null null null null null null [B@60e53b93 
null null null null null null null null [B@60e53b93 [B@5e2de80c 
running over, list size: 10
Heap
 PSYoungGen      total 6144K, used 4378K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 77% used [0x00000007bf980000,0x00000007bfdc6828,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 13824K, used 4470K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 32% used [0x00000007bec00000,0x00000007bf05da48,0x00000007bf980000)
 Metaspace       used 3169K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K

2. Garbage collection algorithm

When we use reachability analysis to determine whether an object is alive, we need to use some strategy to clean up these dead objects and then sort out the surviving objects. This process involves three algorithms, namely Mark clearing method, mark copying method, mark sorting method.

2.1 Mark and clear method

The mark and clear method is to first find the surviving objects in the memory and mark them, and then clean up the unmarked objects uniformly. The process is roughly as follows. Advantages of
Insert image description here
mark and clear : The mark and clear method is characterized by its simplicity, directness, and very fast speed. It is suitable for scenarios where there are many surviving objects and few objects that need to be recycled.

Insufficient mark clearing :

  1. Will cause discontinuous memory space : Just like the cleared memory area in the picture above, there will be a lot of discontinuous space in the memory after clearing. This is what we often call space fragmentation. Too many such space fragments are not only detrimental to us. The next allocation, and when a large object is created, we clearly have the total space that can be accommodated, but the space is not continuous and the object cannot be allocated, so the GC has to be triggered in advance.

  2. Unstable performance : Objects in memory need to be recycled. When a large number of objects in memory need to be recycled, usually these objects may be scattered, so the cleaning process will be more time-consuming, and the cleaning speed will be slower at this time.

2.2 Marked copy method

The biggest problem with the mark and clear method is that it will cause space fragmentation. At the same time, if there are too many recyclable objects, its performance will be affected. The mark copy method can solve these two problems. The mark-and-sweep method focuses on recyclable objects, while the mark-and-copy method focuses on surviving objects. It transfers the surviving objects to a new area, and then uniformly cleans up the objects in the original area. .

First, it divides the memory into three areas, one area is used to store newly created objects called the Eden area, and the other two areas are used to store surviving objects called the S1 area and S2 area. There will be two situations during recycling. One is to copy the surviving objects in the Eden and S1 areas to the S2 area. The second is to copy the surviving objects in the Eden and S2 areas to the S1 area. That is to say, the areas between S1 and S2 Only one of the two areas will be used at the same time. In this way, it is guaranteed that there will always be a blank area to store surviving objects in the next GC, and the original area does not need to consider retaining surviving objects, so it can be directly used once Clear all objects, which should be simple and direct while ensuring memory continuity of the cleared memory area.

Insert image description here

Advantages of the marked copy method :
The marked copy method solves the space fragmentation problem of the mark and clear method, and uses the method of moving surviving objects. Each clearing targets a whole block of memory, so the efficiency of clearing recyclable objects is also relatively high. However, because the object needs to be moved, it will take some time, so the efficiency of the mark copy method will still be lower than the mark clear method.

Disadvantages of the marked copy method :

  1. It will waste some space : It is not difficult to find from the above figure that there is always a free memory area that cannot be used, which also causes a waste of resources.

  2. Having many surviving objects will be very time-consuming : Because the process of copying and moving objects is time-consuming, this requires not only moving the objects themselves, but also modifying the reference addresses using these objects, so scenarios with many surviving objects will be very time-consuming. Therefore, the marked copy method is more suitable for scenarios with fewer surviving objects.

  3. A guarantee mechanism is needed : Because there is always a waste of space in the copy area, and in order to reduce too much wasted space, we will control the space allocation of the copy area to a small range, but too small a space will cause a problem, that is When there are many surviving objects, the space in the copy area may not be enough to accommodate these objects. In this case, you need to borrow some space to ensure that these objects are accommodated. This method of borrowing memory from other places is called a guarantee mechanism.

2.3 Marking and sorting method

The mark copy method perfectly complements the shortcomings of the mark clear method. It not only solves the problem of space fragmentation, but is also suitable for use in scenarios where most objects are recyclable. However, the mark copy method also has imperfections. On the one hand, it needs to free up a memory space to move objects. On the other hand, it is not very suitable for scenes with many surviving objects. Scenes with many surviving objects are usually suitable for using marks. Clear method, but mark and clear method will produce space fragmentation, which is an intolerable problem.

Therefore, there is a need for an algorithm that specifically targets many surviving objects without causing space fragmentation or wasting memory space. This is the original intention of the marking and sorting method. The idea of ​​marking and sorting is easy to understand. Just like when we tidy up a room, we move the useful things and the garbage that needs to be discarded to both sides of the room, and then sweep out the garbage side of the room as a whole.

The marking and sorting method is divided into two stages: marking and sorting. In the marking stage, surviving objects and recyclable objects will be marked first; after marking, the memory objects will be sorted. In this stage, the surviving objects will be moved to one end of the memory. After moving the object, clear the objects outside the boundaries of the surviving objects.

Insert image description here

Advantages of the marking and sorting method :
The marking and sorting method solves the shortcomings of the marking and copying method that wastes space and is not suitable for scenarios with multiple surviving objects. It also solves the shortcomings of the mark and clear method of space fragmentation, so for scenes where the marking and copying method is not suitable, At the same time, if you cannot tolerate the space fragmentation problem of the mark and clear method, you can consider the mark and defragmentation method.

Disadvantages of the marking and sorting method :
No algorithm is omnipotent. The marking and sorting method seems to solve many problems, but it itself has serious performance problems. The marking and sorting method has the lowest performance among the three garbage collection algorithms. , because the marking and sorting method not only needs to move the object when moving the object, but also additionally maintains the reference address of the object. This process may require several scans and positioning of the memory to complete, and at the same time, the empty seats of the object must be cleared. Since there are so many things to do, more time will inevitably be consumed.

2.4 Applicable scenarios of various garbage collection algorithms

After we understand the three garbage collection algorithms, we will find that no algorithm is perfect. Each algorithm has its own characteristics, so we can only choose the appropriate garbage collection algorithm based on specific scenarios.

1. Mark-and-sweep method
Features: Simple and fast collection, but there will be space debris, which will cause the subsequent GC frequency to increase.

Suitable for scenarios: Only a small number of objects need to be recycled, so the mark-and-sweep method is more suitable for garbage collection in the old generation, because there are generally more surviving objects in the old generation than recycled objects.

2. Marked copy method
Features: The collection speed is fast and can avoid space fragmentation, but there is a waste of space. When there are many surviving objects, the process of copying objects will be very time-consuming and requires a guarantee mechanism.

Suitable scenarios: Scenarios where only a small number of objects survive. This is also the characteristic of new generation objects, so generally new generation garbage collectors will basically choose the marked copy method.

3. Mark sorting method
Features: Compared with the mark copy method, it does not waste memory space. The relative mark clearing method can avoid space fragmentation, but it is slower than the other two algorithms.

Suitable scenarios: Scenarios where memory is tight and space fragmentation needs to be avoided. If the elderly want to avoid space fragmentation problems, they usually use mark defragmentation.


3. Generational garbage collection

3.1 Generational garbage collection

JVM garbage collection in actual situations will coordinate several garbage collection algorithms according to specific situations, and the specific implementation is called a generational garbage collection mechanism. The large area of ​​the entire heap memory is divided into the new generation and the old generation, and the new generation is divided into the Garden of Eden, the survival area From, and the survival area To. The reason for this division is that some objects need to be used for a long time, and these objects are placed in the old generation; while some objects that do not need to be used for a long time are placed in the new generation. Use different garbage collection algorithms for different areas.
Insert image description here

  • New objects will create objects in the Eden area
  • When the Eden memory can no longer accommodate new objects, the Minor GC (using the reachability analysis algorithm ) is triggered, and then the surviving objects are copied to the survivor area To using the copy algorithm , making their age + 1, and exchanging the From and To areas.
  • Minor GC will trigger STW (stop the world) - suspend other user threads, and resume user threads only after the garbage collection thread completes recycling.
  • When the object life exceeds the threshold, it will be promoted to the old generation . The maximum lifespan is 15 (4bit, the maximum decimal value is 15)
  • When there is insufficient space in the old generation , Minor GC will be triggered first. If the space is still insufficient, Full GC will be triggered . The STW time will be longer.
  • If the old generation space is still insufficient after Full GC, Out Of Memory Error: Java Heap Space will be triggered.

3.2 Related JVM parameters

Insert image description here

3.3 GC analysis

Run the following program to observe the GC output details and the
initial changes in heap memory.

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
    }
}
Heap
 def new generation   total 9216K, used 1731K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  21% used [0x00000007bec00000, 0x00000007bedb0f58, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,   0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf600200, 0x00000007c0000000)
 Metaspace       used 3159K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K
  • def new generation: New generation, set to -Xmx20M, actually 9216K. Because the survivor area To is an unavailable area by default, the memory occupied by this area needs to be subtracted.
  • tenured generation: old generation
  • Metaspace: metaspace

Small object creation

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
    }
}
[GC (Allocation Failure) [DefNew: 1567K->388K(9216K), 0.0015099 secs] 1567K->388K(19456K), 0.0016601 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 8478K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  98% used [0x00000007bec00000, 0x00000007bf3e6838, 0x00000007bf400000)
  from space 1024K,  37% used [0x00000007bf500000, 0x00000007bf561160, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 0K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,   0% used [0x00000007bf600000, 0x00000007bf600000, 0x00000007bf600200, 0x00000007c0000000)
 Metaspace       used 3153K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 347K, capacity 388K, committed 512K, reserved 1048576K

Small object creation triggers GC

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);
    }
}
[GC (Allocation Failure) [DefNew: 1567K->392K(9216K), 0.0012920 secs] 1567K->392K(19456K), 0.0014405 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8400K->880K(9216K), 0.0028952 secs] 8400K->8048K(19456K), 0.0029182 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 1801K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  11% used [0x00000007bec00000, 0x00000007bece60f0, 0x00000007bf400000)
  from space 1024K,  86% used [0x00000007bf400000, 0x00000007bf4dc390, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 7168K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  70% used [0x00000007bf600000, 0x00000007bfd00010, 0x00000007bfd00200, 0x00000007c0000000)
 Metaspace       used 3160K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 348K, capacity 388K, committed 512K, reserved 1048576K

Large object creation

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
    }
}
Heap
 def new generation   total 9216K, used 1731K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,  21% used [0x00000007bec00000, 0x00000007bedb0f58, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
  to   space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
 tenured generation   total 10240K, used 8192K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  80% used [0x00000007bf600000, 0x00000007bfe00010, 0x00000007bfe00200, 0x00000007c0000000)
 Metaspace       used 3151K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 347K, capacity 388K, committed 512K, reserved 1048576K

Large object creation triggers OOM, causing the main thread to end

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Main {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) {
    
    
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
        list.add(new byte[_8MB]);
    }
}
[GC (Allocation Failure) [DefNew: 1567K->388K(9216K), 0.0012293 secs][Tenured: 8192K->8579K(10240K), 0.0014159 secs] 9759K->8579K(19456K), [Metaspace: 3059K->3059K(1056768K)], 0.0028190 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [Tenured: 8579K->8562K(10240K), 0.0008542 secs] 8579K->8562K(19456K), [Metaspace: 3059K->3059K(1056768K)], 0.0008647 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 410K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,   5% used [0x00000007bec00000, 0x00000007bec66800, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 8562K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  83% used [0x00000007bf600000, 0x00000007bfe5c8a8, 0x00000007bfe5ca00, 0x00000007c0000000)
 Metaspace       used 3179K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.tzuaness.jvm.Demo02GCPrintDetails.main(Demo02GCPrintDetails.java:19)

Creating a large object in the thread triggers OOM and does not cause the main thread to end.

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:PrintGCDetails -verbose:gc
 */
public class Demo02GCPrintDetails {
    
    
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            List<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();

        Thread.sleep(1000);
        System.out.println("over...");
    }
}
[GC (Allocation Failure) [DefNew: 3898K->623K(9216K), 0.0031331 secs][Tenured: 8192K->8813K(10240K), 0.0031949 secs] 12090K->8813K(19456K), [Metaspace: 4095K->4095K(1056768K)], 0.0065025 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 8813K->8757K(10240K), 0.0019078 secs] 8813K->8757K(19456K), [Metaspace: 4095K->4095K(1056768K)], 0.0019295 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
	at com.tzuaness.jvm.Demo02GCPrintDetails.lambda$main$0(Demo02GCPrintDetails.java:20)
	at com.tzuaness.jvm.Demo02GCPrintDetails$$Lambda$1/1480010240.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:750)
over...
Heap
 def new generation   total 9216K, used 546K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
  eden space 8192K,   6% used [0x00000007bec00000, 0x00000007bec88b98, 0x00000007bf400000)
  from space 1024K,   0% used [0x00000007bf500000, 0x00000007bf500000, 0x00000007bf600000)
  to   space 1024K,   0% used [0x00000007bf400000, 0x00000007bf400000, 0x00000007bf500000)
 tenured generation   total 10240K, used 8757K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
   the space 10240K,  85% used [0x00000007bf600000, 0x00000007bfe8d668, 0x00000007bfe8d800, 0x00000007c0000000)
 Metaspace       used 4123K, capacity 4676K, committed 4864K, reserved 1056768K
  class space    used 461K, capacity 496K, committed 512K, reserved 1048576K

4. Garbage collector

4.1 Serial

Insert image description here

  • single thread
  • The heap memory is small and suitable for personal computers
  • Enable serial garbage collection: -XX:+UseSerialGC=Serial+SerialOld

4.2 Throughput priority

Insert image description here

  • Multithreading
  • Large heap memory, multi-core CPU
  • Try to minimize the STW time per unit time (0.2 + 0.2 = 0.4)
  • Related JVM parameters: Parameters 3 and 4 are mutually exclusive, and both are related to parameter 2
    1. -XX:+UseParallelGC ~ -XX:+UseParallelOldGC (turn on one of them, and the other will be turned on by default. 1.8 turns it on by default)
    2. -XX:+UseAdaptiveSizePolicy, uses an adaptive size adjustment strategy to dynamically adjust the Eden Survivor area when ParallelGC occurs
    3. -XX:GCTimeRatio=ratio, the calculation formula is 1/(1+ratio). The total running time of the program is total. If the garbage collection time exceeds total/(1+ratio), the heap memory will be increased to reduce the number of GCs, so as to achieve this target value (increase throughput). After the heap is increased, although the total GC time is reduced, each time GC time will increase). The default value is 99, usually set to 19
    4. -XX:MaxGCPauseMillis=ms, the maximum time for each GC, this value is exclusive with parameter 3 (because lowering this value means that the heap memory needs to be smaller to reduce the average GC time). The default value is 200
    5. -XX:ParallelGCThreads=n, the number of threads for parallel garbage collection

4.3 Response time priority

Insert image description here

  • Multithreading
  • Large heap memory, multi-core CPU
  • Keep the time of each STW as short as possible (0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5)
  • Relevant JVM parameters:
    1. -XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld, when the GC thread is running, the user thread can also run
    2. -XX:ParallelGCThreads=n ~ -XX:ConcGCTreads=threads, the number of parallel GC threads and the number of concurrent GC threads, generally threads=n/4
    3. -XX:CMSInitiatingOccupancyFraction=percent, sets the percentage of memory usage in the old generation that triggers CMSGC. If it is set to 80%, CMS will be triggered when the memory usage in the old generation reaches 80%, and the other 20% is reserved for floating garbage. The default value is 65%
    4. -XX:+CMSScavengeBeforeRemark

The CMS garbage collector has two flaws. One is that it cannot handle floating garbage. That is, during the concurrent cleaning process, the user thread generates new garbage. This garbage cannot be marked and must wait until the next FullGC for cleaning. Therefore, a certain amount of space needs to be reserved in the old generation to hold floating garbage. Under JDK1.6 version, the CMS startup threshold is 92%, that is, if the old generation memory exceeds 92%, the CMS is started. This process may cause concurrency failure, that is, the memory space cannot be satisfied when the CMS is running. At this time, the virtual machine will take out the Serial Old and perform STW to perform serial garbage cleaning.

4.4 G1

Definition: Garbage First

history:

  • 2004 Paper
  • 2009 JDK 6u14 experience
  • 2012 JDK 7u4 official support
  • 2017 JDK 9 support

Features:

  • Pay attention to both throughput and low latency. The default pause target is 200ms.
  • Suitable for very large heap memory (the heap will be divided into multiple regions of equal size)
  • The whole is a marking + sorting algorithm, and the two areas are a copy algorithm.

Relevant JVM parameters:

  • -XX:+UseG1GC
  • -XX:G1HeapReginSize=size
  • -XX:MaxGCPauseMillis=time
4.4.1 G1 garbage collection phase

Insert image description here

4.4.2 Young Collection

The G1 garbage collector divides the heap memory into multiple regions of equal size, where E is the region Eden divided into the Garden of Eden.
Insert image description here
After the space in the Eden area is full, Young GC is triggered, and the copy algorithm is used to copy the surviving objects to the Survivor area.
Insert image description here
After the space in the Survivor area is full, the older objects will be copied to the Old area, and the younger objects will still stay, and their age will be +1.
Insert image description here

4.4.3 Young Collection + CM

Initial marking of GC Root will be performed during Young GC.
When the proportion of heap space occupied by the old generation reaches the threshold, concurrent marking will be performed (without STW). The threshold is determined by this JVM parameter: -XX:InitiatingHeapOccupancyPercent=percent (default 45%)
Insert image description here

4.4.4 Mixed Collection mixed garbage collection

Comprehensive garbage collection will be performed on E, S, and O

  • The final mark (Remark) will be STW, with the purpose of recycling the garbage objects missed during concurrent marking (garbage objects generated by other user threads during concurrent marking)
  • Copy survival (Evacuation) will STW

-XX:MaxGCPauseMillis=ms

Insert image description here

4.4.5 Full GC Analysis

SerialGC and ParallelGC

  • Garbage collection that occurs due to insufficient memory in the new generation: minor gc
  • Garbage collection that occurs due to insufficient memory in the old generation: full gc

CMS and G1

  • Garbage collection that occurs due to insufficient memory in the new generation: minor gc
  • There is insufficient memory in the old generation. When concurrent marking and mixed collection are triggered, if the garbage collection speed is higher than the garbage generation speed, it is not full gc at this time, but concurrent garbage collection; only when the garbage collection speed is lower than the garbage generation speed, concurrent garbage collection will If it fails, it will degenerate into serial garbage collection and full gc will occur. At this time, a longer STW will be performed.
4.4.6 Young Collection cross-generation reference (details)

The problem of cross-generation references in the new generation recycling (the old generation refers to the new generation)

Insert image description here

  • Card List and Remembered Set
  • Pass post-write barrier + dirty card queue when reference changes
  • concurrent refinement threads 更新Remembered Set

Insert image description here

4.4.7 Remark (details)

pre-write barrier + satb_mark_queue

Just after concurrent marking ends, if an object marked as garbage becomes a member of the GC Root reference chain again due to the work of other user threads, the write barrier will be triggered, the object will be added to the queue and marked as a surviving object. , when re-marking (final marking), the object in the queue will be re-checked after STW to see if it is a garbage object.
Insert image description here

4.4.8 JDK 8u20 string deduplication

-XX:+UseStringDeduplication (enabled by default)

String s1 = new String("hello"); // char[]{'h', 'e', 'l', 'l', 'o'}
String s2 = new String("hello"); // char[]{'h', 'e', 'l', 'l', 'o'}

Put all newly allocated strings into a queue. When garbage collection occurs in the new generation, G1 concurrently checks whether there are string duplications so that strings with duplicate values ​​refer to the same char[]

Advantages : Saves a lot of memory
Disadvantages : Slightly more CPU time is occupied, and the new generation recycling time is slightly increased

Note : Its optimization principle is different from String.inter()

  • String.intern() focuses on the non-repetition of string objects
  • This string deduplication focuses on non-duplication of char[]
  • Inside the JVM they use different string tables
4.4.9 JDK 8u40 concurrent mark class unloading

After all objects are marked for concurrency, you can know which classes are no longer used. When all classes of a class loader are no longer used, all classes loaded by it are unloaded.

-XX:+ClassUnloadingWithConcurrentMark (enabled by default)

4.4.10 JDK 8u60 recycles giant objects
  • When an object is larger than half of the region, it is called a giant object.
  • G1 does not copy huge objects
  • Prioritized for recycling
  • G1 will track all incoming references in the old generation, so that giant objects with 0 incoming references in the old generation can be disposed of during new generation garbage collection. (For example, the H in the upper right corner of the picture below is no longer referenced by the card table in the O area, and it will be recycled during the new generation garbage collection)
    Insert image description here
4.4.11 JDK 9 adjustment of concurrent mark starting time
  • Concurrent marking must be completed before the heap space is full, otherwise it will degenerate into Full GC.
  • Before JDK 9, -XX:InitiatingHeapOccupancyPercent was required to set the initial value that triggers the concurrent mark.
  • JDK 9 can perform data sampling and dynamically adjust the value. It will add a safe gap space to accommodate floating garbage and avoid degenerating into Full GC as much as possible.
4.4.12 JDK 9 more efficient recycling
  • 250+ enhancements
  • 180+ bug fixes

5. Garbage collection tuning

Use the command to view the GC related parameter configuration of the current JVMjava -XX:+PrintFlagsFinal -version | grep GC

5.1 Tuning areas

  • Memory
  • lock contention
  • CPU usage
  • IO

5.2 Determine goals

Choose the appropriate garbage collector according to whether the requirement is [low latency] or [high throughput]

  • CMS,G1,ZGC
  • ParallelGC
  • Zing

5.3 The fastest GC is no GC.

Check the memory usage before and after Full GC and consider the following issues:

  • Is there too much data?
    • resultSet = statement.executeQuery(“SELECT * FROM big_table;”);
  • Is the data representation too bloated?
    • Object graph, for example: the object graph retrieved from the query is much more than the actual data needed
    • Object size: Example: The object header in Integer is 16 bytes, and the 4-byte int value takes up 32 bytes after alignment, while int only requires 4 bytes.
  • Is there a memory leak?
    • Example: static Map map = new HashMap<>(), continuously putting data into it will trigger OOM. Soft reference and weak reference optimization can be used

5.4 New Generation Tuning

Characteristics of the new generation:

  • Memory allocation for all new operations is very cheap because there is a TLAB (thread-local allocation buffer) when the object is created.
  • The recycling cost of dead objects is zero, because when garbage collection uses a copy algorithm, the surviving objects of Eden and Survivor From will be copied to Survivor To. The remaining space is cleared
  • Most objects die immediately after use
  • The time of Minor GC is much lower than that of Full GC

Is the larger the new generation (-Xmn) better?

Oracle original text:
-Xmn sets the initial value and maximum value of the young generation in the heap. GC occurs more frequently in the young generation area than in other areas. If the young generation is too small, it will lead to frequent Minor GC; if it is too large, it will lead to only Full GC, which takes up a lot of time. It is recommended that the young generation accounts for 25% to 50% of the entire heap memory.

The specific setting is more appropriate: ideally the product of the objects generated during a request and response and the amount of concurrency. Assuming that one request generates approximately 512KB objects and there are 1,000 concurrent requests, then it is 512KB * 1000 ~ 512MB

Survival zone tuning

  • The survival area is large enough to retain [currently active objects + objects that need to be promoted]
  • Properly configure the promotion threshold to promote long-lived objects as quickly as possible (if promotion is too slow, multiple object copies from and to will take a lot of time, and the main time spent in Minor GC is object copy)

Adjust the maximum promotion threshold: -XX:MaxTenuringThreshold=threshold
Survivor zone object age and occupancy monitoring: -XX:+PrintTenuringDistribution The
Insert image description here
left column is the total memory occupied by objects of this age, and the right column ends with the total memory occupied by objects less than or equal to this age.

5.5 Old generation tuning

Take CMS as an example:

  • The larger the CMS old generation memory, the better (to prevent CMS garbage collection from degenerating into Serial Old)
  • Try not to tune first. If Full GC does not occur, it means that the old generation is abundant and does not require optimization. Even if Full GC occurs, you still need to give priority to tuning the new generation.
  • Observe the old generation memory usage when Full GC occurs, and increase the old generation memory default by 1/4 ~ 1/3
    -XX:CMSInitiatingOccupancyFractioin=percent

5.6 Case

1. Frequent Minor GC and Full GC
Analysis: It may be because the new generation is small, resulting in frequent Minor GC, which further leads to more objects entering the old generation, resulting in frequent Full GC in the old generation.
Solution: You can try to increase the size of the new generation, reduce the frequency of Minor GC, and increase the threshold for promoting new generation objects to the old generation to reduce the occupation of the old generation.

2. Full GC occurs during the peak request period, and the single pause time is particularly long (CMS)
analysis: Check the GC log, and the time spent in each GC stage will be displayed. The remarking phase of the CMS garbage collector is time-consuming. Determine whether the log meets this situation.
Solution: When it meets the requirements, you can use the -XX:+CMSScavengeBeforeRemark parameter to perform garbage collection before remarking to reduce the time spent on remarking.

3. When the old generation is abundant, Full GC occurs (CMS JDK1.7)
Analysis: Insufficient metaspace memory causes Full GC
Solution: Increase metaspace memory usage

4. Class loading and bytecode technology

1. Class loading process

Insert image description here

1.1 Class file structure

1.2 Loading

Load the bytecode of the class into the method area. The instanceKlass of C++ is used internally to describe the java class. Its important fields are:

  • _java_mirror is the class mirror of java. For example, for String, it is String.class. Its function is to expose klass
    to java for use.
  • _super is the parent class
  • _fields is the member variable
  • _methods is the method
  • _constants is the constant pool
  • _class_loader is the class loader
  • _vtable virtual method table
  • _itable interface method table

If this class has a parent class that has not been loaded, load the parent class first. Loading and linking may run alternately

Notice:

  • [Metadata] like instanceKlass is stored in the method area (metaspace after 1.8), but _java_mirror
    is stored in the heap
  • You can view it through the HSDB tool introduced earlier.
  • The corresponding relationship between instanceKlass and class is as follows:
    Insert image description here

1.3 Links

This phase includes verification, preparation, and analysis:

  • Verification: Verify whether the class complies with JVM specifications, security check
  • Preparation: allocate space for static variables and set default values
    • Static variables are stored at the end of instanceKlass before JDK 7, and starting from JDK 7, they are stored at the end of _java_mirror
    • Allocating space and assigning values ​​to static variables are two steps. Allocating space is completed in the preparation phase, and assignment is completed in the initialization phase.
    • If the static variable is a final basic type, and a string constant, then the value is determined during the compilation phase, and the assignment is completed during the preparation phase.
    • If the static variable is a final reference type, then the assignment will also be completed during the initialization phase.
  • Parsing: Resolve symbol references in the constant pool into direct references
    /* 解析的含义 */ 
    public class Load2 {
          
           
    	public static void main(String[] args) throws ClassNotFoundException, IOException {
          
           
    		ClassLoader classloader = Load2.class.getClassLoader(); 
    		// loadClass() 只涉及类的加载,不会进行类的解析和初始化 
    		Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C"); 
    		
    		new C(); // 会进行类的初始化操作
    		System.in.read(); 
    	} 
    }
    class C {
          
           D d = new D(); }
    class D {
          
           }
    

1.4 Initialization

Initialization means calling the <cinit>()V method, and the virtual machine ensures the thread safety of the "constructor method" of this class. To summarize, class initialization is
when [lazy] class initialization occurs.

  • The class where the main method is located will always be initialized first.
  • When accessing a static variable or static method of this class for the first time
  • Subclass initialization, if the parent class has not been initialized, it will trigger
  • When a subclass accesses the static variables of the parent class, it will only trigger the initialization of the parent class.
  • Class.forName() method
  • new keyword

Situations that do not result in class initialization

  • Accessing static final static constants (basic types and strings) of a class will not trigger initialization
  • Class objects.class will not trigger initialization, such as String.class
  • Creating an array of this class will not trigger initialization, such as new String[0]
  • The loadClass method of the class loader
  • When the second parameter of Class.forName is false

Exercise: Lazy loading singleton pattern

// 懒加载单例模式
class Singleton {
    
    
	private Singleton() {
    
    }

	private static class LazyHolder {
    
    
		private static final Singleton INSTANCE = new Singleton();
	}

	// 第一次调用该方法,才会导致内部类加载和初始化其静态成员
	public Singleton getInstance() {
    
    
		return LazyHolder.INSTANCE;
	}
}

2. Class loader

Take JDK 8 as an example:

name Load directory illustrate
Bootstrap ClassLoader JAVA_HOME/jre/lib Start the class loader, implemented in C++, and cannot be accessed directly by users
Extension ClassLoader JAVA_HOME/jre/lib/ext Extension class loader, the superior is the system class loader
Application ClassLoader classpath Application class loader, the parent is the extension class loader
Custom class loader customize User-defined loader, the upper level is the application class loader

Generally, classes whose package names start with java (located in the lib directory) are loaded by the startup class loader. The java code we write ourselves is loaded by the application class loader.

2.1 Start the class loader

Load the class using the startup class loader:

// 使用此命令运行该类:java -Xbootclasspath/a:. BootstrapClassLoaderTest
public class BootstrapClassLoaderTest {
    
    
	public static void main(String[] args) {
    
    
		// 如果使用的是BootstrapClassLoader,打印结果为null
		System.out.println(BootstrapClassLoaderTest.class.getClassLoader());
	}
}
  • -Xbootclasspath means setting bootclasspath
  • Among them, /a:. means appending the current directory to the bootclasspath.
  • You can use this method to replace the core class
    • java -Xbootclasspath:
    • java -Xbootclasspath/a:<append path>
    • java -Xbootclasspath/p:<append path>

2.2 Extension class loader

Load classes using an extension class loader:

1. 将字节码文件打成jar包:jar -cvf extclassloadertest.jar ExtensionClassloaderTest.class
2. 将jar包拷贝到JAVA_HOME/jre/lib/ext
3. 执行该文件,输出结果sun.misc.Launcher$ExtClassLoader@29453f44

2.3 Parental delegation mechanism

The so-called parent delegation refers to the rules for searching classes when calling the loadClass method of the class loader. When ClassLoader receives a class loading request, it will not load the class itself, but will entrust the superior class loader to load it, layer by layer until the uppermost class loader (starting class loader). When the ClassLoader cannot find the class in the loading path, it is handed over to the lower-level class loader for loading.

Relevant Java code implementation:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    
    
	synchronized (getClassLoadingLock(name)) {
    
     
		// 1. 检查该类是否已经加载 
		Class<?> c = findLoadedClass(name); 
		if (c == null) {
    
     
			long t0 = System.nanoTime(); 
			try {
    
    
				if (parent != null) {
    
     
					// 2. 有上级的话,委派上级 
					loadClass c = parent.loadClass(name, false); 
				} else {
    
     
					// 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
					c = findBootstrapClassOrNull(name); 
				} 
			} catch (ClassNotFoundException e) {
    
     
			}

			if (c == null) {
    
     
				long t1 = System.nanoTime(); 
				// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载 
				c = findClass(name); 
				// 5. 记录耗时 
				sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); 		
				sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); 
				sun.misc.PerfCounter.getFindClasses().increment(); 
			} 
		}

		if (resolve) {
    
     
			resolveClass(c); 
		}
	
		return c; 
	} 
}

For example:

public class Load5_3 {
    
     
	public static void main(String[] args) throws ClassNotFoundException {
    
     
		Class<?> aClass = Load5_3.class.getClassLoader() .loadClass("com.ziang.jvm.t3.load.H"); 
		System.out.println(aClass.getClassLoader()); 
	} 
}

The execution process is:

  1. sun.misc.Launcher$AppClassLoader //At 1, start checking the loaded classes, but there is no result
  2. sun.misc.Launcher$AppClassLoader // 2 places, delegate to superior
    sun.misc.Launcher$ExtClassLoader.loadClass()
  3. sun.misc.Launcher$ExtClassLoader // At 1, check the loaded class, the result is no
  4. sun.misc.Launcher$ExtClassLoader // At 3, if there is no superior,
    the search is delegated to BootstrapClassLoader
  5. BootstrapClassLoader looks for the H class under JAVA_HOME/jre/lib, but obviously there is none
  6. sun.misc.Launcher$ExtClassLoader // At 4th place, call your own findClass method
    to find the H class under JAVA_HOME/jre/lib/ext. Obviously there is no such class. Go back to sun.misc.Launcher$AppClassLoader at 2nd place.
  7. Continue execution to sun.misc.Launcher$AppClassLoader // 4, call its own findClass method,
    search under the classpath, and find it

2.4 Thread context class loader

When we use JDBC, we all need to load the Driver. I wonder if you have noticed that the Class.forName("com.mysql.jdbc.Driver")com.mysql.jdbc.Driver class can be loaded correctly without writing it. Do you know how to do it?

Let’s not look at anything else first, let’s take a look at DriverManager’s class loader: execution System.out.println(DriverManager.class.getClassLoader());, the output result is null. It means that its class loader is Bootstrap ClassLoader, and it will search for classes under JAVA_HOME/jre/lib, but there is obviously no mysql-connector-java-5.1.47.jar package under JAVA_HOME/jre/lib, so the problem arises. In DriverManager How can I load com.mysql.jdbc.Driver correctly in the static code block?

Let's trace the source code:

public class DriverManager {
    
     
	// 注册驱动的集合 
	private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>(); 
	
	// 初始化驱动 
	static {
    
     
		loadInitialDrivers(); 
		println("JDBC DriverManager initialized"); 
	}

Continue to look at the loadInitialDrivers() method:

private static void loadInitialDrivers() {
    
     
	String drivers; 
	try {
    
    
		drivers = AccessController.doPrivileged(new PrivilegedAction<String> () {
    
     
			public String run() {
    
     
				return System.getProperty("jdbc.drivers"); 
			} 
		}); 
	} catch (Exception ex) {
    
     
		drivers = null; 
	}
	
	// 1)使用 ServiceLoader 机制加载驱动,即 SPI 
	AccessController.doPrivileged(new PrivilegedAction<Void>() {
    
     
		public Void run() {
    
     
			ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); 	
			Iterator<Driver> driversIterator = loadedDrivers.iterator(); 
			try{
    
    
				while (driversIterator.hasNext()) {
    
     
					driversIterator.next(); 
				} 
			} catch(Throwable t) {
    
     
				// Do nothing 
			}
			return null; 
		} 
	}); 
	
	println("DriverManager.initialize: jdbc.drivers = " + drivers); 
	
	// 2)使用 jdbc.drivers 定义的驱动名加载驱动 
	if (drivers == null || drivers.equals("")) {
    
     
		return; 
	}

	String[] driversList = drivers.split(":"); 
	println("number of Drivers:" + driversList.length); 
	for (String aDriver : driversList) {
    
     
		try {
    
    
			println("DriverManager.Initialize: loading " + aDriver); 
			// 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器 
			Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); 
		} catch (Exception ex) {
    
     
			println("DriverManager.Initialize: load failed: " + ex); 
		} 
	} 
}

First look at 2) and find that it finally uses Class.forName to complete the loading and initialization of the class. It is associated with the application class loader, so the class loading can be successfully completed. Then look at 1) It is the famous Service Provider
Interface (SPI). The convention is as follows:

Under the META-INF/services package of the jar package, use the fully qualified name of the interface as the file name, and the file content is the implementation class name.
Insert image description here

In this way, you can use the following code to get the implementation class, which embodies the idea of ​​[interface-oriented programming + decoupling]:

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class); // 接口类型:java.sql.Driver 
Iterator<接口类型> iter = allImpls.iterator(); 
while (iter.hasNext()) {
    
     
	iter.next(); 
}

This idea is used in some of the following frameworks:

  • JDBC
  • Servlet initializer
  • Spring container
  • Dubbo (expanded SPI)

Next look at the ServiceLoader.load method:

public static <S> ServiceLoader<S> load(Class<S> service) {
    
     
	// 获取线程上下文类加载器 
	ClassLoader cl = Thread.currentThread().getContextClassLoader(); 
	return ServiceLoader.load(service, cl); 
}

The thread context class loader is the class loader used by the current thread. The default is the application class loader. Inside ServiceLoader.load(service, cl), Class.forName() calls the thread context class loader to complete the loading. Specifically The code is in the internal class LazyIterator of ServiceLoader:

private S nextService() {
    
     
	if (!hasNextService()) 
		throw new NoSuchElementException(); 
	String cn = nextName; 
	nextName = null; 
	Class<?> c = null; 
	try {
    
    
		// 此处的loader就是上一步中的cl
		c = Class.forName(cn, false, loader); 	
	} catch (ClassNotFoundException x) {
    
     
		fail(service, "Provider " + cn + " not found"); 
	}
	
	if (!service.isAssignableFrom(c)) {
    
     
		fail(service, "Provider " + cn + " not a subtype"); 
	}
	try {
    
    
		S p = service.cast(c.newInstance()); 
		providers.put(cn, p); 
		return p; 
	} catch (Throwable x) {
    
     
		fail(service, "Provider " + cn + " could not be instantiated", x); 
	}

	throw new Error(); // This cannot happen 
}

2.5 Custom class loader

Ask yourself, when do you need a custom class loader:

  1. Want to load class files in any path other than classpath
  2. They are all implemented through interfaces. When decoupling is desired, they are often used in framework design.
  3. These classes are expected to be isolated. Classes with the same name in different applications can be loaded without conflict. They are common in tomcat containers.

step:

  1. Inherit ClassLoader parent class
  2. To comply with the parental delegation mechanism, override the findClass method (note that you do not override the loadClass method, otherwise the parental delegation mechanism will not be used)
  3. Read bytecode of class file
  4. Call the defineClass method of the parent class to load the class
  5. The user calls the loadClass method of the class loader
    . Example:
class MyClassloader extends Classloader {
    
    
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
		String path = "\\myclasspath\\" + name + ".class";

		try {
    
    
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			Files.copy(Paths.get(path), os);
			// 得到字节数组
			byte[] bytes = os.toByteArray();
			return defineClass(name, bytes, 0, bytes.length);
		} catch (IOException e) {
    
    
			e.printStackTrace();
			throw new ClassNotFoundExeception("类文件未找到:", path);
		}
	}
}

5. Runtime optimization

1. Just-in-time compilation

Layered compilation:
Let’s take an example first

public class JIT1 {
    
     public static void main(String[] args) {
    
     for (int i = 0; i < 200; i++) {
    
     long start = System.nanoTime(); for (int j = 0; j < 1000; j++) {
    
     new Object(); }long end = System.nanoTime(); System.out.printf("%d\t%d\n",i,(end - start)); } } }

Observing the time interval of the output, you will find that it may be 5 digits at first, but then it becomes 3 digits.
What is the reason?
JVM divides execution status into 5 levels:

  • Level 0, Interpreter
  • Level 1, compiled and executed using C1 just-in-time compiler (without profiling)
  • Layer 2, compiled and executed using C1 just-in-time compiler (with basic profiling)
  • Layer 3, compiled and executed using C1 just-in-time compiler (with complete profiling)
  • Level 4, compiled and executed using C2 just-in-time compiler

Profiling refers to collecting some data on the execution status of the program during the running process, such as [number of method calls], [
number of loop loopbacks], etc.

The difference between a just-in-time compiler (JIT) and an interpreter

  • The interpreter interprets bytecode into machine code. Even if the same bytecode is encountered next time, repeated interpretation will still be performed.
  • JIT compiles some bytecodes into machine codes and stores them in Code Cache. Next time you encounter the same code, you can execute it directly without compiling again.
  • An interpreter interprets bytecode into machine code that is common to all platforms
  • JIT will generate platform-specific machine code based on the platform type

For the infrequently used code that occupies a large part, we do not need to spend time compiling it into machine code, but run it through interpretation and execution; on the other hand, for the hot code that only occupies a small part, we can compile it into machine code to achieve the desired running speed. A simple comparison of Interpreter < C1 < C2 in terms of execution efficiency. The overall goal is to discover hotspot codes (the origin of the name hotspot). The optimization method just now is called [escape analysis] to find out whether the newly created objects escape. You can use -XX:-DoEscapeAnalysis to turn off escape analysis, and then run the example just now to observe the results.

Reference: https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machineperformance-enhancements.html#GUID-D2E3DC58-D18B-4A6C-8167-4A1DFB4888E4

method inline

public static void main(String[] args) {
    
     
	int x = 0; 
	for (int i = 0; i < 500; i++) {
    
     
		long start = System.nanoTime(); 
		for (int j = 0; j < 1000; j++) {
    
     
			x = square(9); 
		}
		long end = System.nanoTime(); 
		System.out.printf("%d\t%d\t%d\n",i,x,(end - start)); 
	} 
}

private static int square(final int i) {
    
     
	return i * i; 
}

If it is found that square is a hotspot method and the length is not too long, it will be inlined. The so-called inlining is to copy and paste the code in the method to the caller's location:

System.out.println(9 * 9);

It can also perform constant folding optimization

System.out.println(81);

2. Other optimizations

Other optimizations such as field optimization and reflection optimization. More optimization details can be found online, which will not be described in detail.


Guess you like

Origin blog.csdn.net/qq_45867699/article/details/126336909