In depth analysis of java reflection (V) – class instantiation and class loading

premise

In fact, the previously written in-depth analysis of java reflection (I) - core class libraries and methods has been introduced through class names or Java Lang. class instance to instantiate an object. In "analysis of resource loading in Java", the parent delegation model in the process of class loading is also introduced in detail. This article is mainly to deepen some understanding of class instantiation and class loading.

Class instantiation

In the reflection class library, there are only two methods for instantiating objects:

When writing the reflection class library, t Java is preferred lang.reflect. Constructor #newinstance (object... Initargs) is used for object instantiation. At present, many excellent frameworks (such as spring) refer to this method for object instantiation.

Class loading

Class loading is actually completed by the class loader. Protected class java. The lang. classloader #loadclass (string name, Boolean resolve) method suggests that the class loading process follows the parental delegation model. In fact, we can override this method to completely not follow the parental delegation model and reload the same class (here refers to the complete same class name). The JDK provides two methods for class loading related features:

Class loading in classloader

Class loading is actually a very complex process, which mainly includes the following steps:

The classloader #loadclass () method is the first step to control the class loading process - the loading process, that is, the process of generating class instances from bytecode byte arrays and class names. There is also a protected final class in classloader The defineclass (string name, byte [] B, int off, int len) method is used to specify the full class name and bytecode byte array to define a class. Let's look at the source code of loadclass():

    protected Class<?> loadClass(String name,boolean resolve)
        throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // 检查类是否已经加载过,如果已经加载过,则直接返回
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                // 委派父类加载器去加载类
                try {
                    if (parent != null) {
                        c = parent.loadClass(name,false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                // 委派父类加载器如果加载失败则调用findClass方法进行加载动作
                if (c == null) {
                    // If still not found,then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    // 扩展点-1
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    } 
    // 扩展点-2
    protected final void resolveClass(Class<?> c) {
        if (c == null) {
            throw new NullPointerException();
        }
    }       

In fact, the loadclass () method leaves two extension points to change the behavior of class loading, and the findclass () method is used to extend the behavior of child class loader when the parent class loader fails to load. Of course, in fact, class The loadclass (string name, Boolean resolve) method is a non final method, which can be overridden by the entire method. In this way, the two parent delegation mechanism can be completely broken. However, it should be noted that even if the parental delegation mechanism is broken, it is impossible for the subclass loader to reload some class libraries loaded by the bootstrap class loader, such as Java Lang. string, which are verified and guaranteed by the JVM. The use of custom class loader is expanded in detail in "class reload" in the next section.

Finally, there are two important points:

Class loading in class

java. Class loading in lang.class is mainly performed by public static class The forname (string name, classloader) method is completed. This method can specify the full class name, whether to initialize and the class loader instance. The source code is as follows:

    @CallerSensitive
    public static Class<?> forName(String name,ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // Reflective call to get caller class is only needed if a security manager
            // is present.  Avoid the overhead of making this call otherwise.
            caller = Reflection.getCallerClass();
            if (loader == null) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (ccl != null) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name,initialize,loader,caller);
    }

    private static native Class<?> forName0(String name,ClassLoader loader,Class<?> caller) throws ClassNotFoundException;

It finally calls the local interface method of the JVM. Because it is temporarily unable to analyze the source code of the JVM, it can only understand the function of the method through the annotation of the forname method:

In other words, the initialize parameter has no meaning for the initialized class or interface. The features of this method can also refer to Chapter 12 of the Java language specification, which is not expanded here.

Although it is temporarily impossible to analyze the JVM local interface method native class Forname0() function, but it relies on a class loader instance input parameter. You can boldly guess that it also relies on loadclass() of the class loader for class loading.

Class reload

First, an experiment is proposed. If a class is defined, it is as follows:

public class Sample {

	public void say() {
		System.out.println("Hello Doge!");
	}
}

If you use the bytecode tool to modify the content of the say () method to system out. println("Hello Throwable!");, And use the custom classloader to reload a sample class with the same class name, then the sample object instantiated by the new keyword calls say() to print "Hello Doge!" Or "Hello throwable!"?

First, the bytecode tool javassist is introduced to modify the bytecode of the class:

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.24.0-GA</version>
</dependency>

Here is the test code:

// 例子
public class Demo {

	public void say() {
		System.out.println("Hello Doge!");
	}
}

// 一次性使用的自定义类加载器
public class CustomClassLoader extends ClassLoader {

	private final byte[] data;

	public CustomClassLoader(byte[] data) {
		this.data = data;
	}

	@Override
	public Class<?> loadClass(String name) throws ClassNotFoundException {
		if (!Demo.class.getName().equals(name)) {
			return super.loadClass(name);
		}
		return defineClass(name,data,data.length);
	}
}

public class Main {

	public static void main(String[] args) throws Exception {

		String name = Demo.class.getName();
		CtClass ctClass = ClassPool.getDefault().getCtClass(name);
		CtMethod method = ctClass.getmethod("say","()V");
		method.setBody("{System.out.println(\"Hello Throwable!\");}");
		byte[] bytes = ctClass.toBytecode();
		CustomClassLoader classLoader = new CustomClassLoader(bytes);
		// 新的Demo类,只能反射调用,因为类路径中的Demo类已经被应用类加载器加载
		Class<?> newDemoClass = classLoader.loadClass(name);
        // 类路径中的Demo类
		Demo demo = new Demo();
		demo.say();
		// 新的Demo类
		newDemoClass.getDeclaredMethod("say").invoke(newDemoClass.newInstance());
		// 比较
		System.out.println(newDemoClass.equals(Demo.class));
	}
}

Output after execution:

Hello Doge!
Hello Throwable!
false

The conclusion here is:

How to avoid memory overflow caused by class reload

In fact, the JDK does not provide a method to unload a loaded class, that is, the life cycle of the class is managed by the JVM. Therefore, to solve the problem of memory overflow caused by class reload is to solve the problem of recycling the reloaded class in the final analysis. Because it is created in Java Lang. class object. If you need to recycle it, you should consider the following points:

Based on these considerations, an experiment can be conducted to verify:

public class Demo {
    // 这里故意建立一个数组占用大量内存
    private int[] array = new int[1000];

    public void say() {
        System.out.println("Hello Doge!");
    }
}

public class Main {

	private static final Map<ClassLoader,List<Class<?>>> CACHE = new HashMap<>();

	public static void main(String[] args) throws Exception {
		String name = Demo.class.getName();
		CtClass ctClass = ClassPool.getDefault().getCtClass(name);
		CtMethod method = ctClass.getmethod("say","()V");
		method.setBody("{System.out.println(\"Hello Throwable!\");}");
		for (int i = 0; i < 100000; i++) {
			byte[] bytes = ctClass.toBytecode();
			CustomClassLoader classLoader = new CustomClassLoader(bytes);
			// 新的Demo类,因为类路径中的Demo类已经被应用类加载器加载
			Class<?> newDemoClass = classLoader.loadClass(name);
			add(classLoader,newDemoClass);
		}
		// 清理类加载器和它加载过的类
		clear();
		System.gc();
		Thread.sleep(Integer.MAX_VALUE);
	}

	private static void add(ClassLoader classLoader,Class<?> clazz) {
		if (CACHE.containsKey(classLoader)) {
			CACHE.get(classLoader).add(clazz);
		} else {
			List<Class<?>> classes = new ArrayList<>();
			CACHE.put(classLoader,classes);
			classes.add(clazz);
		}
	}

	private static void clear() {
		CACHE.clear();
	}
}

Use VM parameter - XX: + printgc - XX: + printgcdetails to execute the above method. Jdk11 uses G1 collector by default. Since Z collector is still in the experimental stage, it is not recommended. After executing the main method, output:

[11.374s][info   ][gc,task       ] GC(17) Using 8 workers of 8 for full compaction
[11.374s][info   ][gc,start      ] GC(17) Pause Full (System.gc())
[11.374s][info   ][gc,phases,start] GC(17) Phase 1: Mark live objects
[11.429s][info   ][gc,stringtable ] GC(17) Cleaned string and symbol table,strings: 5637 processed,0 removed,symbols: 135915 processed,0 removed
[11.429s][info   ][gc,phases      ] GC(17) Phase 1: Mark live objects 54.378ms
[11.429s][info   ][gc,start] GC(17) Phase 2: Prepare for compaction
[11.429s][info   ][gc,phases      ] GC(17) Phase 2: Prepare for compaction 0.422ms
[11.429s][info   ][gc,start] GC(17) Phase 3: Adjust pointers
[11.430s][info   ][gc,phases      ] GC(17) Phase 3: Adjust pointers 0.598ms
[11.430s][info   ][gc,start] GC(17) Phase 4: Compact heap
[11.430s][info   ][gc,phases      ] GC(17) Phase 4: Compact heap 0.362ms
[11.648s][info   ][gc,heap        ] GC(17) Eden regions: 44->0(9)
[11.648s][info   ][gc,heap        ] GC(17) Survivor regions: 12->0(12)
[11.648s][info   ][gc,heap        ] GC(17) Old regions: 146->7
[11.648s][info   ][gc,heap        ] GC(17) Humongous regions: 3->2
[11.648s][info   ][gc,Metaspace   ] GC(17) Metaspace: 141897K->9084K(1062912K)
[11.648s][info   ][gc             ] GC(17) Pause Full (System.gc()) 205M->3M(30M) 273.440ms
[11.648s][info   ][gc,cpu         ] GC(17) User=0.31s Sys=0.08s Real=0.27s

It can be seen that after FullGC, Metaspace (KB) has recovered (141897-9084) KB, which has recovered the memory space of 202M. It can be considered that the memory of the meta space is recovered. Then the clear () method called in the main method is annotated, and then the main method is called again.

....
[4.083s][info   ][gc,heap        ] GC(17) Humongous regions: 3->2
[4.083s][info   ][gc,Metaspace   ] GC(17) Metaspace: 141884K->141884K(1458176K)
[4.083s][info   ][gc             ] GC(17) Pause Full (System.gc()) 201M->166M(564M) 115.504ms
[4.083s][info   ][gc,cpu         ] GC(17) User=0.84s Sys=0.00s Real=0.12s

It can be seen that the meta space is not recycled during fullgc execution, and the heap memory recovery rate is relatively low. From this, we can draw an empirical conclusion: we only need to make a mapping relationship through the classloader object to save the new classes loaded using it. We only need to ensure that these classes have no strong references and class instances have been destroyed, so we only need to remove the references of the classloader object, When the JVM performs GC, it will recycle the classloader object and the classes loaded with it, so as to avoid memory leakage in the meta space.

Summary

Through some data and experiments, some understanding of class loading process is deepened.

reference material:

Personal blog

(end of this paper e-2018129 c-2-d)

The official account of Technology (Throwable Digest), which is not regularly pushed to the original technical article (never copied or copied):

Entertainment official account ("sand sculpture"), select interesting sand sculptures, videos and videos, push them to relieve life and work stress.

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>