Actual JVM: principle and application of ClassLoader

preface

I was asked such a question in the original interview. If you build a Java Lang. String class. Is the String class used in the system your defined String class or the String class in the native api?

You can try and find that the String class in the native api is still used in the final system. Why does this happen? This has to start with the loading process of the class.

We all know that Java is cross platform because JVMs on different platforms can interpret bytecode files as local machine instructions. How does the JVM load bytecode files? The answer is ClassLoader. Let's print the ClassLoader object first

public class ClassLoaderDemo1 {

    public static void main(String[] args) {
        // null
        System.out.println(String.class.getClassLoader());
        ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
        while (loader != null) {
            // sun.misc.Launcher$AppClassLoader@58644d46
            // sun.misc.Launcher$ExtClassLoader@7ea987ac
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}

Parents Delegation Model

To understand this output, we have to talk about the parental delegation model. If a class loader receives a class loading request, it will not load it first, but delegate the request to the loader of the parent class for execution. If the parent class loader still has its parent class loader, it will further delegate upward and recurse in turn, The request will eventually reach the top-level startup class loader. If the parent class loader can complete the class loading task, it will return successfully. If the parent class loader cannot complete the loading task, the child loader will try to load it by itself. This is the parental delegation mode, that is, every child is very lazy, and every time he has work, he will leave it to his father until his father says I can't do it, The son tried to finish it by himself. The parent-child relationship in the parental delegation mode is not the so-called class inheritance relationship, but uses the combination relationship to reuse the relevant code of the parent class loader.

The advantage of adopting the parental delegation mode is that the Java class has a hierarchical relationship with priority along with its class loader. Through this hierarchical relationship, the repeated loading of the class can be avoided. When the father has loaded the class, there is no need for the sub ClassLoader to load it again.

Secondly, considering the security factors, the types defined in the java core API will not be replaced at will. It is assumed that a type named java. Net is passed through the network The class of lang. integer is passed to the startup class loader through the two parent delegation mode, and the startup class loader finds the class with this name in the core Java API and finds that the class has been loaded, and will not reload the java passed through the network Lang. integer, and directly return the loaded integer Class, which can prevent the core API library from being tampered with at will.

The checking and loading process and the function of ClassLoader provided by the system are shown in the figure below. At the beginning of the article, it is easy to understand that the problem is explained by parental loading, and the String class in the native api is used

The relationship between class loaders is as follows:

  1. Start the class loader, which is implemented by C + + and has no parent class.
  2. The extended class loader (ExtClassLoader) is implemented by the Java language, and the parent class loader is null
  3. The application class loader (AppClassLoader) is implemented by Java language, the parent class loader is ExtClassLoader, and the system class loader (AppClassLoader) is implemented by Java language, and the parent class loader is ExtClassLoader
  4. User defined class loader. The parent class loader must be AppClassLoader. Custom class loader. The parent class loader must be AppClassLoader.

Destroy the parental delegation model

However, due to the limitation of loading range, the top-level ClassLoader cannot access the classes loaded by the bottom-level ClassLoader. Therefore, it is necessary to destroy the parental delegation model at this time. Take JDBC as an example to explain why the parental delegation model should be destroyed

Do not use Java SPI

When we first started to operate the database with JDBC, you must have written the following code. First load the driver implementation class, and then get the database link through DriverManager

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

Register the corresponding Driver implementation class with the DriverManager in the Driver class

Using Java SPI

In JDBC 4 After 0, it starts to support the use of SPI to register the Driver. The specific method is to use meta-inf / services / Java in the jar package of mysql sql. The Driver file indicates which Driver is currently used.

SPI is a policy pattern, which determines the implementation class of the runtime interface according to the configuration

In this way, when using different drivers, we do not need to manually pass class Forname loads the driver class. You only need to import the corresponding jar package. So the above code can be changed into the following form

Connection conn = DriverManager.getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");

When is the corresponding driver class loaded?

  1. We start with meta-inf / services / Java sql. Get the specific implementation class "com.mysql.jdbc.Driver" from the driver file
  2. Pass class Forname ("com.mysql.jdbc.Driver") loads this class

DriverManager is in rt.jar package, so DriverManager is loaded by starting ClassLoader. And class Forname() is loaded with the caller's ClassLoader, so if you use the startup ClassLoader to load com mysql. jdbc. Driver, it can't be loaded (because generally, starting the class loader only loads the classes in the rt.jar package).

How to solve it?

If you want the top-level ClassLoader to load the bottom-level ClassLoader, you can only destroy the parent delegation mechanism. Let's see how DriverManager does it

When the DriverManager is loaded, the static code block will be executed. In the static code block, the loadInitialDrivers method will be executed.
The corresponding driver class will be loaded in this method.

public class DriverManager {

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {

        // Omit some codes
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                // Load the driver implementation class according to the configuration file
                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;
            }
        });

        // Omit some codes
    }

}

Let's see what type of ClassLoader he uses. You can see that by executing thread currentThread(). Getcontextclassloader() got the thread context loader

Thread class loader SetContextClassLoader () method setting. The default is application class loader (AppClassLoader)

// ServiceLoader#load
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

ExtClassLoader and AppClassLoader are created through the Launcher class. In the constructor of the Launcher class, we can see that the thread context class loader is AppClassLoader by default

public class Launcher {

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        // Set the thread context class loader to AppClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);

        // Omit some codes

    }
}    

Obviously, the thread context class loader allows the parent class loader to load classes by calling the child class loader, which breaks the principle of the two parent delegation model

Custom class loader

Why customize the class loader?

ClassLoader implementation

Java provides abstract classes Let's take a look at all the user-defined ClassLoader classes, so let's take a look at the main ClassLoader classes of ClassLoader. Lang

