深入理解JVM类加载机制

深入理解JVM类加载机制

注:本文基于jdk1.8


1.类加载器

1.1类加载器的分类

JVM中默认的类加载器有三种,分别是:

(1)引导类加载器bootstrapLoaser(C++负责实例化,C++实现)

(2)扩展类加载器ExtClassLoader(Launcher类负责实例化,Java实现)

(3)应用类加载器AppClassLoader(Launcher类负责实例化,Java实现)

        bootstrapLoader主要加载jre/lib包下的jar包,这些jar包都是JVM中比较核心的jar包,比如其中的rt.jar,我们常用的String,HashMap等就在这个jar包下,也包括下面着重要说的Launcher类。ExtClassLoader主要加载jre/lib/ext包下的jar包。剩下的类比如我们导入的jar包中的类或者是自己写的类默认都是由AppClassLoader加载。这一点可以通过下面一段代码验证:

public class ClassLoaderTest {
	class Apple {
		int weight;
	}

	public static void main(String[] args) {
		System.out.println("String类的类加载器:" + String.class.getClassLoader());
		System.out.println("DNSNameService类的类加载器:" + DNSNameService.class.getClassLoader());
		System.out.println("Apple类的类加载器:" + Apple.class.getClassLoader());
		System.out.println("----------------------分割线----------------------");
		URL[] urls = Launcher.getBootstrapClassPath().getURLs();
		System.out.println("bootstrapLoader加载路径如下:");
		for (URL url : urls) {
			System.out.println(url);
		}
		System.out.println("----------------------分割线----------------------");
		System.out.println("extClassLoader加载路径如下:" + System.getProperty("java.ext.dirs"));
		// appClassLoader加载路径较长,建议复制到txt文本中查看
		System.out.println("appClassLoader加载路径如下:" + System.getProperty("java.class.path"));
		System.out.println("系统默认的类加载器:" + ClassLoader.getSystemClassLoader());
	}
}

输出结果如下:

        String类的类加载器为null是因为bootstrapLoader是由C++实现的,Java中无法看到;DNSNameService是jre/lib/ext/dnsns.jar包中的类。除此以外,我们还看到系统默认的类加载器就是appClassLoader,也说明除了jre中的包,其他jar包或者说其中的java类默认都是由appClassLoader加载的。

1.2类的加载过程,以com.xx.A为例

加载流程图如下:

        其中左侧黄色背景的部分是由JVM底层C++实现的,这里不做赘述,而右边蓝色背景的则是Java实现,需要重点关注一下。bootstrapLoader(也就是引导类加载器)会加载Launcher类,之后由Launcher类负责初始化ExtClassLoader和AppClassLoader,我们来看下Launcher类的源码(截取了重要的部分):

public class Launcher {
    // 这里保证了Launcher是单例的
    private static Launcher launcher = new Launcher();
    // 这个loader就是appClassLoader
    private ClassLoader loader;

    public static Launcher getLauncher() {
        return launcher;
    }

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

        try {
            // 初始化appClassLoader,这里将extClassLoader作为参数传入了,后面会说明传入的原因
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
    }

    public ClassLoader getClassLoader() {
        return this.loader;
    }
}

        通过源码,我们知道Launcher类是一个单例的类,并且会初始化extClassLoader和appClassLoader,最后向外界提供了获取AppClassLoader的接口,extClassLoader因为不会给外界用了所以不用提供接口。至于初始化appClassLoader时将extClassLoader作为参数传入是为了设置appClassLoader的父加载器为extClassLoader,后面的双亲委派机制会详细说用,而extClassLoader实际上是将父加载器设置为了null,所以extClassLoader是没有父加载器的,更确切的说父加载器是bootstrapLoader,因为bootstrapLoader在java中拿不到所以设置为了null。

