JVM系列(1)深入浅出分析JVM类加载机制

2022-07-28,,,,

1.一个类从加载到运行的整体流程

 这里先用下面一个Demo类的加载到运行过程进行分析

public class Demo {
    public static final int count = 128;
    public static Test test = new Test();

    public int compute(){ 
        int x = 1;
        int y = 2;
        int z = (x + y) * 3;
        return z;
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.compute();
    }
}

当我们启动程序入口main函数时,首先需要通过类加载器把主类加载到JVM中,大体流程如下:

2.Demo类加载过程会经历以下过程:

加载:这个过程中JVM会在电脑硬盘上通过IO和该类的文件路径寻找对应的字节码文件,当用到该类时(如new其实例或者调用该类的main函数)就会加载该类,同时会在内存里产生一个代表该类的java.lang.Class对象,作为方法区(至于方法区是什么我会在后面写一篇关于JVM内存模型的文章详细阐述)该类的数据访问入口。

验证:主要确保字节码文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机的自身安全。

准备:为类的静态变量分配内存,并将其赋予默认值(此时为默认值,在初始化的时候才会给变量赋值),即在方法区中分配这些变量存放所需要的内存空间。

解析:该过程会将符号引用(由于在编译时java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替)替换为直接引用(指向引用类的实际内存地址的指针),该阶段会把一些静态方法(比如main()方法)替换为指向数据所存的指针或者句柄(直接引用),这也是在被称为静态链接过程。

初始化:对类的静态变量赋予指定的值,同时执行静态代码块。

注意点:类被加载到JVM的方法区中主要包含了类型信息、字段信息、方法信息、对应class实例的引用、运行时常量池、类加载器的引用等信息。

类加载器的引用:这个类加载器实例的引用;

对应class实例的引用:类加载器在加载该个类信息存到方法区后,还会创建一个这个类对应的Class类型的对象放到堆区(Heap),这么做相当于给了开发人员访问方法区中类定义的入口。

下面代码用于验证两点:(1)静态代码块在调用类构造器前执行;(2)主类在运行过程中若使用到其他类(如jar包),不会一次性全部加载,而是需要使用时才会加载。

public class TestClassLoad {

    static{
        System.out.println("TestClassLoad --- static块执行");
    }

   static class TestA{
        public TestA(){
            System.out.println("TestA --- TestA()执行");
        }
        static{
            System.out.println("TestA --- static块执行");
        }
    }

    static class TestB{
        public TestB(){
            System.out.println("TestB --- TestB()执行");
        }
        static{
            System.out.println("TestB --- static块执行");
        }
    }

    public static void main(String[] args) {
        new TestA();
        System.out.println("TestA 完毕, 接下来看TestB是否会加载");
        TestB testB = null;   // TestB不会加载,除非实例化
    }

}

运行结果:

3.类加载器和双亲委派机制

在Java中加载类所用到的类加载器有如下几种:

引导类加载器:用于加载支撑JVM运行的位于JRE的lib目录下的核心类库,如rt.jar、charsets.jar等

扩展类加载器:用于加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包

应用程序类加载器:用于加载ClassPath路径下的类包,主要就是加载开发者写的那些类

自定义加载器:用于加载用户定义路径下的类包

类加载器实例代码:

public class TestClassLoader {

    public static void main(String[] args) {
        System.out.println("Hello World!");
        System.out.println("引导类加载器:"+String.class.getClassLoader());
        System.out.println("扩展类加载器:"+ AccessBridge.class.getClassLoader().getClass().getName());
        System.out.println("应用类加载器:"+TestClassLoader.class.getClassLoader().getClass().getName());

        System.out.println("");

        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassLoader = appClassLoader.getParent();
        ClassLoader bootsrapClassLoader = extClassLoader.getParent();
        System.out.println("引导类加载器:"+ bootsrapClassLoader);
        System.out.println("扩展类加载器:"+ extClassLoader);
        System.out.println("应用类加载器:"+ appClassLoader);
        System.out.println("");
        System.out.println("引导类加载器加载的类文件如下:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0;i<urls.length;i++){
            System.out.println(urls[i]);
        }
        System.out.println("扩展类加载器加载的类文件如下:");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println("应用类加载器加载的类文件如下:");
        System.out.println(System.getProperty("java.class.path"));

    }

}