I intercepted three important methods

  1. loaderClass: enables parent delegation
  2. findClass: used to copy and load, that is, return the corresponding Class object according to the passed in Class name
  3. defineClass: local method. The final loaded class can only pass defineClass
// Load from this method
public Class<?> loadClass(String name) throws ClassNotFoundException {
	return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
{
	synchronized (getClassLoadingLock(name)) {
		// First, check if the class has already been loaded
		// First find the class object from the cache, and there is no need to reload it
		Class<?> c = findLoadedClass(name);
		if (c == null) {
			long t0 = System.nanoTime();
			try {
				if (parent != null) {
                    // If it cannot be loaded, delegate the parent class to load it	
                    // This reflects the bottom-up check whether the class has been loaded					
					c = parent.loadClass(name, false);
				} else {
				    // If there is no parent class, the delegate starts the loader to load
					c = findBootstrapClassOrNull(name);
				}
			} catch (ClassNotFoundException e) {
				// ClassNotFoundException thrown if class not found
				// This reflects the top-down attempt to load the class. When the parent class cannot be loaded
				// ClassNotFoundException will be thrown
				// from the non-null parent class loader
			}

			if (c == null) {
				// If still not found, then invoke findClass in order
				// to find the class.
				long t1 = System.nanoTime();
				// If they are not found, load them through their own findClass
				// If the findClass method is not found, ClassNotFoundException will be thrown
				c = findClass(name);

				// this is the defining class loader; record the stats
				sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
				sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
				sun.misc.PerfCounter.getFindClasses().increment();
			}
		}
		// Do you need to parse when loading
		if (resolve) {
			resolveClass(c);
		}
		return c;
	}
}

findClass is used for replication loading

protected Class<?> findClass(String name) throws ClassNotFoundException {
	throw new ClassNotFoundException(name);
}

How to customize the class loader?

Java provides abstract classes Lang. ClassLoader, all user-defined class loaders should inherit the ClassLoader class

There are two common ways to customize the loader

  1. Override loadClass method
  2. Override findClass method

However, in general, you can override the findClass method instead of the loadClass method. Because loadClass is used to implement the parent delegation model, modifying this method will cause the model to be damaged and easy to operate. Therefore, generally, we can override the findClass method and return the corresponding Class object according to the passed in Class name

Let's write a ClassLoader by ourselves and load the class file from the specified file

public class DemoObj {

    public String toString() {
        return "I am DemoObj";
    }

}

javac generates the corresponding class file, puts it in the specified directory, and then loads it by FileClassLoader

public class FileClassLoader extends ClassLoader {

    // Directory of class files
    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] getClassData(String className) {

        String path = rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {

        String rootDir = "/Users/peng/study-code/java-learning/src/main/java";
        FileClassLoader loader = new FileClassLoader(rootDir);

        try {
            // The fully qualified name of the incoming class file
            Class<?> clazz = loader.loadClass("com.javashitang.classloader.DemoObj");
            // com.javashitang.classloader.FileClassLoader@1b28cdfa
            System.out.println(clazz.getClassLoader());
            // I am DemoObj
            System.out.println(clazz.newInstance().toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Application of custom loader

It can encrypt and uncover class files, realize the hot deployment of applications, and realize application isolation. Take the ClassLoader in Tomcat as an example

Before explaining the function of preventing Class name duplication, a question is raised. Can the unique identification of Class object be determined only by the fully qualified name?

The answer is no, because you cannot guarantee that classes with the same fully qualified name do not appear among multiple projects

The condition for the JVM to judge whether the two classes are the same is

  1. Same fully qualified name
  2. Loaded by the same class loader

Let's use the FileClassLoader written above to verify

String rootDir = "/Users/peng/study-code/java-learning/src/main/java";
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);

Class class1 = loader1.findClass("com.javashitang.classloader.DemoObj");
Class class2 = loader2.findClass("com.javashitang.classloader.DemoObj");

// false
System.out.println(class1 == class2);

The distribution of data area during operation is as follows:

Tomcat defines many classloaders to realize application isolation

A Common ClassLoader is provided in tomcat, which is mainly responsible for loading the classes and Jar packages used by Tomcat and some classes and Jar packages common to applications, such as Catalina_ All classes and Jar packages in the home / lib directory.

Tomcat will create a unique Class loader for each deployed application, that is, WebApp ClassLoader, which is responsible for loading the Jar file under the WEB-INF/lib directory and the Class file under the WEB-INF/classes directory of the application. Since each application has its own WebApp ClassLoader, it can isolate different Web applications from each other and can't see the Class files used by each other. Even if the fully qualified names of classes under different projects may be equal, they can work normally.

When the application is hot deployed, the original WebApp ClassLoader will be discarded and a new WebApp ClassLoader will be created for the application.

Blog reference

JDBC destroys the parental delegation model
[0]https://www.jianshu.com/p/09f73af48a98
As soon as you see it, you can see the detailed explanation of ClassLoader in super detailed java
[1]https://blog.csdn.net/briblue/article/details/54973413
Deep understanding of Java classloader
[2]https://blog.csdn.net/javazejian/article/details/73413292
In depth analysis of Java ClassLoader mechanism (source level)
[3]http://www.hollischuang.com/archives/199
Detailed and in-depth analysis of the working mechanism of Java ClassLoader
[4]https://segmentfault.com/a/1190000008491597
In depth analysis of the principle of Java ClassLoader
[5]https://blog.csdn.net/xyang81/article/details/7292380
Deep understanding of ClassLoader in JVM
[6]https://juejin.im/post/5a27604d51882503dc53938d

Keywords: Java jvm Interview

Added by colbyg on Wed, 09 Mar 2022 09:56:50 +0200