搞懂ClassLoader

1. 类加载器

自上而下有如下:

  1. BootstrapClassLoader 启动类加载器,主要加载核心类库,jre/lib下的rt.jar、resources.jar、charsets.jar和classes文件等。可以通过-Xbootclasspath改变其路径。
  2. ExtentionClassLoader 扩展类加载器,加载目录jre/libext目录下的jar包和class文件。可以通过-Djava.ext.dirs指定目录。
  3. AppClassLoader 应用类加载器,加载当前应用的classpath的所有类。默认情况下也作为系统类加载器ClassLoader.getSystemClassLoader(),通过-Djava.system.class.loader 修改。
  4. UserClassLoader用户自定义的类加载器

2. 双亲委派

什么是双亲委派?
虚拟机使用类加载器加载类,但是如上所示有很多类加载器。当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载(比如 BootstrapClassLoader 去加载rt.jar的类)。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。

那么双亲委派是如何实现的?需要从下面方法找答案

public abstract class ClassLoader {
	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 { // 从下文可以知道,ExtClassLoader 会走这里
	                    c = findBootstrapClassOrNull(name);
	                }
	            } catch (ClassNotFoundException e) {
	                // ClassNotFoundException thrown if class not found
	                // from the non-null parent class loader
	            }
	
	            if (c == null) { 
	            	// 从父加载器没加载到,才调用当前加载器的 findClass 加载类
	            	// 具体实现参考 URLClassLoader 的 findClass
	                long t1 = System.nanoTime();
	                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();
	            }
	        }
	        if (resolve) {
	            resolveClass(c);
	        }
	        return c;
	    }
	}
}

由源码可以看到,AppClassLoader、ExtClassLoader都是继承于URLClassLoader,和 BootstrapClassLoader 没什么关系。那这里的parent又是指什么呢?

public abstract class ClassLoader {
	// 父加载器
	private final ClassLoader parent;
	
	public final ClassLoader getParent() {
	    if (parent ==null)
	        return null;
	    SecurityManager sm = System.getSecurityManager();
	    if (sm != null) {
	        // Check access to the parent class loader
	        // If the caller's class loader is same as this class loader,
	        // permission check is performed.
	        checkClassLoaderPermission(parent, Reflection.getCallerClass());
	    }
	    return parent;
	}
}

由代码可以知道,这里的父加载器不是继承关系,不是父类,而是当前类加载器的一个属性。

public class Test {
    public static void main(String[] args) {
        ClassLoader classLoader = Test.class.getClassLoader();

        System.out.println(classLoader);
        System.out.println(classLoader.getParent());
        System.out.println(classLoader.getParent().getParent());
    }
}

输出如下:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@7506e922
null

这里为什么获取不到 BootstrapClassLoader ?先看看parent是如何赋值的

public abstract class ClassLoader {
	private ClassLoader(Void unused, ClassLoader parent) {
	    this.parent = parent;
		......
	}
}

Launcher 作为入口,在构造函数中创建了ExtentionClassLoader和AppClassLoader

public class Launcher {
	private static Launcher launcher = new Launcher();
	private static String bootClassPath =
        System.getProperty("sun.boot.class.path"); // -Xbootclasspath
	private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        extcl = ExtClassLoader.getExtClassLoader();
		loader = AppClassLoader.getAppClassLoader(extcl);
        
        // Also set the context class loader for the primordial thread.
        // 设置AppClassLoader为线程上下文类加载器
        Thread.currentThread().setContextClassLoader(loader);
		......
    }
}

继续看 ExtClassLoader 的创建

static class ExtClassLoader extends URLClassLoader {
	public static ExtClassLoader getExtClassLoader() throws IOException
    {
            final File[] dirs = getExtDirs(); // 获取目录

            try {
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() {
                        public ExtClassLoader run() throws IOException {
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs); // 创建,往下看构造函数
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                throw (IOException) e.getException();
            }
        }
	
	public ExtClassLoader(File[] dirs) throws IOException {
           super(getExtURLs(dirs), null, factory);
           SharedSecrets.getJavaNetAccess().
               getURLClassPath(this).initLookupCache(this);
       }

	private static File[] getExtDirs() {
		   // 通过系统变量 java.ext.dirs 获取需要加载的目录
           String s = System.getProperty("java.ext.dirs");
           File[] dirs;
           if (s != null) {
               StringTokenizer st =
                   new StringTokenizer(s, File.pathSeparator);
               int count = st.countTokens();
               dirs = new File[count];
               for (int i = 0; i < count; i++) {
                   dirs[i] = new File(st.nextToken());
               }
           } else {
               dirs = new File[0];
           }
           return dirs;
       }
       
}

往上看构造函数

public class URLClassLoader extends SecureClassLoader implements Closeable {
	public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        acc = AccessController.getContext();
        ucp = new URLClassPath(urls, factory, acc);
    }
}