2.双亲委派机制

        所谓双亲委派机制就是子加载器在加载一个类时,会先请求父加载器加载,如果父加载器没有加载成功,再由自己加载,具体如下图所示:

        需要注意的是箭头指示表示extClassLoader是appClassLoader的父加载器,而不是父类。事实上extClassLoader和appClassLoader都继承自URLClassLoader,其继承关系图如下:

        并且默认情况下所有自定义的类加载器的父加载器都是appClassLoader。这是因为所有的类加载器都是ClassLoader类的子类,初始化时会默认调用ClassLoader类的实例化方法,具体代码如下图所示:

public abstract class ClassLoader {
    private final ClassLoader parent;

    protected ClassLoader() {
        // 上面的代码验证过getSystemClassLoader返回的是appClassLoader的实例
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

    private ClassLoader(Void unused, ClassLoader parent) {
        // 这里设置了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;
        }
    }
}

        同理,还记得创建appClassLoader实例时传入了extClassLoader,但是创建extClassLoader实例时什么都没有传入吗,如果你跟一下源代码,就会发现最终还是会调到这个方法,只是extClassLoader传入的parent是一个null值。

3.loadClass()方法详解

        appClassLoader的loadClass方法最终会调用到ClassLoader的loadClass方法,那来看看ClassLoader类的loadClass方法以及另外两个重要方法findClass和defineClass源码:

public abstract class ClassLoader {
  
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 从已经加载过的类中按name查找
            Class<?> c = findLoadedClass(name);
            // 如果没有加载过则请求父加载器加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 只有extClassLoader的父加载器是null,所以让bootstrapLoader加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    // 自己加载名为name的类
                    c = findClass(name);

                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

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

    @Deprecated
    protected final Class<?> defineClass(byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(null, b, off, len, null);
    }

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

    protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        int len = b.remaining();

        // Use byte[] if not a direct ByteBufer:
        if (!b.isDirect()) {
            if (b.hasArray()) {
                return defineClass(name, b.array(),
                                   b.position() + b.arrayOffset(), len,
                                   protectionDomain);
            } else {
                // no array, or read-only array
                byte[] tb = new byte[len];
                b.get(tb);  // get bytes out of byte buffer.
                return defineClass(name, tb, 0, len, protectionDomain);
            }
        }

        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }

    private native Class<?> defineClass0(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd);

    private native Class<?> defineClass1(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd, String source);

    private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
                                         int off, int len, ProtectionDomain pd,
                                         String source);
}

        从代码可以看出双亲委派机制主要是在loadClass方法中实现的,所以想要打破双亲委派机制的话只要重写loadClass方法,发现类没有加载过时,直接调用findClass加载即可。不过这里的findClass方法只是抛出了一个异常,原因在于ClassLoader是一个抽象类,findClass方法会由子类重写。上面不是说过appClassLoader是继承自URLClassLoader吗,那我们看看URLClassLoader中的findClass方法:

public class URLClassLoader extends SecureClassLoader implements Closeable {
    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                // 调用defineClass方法真正去加载类
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }
}

        findClass方法最终调用了defineClass方法去加载类,这下也明白defineClass方法的作用了,那继续跟到defineClass方法就可以知道具体的类加载过程了,不过很遗憾,defineClass是通过调用native方法加载类的,已经脱离Java的范畴了,那就通过文字了解一下具体的加载过程吧:

1.加载:通过磁盘IO将.class文件加载到内存中

2.验证:加载到.class文件之后总得验证一下是不是真的.class文件吧,或者是不是有错误

3.准备:给静态变量分配内存并赋初始值(Java默认的0,false等)

4.解析:静态方法和静态变量的内存地址通常不会改变了,所以对它们的符号引用可以替换为直接引用,这个过程也叫静态链接

5.初始化:给静态变量赋予程序指定的初始值并且执行静态代码块(所以静态代码块是在类加载时就执行了)

总算是讲完整个Java类加载过程了,如有错误,请在评论中指出,感谢指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值