结果如下:

类加载器初始化过程:

类加载过程中需要创建JVM启动器实例sun.misc.Launcher,而这个sun.misc.Launcher初始化采用单例模式,进而保证了一个JVM虚拟机中只有一个启动器实例,通过阅读类加载器构造方法的源码,发现在Launcher构造方法内部创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。而JVM默认会使用Launcher的getClassLoader()方法返回类加载器AppClassLoader的实例用来加载我们的应用程序。

对Launcher加载器初始化源码进行分析:

// Launcher的构造函数 
public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            /**创建扩展类加载器,注意Launcher.ExtClassLoader.getExtClassLoader()
             *并未传入父加载器,这是因为扩展类加载器的父加载器是引导类加载器,
             *而引导类加载器是通过C++代码实现的,所以这里将其父加载器不做设置*/
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            // 1.创建应用类加载器,同时将父加载器扩展类加载器作为参数传入(传入原因是为了实现后面的双亲委派机制)
            // 2.这里的this.loader就是用来加载我们自己的应用程序的应用程序加载器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }

双亲委派机制

上诉这个图就是双亲委派机制流程图,当要加载一个类时,会先通过委托其父加载器进行加载目标类,当找不到目标类时会再向上更上层的父类加载器加载,如果所有父类加载器在自己的加载路径下都找不到需要加载的目标类,则会由该类自己的加载器在加载路径下查找并载入目标类。

例如:前面的Demo类,最开始会找应用程序类加载器加载,而应用程序类加载器不会自己直接加载,它会先委托扩展类加载器加载,扩展类加载器再会委托给引导类加载器加载,如果引导类加载器在自己的类加载路径里没有找到Demo类,则会再向下交给其子加载器加载(扩展类加载器)Demo类,若扩展类加载器也找不到Demo类则会再交给应用程序类加载器亲自在类路径下找Demo类并进行加载。

对双亲委派机制源码进行分析:

思路:通过阅读应用程序类加载器AppClassLoader加载类的源码发现它的loadClass()方法最终会调用其父类的ClassLoader的loadClass方法,(1)首先检查一波指定名称的类是否已经被加载过,若加载过了就不会再加载,直接返回。(2)若该类没有被加载,那么先判断该类的加载器是否有父类加载器,有父类加载器就会委托给父类加载器加载,若没有父类加载器就直接委托给引导类加载器加载该类。(3)若引导类加载器都没有加载成功,则交给应用程序类加载器亲自加载。

 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
                    //如果找不到类,则抛出ClassNotFoundException 从非空父类加载器
                    
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.

                    long t1 = System.nanoTime();
                    // 如果仍然找不到,调用当前类加载器的findClass()方法,以便找到该类。
                    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;
        }
    }

看起来设计双亲委派机制感觉有点多余(明明可以直接由当前类加载器直接加载目标类),但是为啥还需要双亲委派机制?

我打算用下面代码说明问题:

package java.lang;

public class Integer {
    public int var1 = -1;
    public static void main(String[] args) {
        System.out.println("我自己的Integer类");

    }
}

运行结果:

所以设计双亲委派机制的初衷如下:

(1)涉及到沙箱安全机制,假如开发者自己写了一个和JDK同名(类路径和类名一模一样)的类如java.lang.Integer.java,一旦类似这种同名类被加载问题就出现了,加载器不知道加载哪一个Integer类,有了双亲委派机制就能保证加载的是JDK自己的Integer,这也是JDK设计者为了防止核心类库随意或者恶意篡改。

(2)双亲委派避免了一个类被重复加载,父加载器加载过的目标类不会被子加载器再加载一次,保证了类加载的唯一性。

补充:全盘负责委托机制(是指当一个类加载器加载一个类时,如果不显示的指定另外一个加载器,那么该类所依赖及引用的类也会交由这个类的类加载器负责加载)

本文地址:https://blog.csdn.net/qq_40436854/article/details/109273781

《JVM系列(1)深入浅出分析JVM类加载机制.doc》

下载本文的Word格式文档,以方便收藏与打印。