• 类的加载classload和类对象的生成


    在Java中最重要的可以说就是类的加载了。不论我们编写的功能多么复杂,或是多么简单,永远逃离不开的,就是将这个类从class文件加载到JVM中来。

    类的加载过程

    首先我们要了解一下类的加载过程,包括:加载、连接(验证、准备、解析)、初始化、使用、卸载。

    加载:将根据类的全限定名找到对应的Class文件,将它加载进JVM中,并生成Class对象保存在堆中。

    连接:

      验证:检查加载进来的类信息是否满足我们JVM的规范。

      准备:对类中的静态变量分配内存空间,并赋予原始值。对常量直接赋予指定的值。

      解析:将类中的符号引用转变为直接引用。

    初始化:为类中的静态变量赋值,执行静态代码块。

    下面我们用一个类来验证一下:

    public class Main7 {
    
        private final int z = 6;
        private final static int k = 1;
        private static int i = 5;
        private int j = 2;
    
        static {
            i = 10;
        }
    
        {
            i = 11;
            j=3;
        }
    
        public static void main(String[] args) {
    
        }
    }

    如上,我们定义一个Main7类,并对类中的每一步都打上断点:

     

    然后点击debug运行:

    第一步:程序最先进入到第9行代码,此时查看我们最下面的静态成员中,k由于是final static,被直接赋予了我们给它指定的值1。而i由于只是一个static,它被先赋予了默认值0。至于我们其他的两个变量z和j,此时是没有被初始化的。

    注意:第八行代码在我们的程序运行中并没有被debug进入断点,但是实际上它是最先和i一起被初始化的。即说明,加载和连接两个步骤,是无法被我们的debug进入的。我们这里能进入断点的,也仅仅只是初始化步骤。(第九行之所以能进入是因为我们对i赋予了i=5,如果我们只定义static int i,则这行也不会被进入debug)

     

    第二步:执行静态代码块。

    第三步:结束。

    由于我们的main方法中并没有内容,因此我们不会创建任何自定义类的对象。Main7中的static变量与static代码块之所以会被初始化,也是因为这是作为main方法所在的类,会被加载进JVM。

    总结:由上面的结果可以总结如下几点:

    1.类只会在第一次被调用时候进行加载。这个调用包括Main方法所在的类、调用类中的静态成员变量、执行类的静态方法、通过反射创建对象、new一个对象、子类被初始化。

    2.类的加载不会初始化非static变量,也不会执行非static代码块。

    类加载器

    JDK默认给我们提供了三个类加载器:

    BootStrap ClassLoad:最顶级的类加载器,使用C++编写,由JVM启动,默认加载%JAVA_HOME%/lib下的jar包和类。

    Extension ClassLoad:扩展加载器,由BootStrap ClassLoad启动,父类加载器是BootStrap ClassLoad,默认加载%JAVA_HOME%/lib/ext下的jar包和类。

    Application ClassLoad:应用加载器,由BootStrap ClassLoad启动,父类加载器是Extension ClassLoad。默认加载classpath下的jar包和类。

    关系图如下:

    我们查看一个自定义类的加载类:

        public static void main(String[] args) {
            ClassLoader cl = Main7.class.getClassLoader();
            while (cl != null) {
                System.out.println(cl.toString());
                cl = cl.getParent();
            }
        }

    打印结果:

      sun.misc.Launcher$AppClassLoader@18b4aac2
      sun.misc.Launcher$ExtClassLoader@77459877

    可以看到,上面只打印出来了两个ClassLoader对象,一个是AppClassLoader,这是自定义类的加载器,另一个是ExtClassLoader,这是自定义类加载器的父类加载器。

    而我们再次通过getParent()方法获取ExtClassLoader对象的父类加载器时候,返回的结果等于null,因此跳出了循环。

    我们再尝试一个由BootStrap ClassLoader加载的类String,查看它的类加载类

        public static void main(String[] args) {
            ClassLoader cl = String.class.getClassLoader();
            System.out.println(cl == null ? "cl is null" : cl.toString());
        }

    打印结果:

    cl is null

    虽然BootStrap ClassLoader是ExtClassLoader的父类加载器,但是由于它是C++编写,因此在Java代码中,并没有任何的体现,如果一个类的类加载器是null,那么它就是由BootStrap ClassLoader启动。

    下面我们来检验一下上面的说法,首先利用类加载器的双亲委派机制来确认。

    双亲委派机制:类加载器加载一个类时,会先交由它的父类加载器加载,如果父类加载器的加载范围中有全限定名相同的类文件,则由父类加载器加载这个类,子类加载器不再加载。

    意即:自定义加载器加载一个类Aclass,首先交给父类加载器AppClassLoader,AppClassLoader再交由它的父类加载器ExClassLoader,ExtClassLoader再交由它的父类加载器BootStrapClassLoader,BootStrapClassLoader没有父类加载器,因此检查自己的加载范围%JAVA_HOME%/lib下有没有这个类,没有则再由ExtClassLoader检查它的加载范围%JAVA_HOME%/lib/ext下有没有这个类,没有则再有AppClassLoader检查classpath下有没有这个类,没有则再交由自定义类加载器去它的路径下加载。其中一旦有一个类加载器找到这个类的class文件,就会由这个类加载器进行加载,它的子类加载器而不会在进行处理。

    下面我们来查看一下类加载器(ClassLoader)的加载方法:

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

    它这里接收一个全限定的二进制类名,然后调用loadClass(name,false)方法

        // 这个protected可以看到,是允许我们通过自定义类加载器来重写这个方法。
        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);  //首先检查Class对象中是否已经包含了这个类。即这个类是否已经被加载过了。
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) { //首先判断父类加载器不等于null,也就是说父类加载器不是BootStrap ClassLoader
                            c = parent.loadClass(name, false); //交由父类加载器加载。双亲委派机制
                        } else {
                            c = findBootstrapClassOrNull(name);  //否则的话,从BootStrapClassLoader中查找该类
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) { //父类加载器中没有对应的class文件
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        c = findClass(name);  //调用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;
            }
        }

    从上面的代码里面可以看到,ClassLoader中明确指定了,当parent == null时,会调用findBootstrapClassOrNull(String)方法从BootStrapClassLoader中加载相关的类。

    而且从ClassLoader的源码中我们也可以看到,如果我们要自定义自己的类加载器,只要继承ClassLoader,并重写loadclass(String)或findClass(String)方法即可。

    下面我们来查看一下ClassLoader自带的findClass(String)方法。

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

    可见,ClassLoader在这里采用了模版方法模式,将详细的加载类文件的方法交由它的子类去实现。

    我们定义一个自定义类加载器

    public class MyClassLoader extends ClassLoader {
        // 加载器名称
        private String name;
        // 加载器的加载路径
        private String path;
    
        public MyClassLoader(String name, String path) {
            super();  // 采用默认的父类加载器,即为调用它的类的类加载器。一般是appClassLoader
            this.name = name;
            this.path = path;
        }
    
        public MyClassLoader(ClassLoader classLoader, String name, String path) {
            super(classLoader); //指定父类加载器
            this.name = name;
            this.path = path;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] bytes = readClassFile(name); //根据名称找到文件,并转为字节数组
            return super.defineClass(name, bytes, 0, bytes.length);  //将字节数组转为Class对象。
        }
    
        private byte[] readClassFile(String name) {
            byte[] bytes = null;
            InputStream is = null;
    
            String filename = path + "/" + name.replaceAll("\.", "/") + ".class";
    
            File file = new File(filename);
    
            ByteArrayOutputStream os = new ByteArrayOutputStream();
    
            try {
                is = new FileInputStream(file);
                int tmp = 0;
                while ((tmp = is.read()) != -1) {
                    os.write(tmp);
                }
                bytes = os.toByteArray();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (os != null) {
                    try {
                        os.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            return bytes;
        }
    
    }

    我们定义一个用来被加载的测试类,将它编译后的class文件放在D://tmp目录下

    public class Main5 {
    
        Main5() {
            System.out.println("Main5:" + this.getClass().getClassLoader().toString());
        }
    
    }

    客户端调用代码

    public class Main11 {
    
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            MyClassLoader mcl = new MyClassLoader("yxf", "d://tmp");
            Class c = mcl.loadClass("Main5");
            c.newInstance();
        }
    }

    输出结果:

    Main5:main11.MyClassLoader@5b2133b1

     可见打印出来的类加载器正是我们自定义的MyClassLoader。

    假如我们在classpath下同样也存放一个Main5的class对象。

    再次运行我们的客户端调用代码,输出结果:

    Main5:sun.misc.Launcher$AppClassLoader@18b4aac2

     这一次由于双亲委派机制,我们Main5类被加载时使用的是AppClassLoader。

    我们修改一下客户端代码:

    public class Main11 {
    
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
            MyClassLoader mcl = new MyClassLoader(null, "yxf", "d://tmp"); //指定了父类加载器null,即BootStrapClassLoader
            Class c = mcl.loadClass("Main5");
            c.newInstance();
        }
    }

    再次运行,输出结果:

    Main55:main11.MyClassLoader@5b2133b1

     这一次是由于我们给自定义的类加载器指定了它的父类加载器BootStrapClassLoader,因此,即使我们在classpath下存放了一个Main5.class,也不会调用到AppClassLoader中去。

  • 相关阅读:
    iOS MVC <模型-视图-控制器>
    iOS interface building 大纲视图内容
    循序渐进VUE+Element 前端应用开发(28)--- 附件内容的管理 (转载)
    循序渐进VUE+Element 前端应用开发(27)--- 数据表的动态表单设计和数据存储(转载)
    ABP框架中一对多,多对多关系的处理以及功能界面的处理(2)(转载)
    ABP框架中一对多,多对多关系的处理以及功能界面的处理(1)(转载)
    电商商品数据库的设计和功能界面的处理 (转载)
    循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2)(转载)
    循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1)(转载)
    循序渐进VUE+Element 前端应用开发(24)--- 修改密码的前端界面和ABP后端设置处理(转载)
  • 原文地址:https://www.cnblogs.com/yxth/p/10923392.html
Copyright © 2020-2023  润新知