Jvm类加载机制

类的运行过程

以一个main方法举例:
image

类加载的具体流程为:

image
1.加载:把class字节码文件通过类加载器加载到内存中

2.验证:校验字节码文件是否符合jvm规范

3.准备:给静态变量赋初始值
比如8种基本类型的初值,默认为0;引用类型的初值则为null;常量的初值即为代码中设置的值,final static tmp = 456, 那么该阶段tmp的初值就是456

4.解析:将符号引用转为直接引用
符号引用。即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用。可以理解为一个内存地址,或者一个偏移量。比如类方法,类变量的直接引用是指向方法区的指针;而实例方法,实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量
举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。
如果在类加载期间解析的则为“静态链接”,代码运行到相应代码行才解析的则为“动态链接”。
举个例子:

点击查看代码
public static void main(String[] args) { 
Math math = new Math();
math.compute();
}

其中main方法是在类加载时放到内存中的,为静态链接;math.compute()是运行到这一行的时候才加载的,为动态链接。

5.初始化:对类的静态变量初始化为指定的值,执行静态代码块
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

注意:java中的类加载为懒加载,一个jar包启动的时候不会加载所有的类,他只有用到了才会加载。
举个例子:

点击查看代码
public class Main4 {


    static {
        System.out.println("*************load TestDynamicLoad************");
    }

    public static void main(String[] args) {
        new A();
        System.out.println("*************load test************");
        B b = null;
    }
}

class A {
    static {
        System.out.println("*************load A************");
    }

    public A() {
        System.out.println("*************initial A************");
    }
}

class B {
    static {
        System.out.println("*************load B************");
    }

    public B() {
        System.out.println("*************initial B************");
    }

}

运行结果:

点击查看代码
*************load TestDynamicLoad************
*************load A************
*************initial A************
*************load test************

如果将main方法中的B b = null;修改为B b = new B();,运行结果会变为:

点击查看代码
*************load TestDynamicLoad************
*************load A************
*************initial A************
*************load test************
*************load B************
*************initial B************

类加载器与双亲委派

类加载器

java中的类加载器有引导类加载器、拓展类加载器、应用程序类加载器与自定义类加载器;
1.引导类加载器:负责加载位于jre的lib目录下的核心类库;
2.拓展类加载器:负责加载位于jre的lib目录下ext目录下的jar包;
3.应用程序类加载器:负责加载ClassPath路径下的类包(自己写的类);
4.自定义类加载器:负责加载用户自定义路径下的类包。
image

双亲委派

双亲委派的具体流程如下图,在加载某个类时,首先会从AppClassLoader中判断是否加载过此类,如果没有加载,则委托父类加载器去判断;如果都没加载过,则由父类先判断是否可以加载,父类可以加载则由父类加载,否则交给子类去加载。
双亲委派机制简单说就是优先由父类去加载,父类加载不了再由子类加载。
image

这样的好处是:
1.沙箱安全:自己写的类不会覆盖核心库中的同名类,例如自己写个java.lang.String,然后你想篡改它的实现,由于BootstrapClassLoader中已经有同名类,因此不会被加载,从一定程度防止了危险代码的注入。
2.避免类的重复加载;

全盘负责委托机制
ClassLoader加载类用的是全盘负责委托机制,“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类通常也由这个ClassLoder载入。

实现代码如下:

点击查看代码
//ClassLoader的loadClass方法,里面实现了双亲委派机制
    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 { //如果当前加载器父加载器为空则委托引导类加载器加载该类
                        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();
                    //都会调用URLClassLoader的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;
        }
    }


打破双亲委派

首先看自定义类加载器:自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,一般来说我们自定义类加载器主要是重写findClass方法,如果想打破双亲委派,则需要我们覆写loadClass方法。

观察上面双亲委派的代码可知,只要删除if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); }即可打破双亲委派。

实现代码如下:

点击查看代码
    /**
     * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
     *
     * @param name
     * @param resolve
     * @return
     * @throws ClassNotFoundException
     */
    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) {
                // 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.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

posted @ 2022-03-10 22:22  君子酱  阅读(354)  评论(0编辑  收藏  举报