美文网首页
Java类加载机制

Java类加载机制

作者: Coding小聪 | 来源:发表于2019-09-24 00:54 被阅读0次

    1. 引言

    我们日常开发的Java代码都是保存在以.java为后缀的文件当中。想要执行java代码的话,首先需要将.java文件编译成以.class为后缀的字节码文件,然后类加载器将.class字节文件加载到JVM当中,最后在JVM中运行我们编写的代码。

    2. 类的加载

    2.1. 触发类加载的时机

    在代码运行过程中,使用到哪个类就会触发哪个类的加载。具体来说,当遇到以下6种情况会触发:

    1. 通过new关键字创建对象;
    2. 访问类的静态变量;
    3. 访问类的静态方法;
    4. 对某个类进行反射,如Class.forName("")
    5. 加载子类时会导致父类的加载;
    6. 包含main方法的启动类。

    当首次遇到上面6中情况之时,会导致类的加载(包含类加载的一系列过程)。

    2.2. 类加载的过程

    类的加载主要包含三个阶段:加载、连接、初始化,其中连接阶段分为:验证、准备、解析。


    各个阶段主要的工作如下
    • 加载:查找并加载class文件到 JVM内存;
    • 验证:检查class文件是否符合JVM的规范;
    • 准备:1.给类以及类中的静态变量分配内存空间;2. 给静态变量设置默认的初始值
    • 解析:将符号引用替换成直接引用,即内存地址;
    • 初始化:执行静态代码块,并给静态变量赋值

    public static Integer num=10 类变量num在准备阶段的值会设置成0,初始化阶段才会设置为10。

    加载阶段可以和连接阶段交叉执行,即在加载阶段开始之前,验证阶段开始进行验证。

    class被加载之后的内存情况,如下图所示:

    class被加载后的内存情况

    3. 类加载器

    JVM为我们提供了三大内置类加载器,不同的类加载器负责将不同的类加载的JVM内存中。三大内置加载器分别是:

    • BootstrapClassLoader(根类加载器);
    • ExtClassLoader(扩展类加载器);
    • ApplicationClassLoader(应用类加载器);

    3.1. 根类加载器

    根类加载器是最顶层的类加载器,没有父类。它是用C++编写的,主要用来加载JDK安装目录下jre/lib/中用于支撑Java系统运行的核心类库,例如:java.lang包下的类。

    可以通过-Xbootclasspath来指定根加载器的路径,也可以通过系统属性来得知当前JVM的根加载器都加载了哪些资源,如以下程序:

    public class BootStrapClassLoaderTest {
        public static void main(String[] args) {
            /**
             * 输出 Bootstrap:null
             * 根类加载器获取不到引用,所以打印出来为null
             */
            System.out.println("Bootstrap:"+String.class.getClassLoader());
            System.out.println(System.getProperty("sun.boot.class.path"));
        }
    }
    

    3.2. 扩展类加载器

    扩展类加载器主要用于加载JAVA_HOME下的jre/lib/ext子目录下的类库,它的父类是根类加载器。扩展类加载器是由纯Java代码实现的,它的完整类名是sun.misc.Launcher$ExtClassLoader。扩展类加载器所加载的路径,可以通过java.ext.dir系统属性获得。

    3.3. 系统类加载器

    系统类加载器主要负责classpath下的类资源加载,我们开发过程中所依赖的第三方jar包默认就是系统类加载器加载的。系统类加载器的父类是扩展类加载器,其类全名是sun.misc.Launcher$AppClassLoader。系统类加载器的加载路径一般通过-classpath或者-cp指定,同样也可以通过系统属性java.class.path进行获取。实例代码如下:

    public class ApplicationClassLoaderTest {
    
        public static void main(String[] args) {
            System.out.println("classloader: "+ApplicationClassLoaderTest.class.getClassLoader());
            System.out.println(System.getProperty("java.class.path"));
        }
    }
    

    3.4. 自定义类加载器

    除了上面介绍的三大内置类加载器,我们还能根据需求实现自己的类加载器。所有的自定义类加载器都需要直接或者间接继承ClassLoader类,这个类是一个抽象类,但是没有抽象方法,但是其中的findClass(String name)方法必须得重写,因为其默认实现会抛出一个异常。

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

    下面的代码,演示了如何自定义类加载器

    public class MyClassLoader extends ClassLoader{
        private final static Path DEFAULT_CLASS_DIR = Paths.get("/Users/classloader");
    
        private final Path classDir;
    
        public MyClassLoader () {
            super();
            this.classDir = DEFAULT_CLASS_DIR;
        }
    
        public MyClassLoader (String classDir) {
            super();
            this.classDir = Paths.get(classDir);
        }
    
        public MyClassLoader (String classDir, ClassLoader parentClassLoader) {
            super(parentClassLoader);
            this.classDir = Paths.get(classDir);
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
    
            byte[] classBytes = this.readClassBytes(name);
    
            if (null == classBytes || classBytes.length == 0 ){
                throw new ClassNotFoundException("Can not load the class "+name);
            }
    
            return this.defineClass(name,classBytes,0,classBytes.length);
        }
    
        private byte[] readClassBytes(String className) throws ClassNotFoundException {
            String classPath = className.replace(".","/");
            Path classFullPath = classDir.resolve(Paths.get(classPath+".class"));
            if (!classFullPath.toFile().exists()) {
                throw new ClassNotFoundException("The class "+className+" not found");
            }
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                Files.copy(classFullPath,baos);
                return baos.toByteArray();
            } catch (IOException e) {
                throw new ClassNotFoundException("load the class "+className+" occur error");
            }
        }
    }
    

    使用自定义类加载器来加载类:

    private static void testMyClassLoader() throws Exception {
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> aClass = classLoader.loadClass("thread.classloader.Hello");
        System.out.println(aClass.getClassLoader());
    
        Object hello = aClass.newInstance();
        System.out.println(hello);
    
        Method method = aClass.getMethod("hello", String.class);
        String result = (String) method.invoke(hello, "tomcat");
        System.out.println("Result:"+result);
    }
    

    完整的代码请访问:https://github.com/codingXcong/Java-Guide/tree/master/Thread/src/main/java/thread/classloader

    上面的示例中,我们通过自定义类加载器对Hello.java进行加载,运行示例的时候,需要先将java文件编译成字节码文件,然后将字节码文件放入MyClassLoader类中指定的classDir路径下。
    PS:如果在IDEA环境中,还需要将Hello.java文件删除,不删除的话,根据类加载器的双亲委派模型,会有MyClassLoader的父类加载器对Hello类进行加载。

    3.5. 双亲委派模式

    JVM的类加载器是有亲子层级结构的,如下图所示:

    基于这个亲子层级结构,有个双亲委派的机制。在进行类加载的时候,当一个类被调用loadClass之后,它并不会直接去加载,而是交给当前类加载器的父加载器尝试加载,直到传递到最顶层的父加载器。

    例如,现在JVM需要加载A类,此时系统类加载器会问问自己的爸爸,也就是扩展类加载器,你能加载到A类吗?

    然后扩张类加载器会直接问自己的爸爸,启动类加载器,你能加载A类吗?

    启动类加载器在Java安装目录下没有找到这个类,就告诉扩张类加载器,我没法加载这个类,你自己加载去吧。

    扩展类加载器就尝试自己加载,它在jre/lib/ext下也没找到这个类,就会通知系统类加载器,没有加载到类A,你自己去加载。

    最后系统类加载器在自己负责加载的范围内找到了类A,然后就将其加载到内存中。

    为啥要设计成双亲委派模式么?防止同一个类被重复加载。

    我们再通过源码来看看双亲委派的工作方式,其逻辑主要封装在ClassLoader.loadClass(String name)中:

        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
                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
                    }
    
                    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
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    

    相关文章

      网友评论

          本文标题:Java类加载机制

          本文链接:https://www.haomeiwen.com/subject/eypjyctx.html