public class SecureClassLoader extends ClassLoader {
	protected SecureClassLoader(ClassLoader parent) {
	    super(parent);
	    // this is to make the stack depth consistent with 1.1
	    SecurityManager security = System.getSecurityManager();
	    if (security != null) {
	        security.checkCreateClassLoader();
	    }
	    initialized = true;
	}
}

public abstract class ClassLoader {

	protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
	
	private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }
}

super(getExtURLs(dirs), null, factory);可以很明显的看到 ExtClassLoader 给parent传的值就是null,所以也解释了为什么空指针。而且由于 BootstrapClassLoader 是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类。

由下面代码可以知道 AppClassLoader 设置了父加载器为 extcl 也就是 ExtClassLoader。

static class AppClassLoader extends URLClassLoader {
	public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
        	// 加载工作目录
            final String s = System.getProperty("java.class.path");
			return new AppClassLoader(urls, extcl);
        }
}

说了这么多,双亲委派有哪些好处?

  1. 可以避免类的重复加载,因为都会先去看看父类有没有加载,加载就直接返回了。
  2. 安全性,比如 java.lang.String 只会由Bootstrap ClassLoader加载,可以防止代码被别人随意改动。

3. 加载器的使用

类加载器有几个比较重要的方法

  • loadClass:参考上文代码,双亲委派机制在里面实现,当已经就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中
  • findClass:根据名称或位置来加载.class字节码,然后通过defineClass返回class 参考 URLClassLoader
  • definclass:根据二进制数据加载class

当我们需要自定义ClassLoader的时候,可以先考虑是否要打破双亲委派。
如果要打破则重写其loadClass,修改其先从parent获取class的逻辑。
如果不需要打破双亲委派,则可以重写findClass方法,实现具体逻辑即可。

自定义加载器有很多使用场景,下面就列举部分

  1. 热加载/热部署:项目启动时候,jvm虚拟机需要重新加载所有类数据,导致启动相对较慢,而一种热部署的思路就是,我只加载有变更的而不是所有,如spring boot devtools。
  2. 字节码加解密:我们的应用可能部署在很多地方,但是具体实现我们不希望被人了解细节,可以通过加解密字节码文件
  3. 依赖冲突:如果我们需要依赖A和B两个jar,但是他们都引用了第三方类库C。A引用C的v1版本,代码中使用了方法c1。B引用了C的v2版本,使用方法c2。这个时候,如果我们用maven管理,它会使用其中一个版本的C,如果是v1,没有c2方法,那么在使用B包中方法的时候就有可能导致NoSuchMethodException。这个时候可以让A和B两个jar提供方自定义类加载器,去加载不同版本的C,实现隔离,也就可以避免这种问题的发生。

4. 类加载过程

类的生命周期包括:加载、链接(验证、准备、解析、初始化)、使用、卸载7个阶段,其中前面4个作为其加载过程。

加载

  1. 通过一个类的全限定名来获取定义此类的二进制字节流,可以只jar、网络、动态代理创建等
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

链接

验证

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

准备

为类的静态变量(static修饰)分配内存,并初始化默认值(根据类型,null、false、0、0L,而非代码定义的值—因为值不一定已经初始化了)。
如果是static final同时修饰的,在javac编译时生成ConstantValue属性(可以理解为编译的时候已经放入常量池),这个时候直接赋值。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和符号引用进行。

符号引用:一个java类会变编译为字节码文件,在编译时,java类并不知道所引用类的实际地址(也就是直接引用),所以要用符号引用来代替,在编译完成后,类加载时,在解析这一步来将符号引用来转变为直接引用。
直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

对类变量进行初始化。初始化阶段是执行类构造器()方法的过程。

  • ()方法与类构造函数不一样,不需要显示调用父类构造函数,虚拟机会保证在子类的()方法执行之前,父类的()方法已执行完毕。
  • ()主要针对static静态代码块,如果没有则不会生成
  • 虚拟机保证多线程执行时,只会有一次的()
  • ()对象构造方法是在new对象时候加载的,比如直接使用类的静态属性就不会触发

你可能感兴趣的:(Java,java,开发语言)