JVM专题系列——类装载器代码级介绍;手撸一个简易热加载器

概述

  本篇博客来介绍一下类装载器,即ClassLoader,它跟我们程序的运行密切相关,通过了解其装载流程和工作机制有利于我们对Java本身有更加深刻的理解。

装载验证流程

加载

  加载是装载类的第一个阶段,它会取得类的二进制流,将字节码信息存储在元空间(JDK1.8)之中,并在Java堆内存中生成对应的Class对象(Class对象还不完整,所以此时的类还不可用)。

链接

  链接分为验证,准备和解析三步。

验证

  验证阶段用一句话来说:保证Class流的格式是正确的(例如文件格式验证:是否以0xCAFEBABE开头等)。

准备

  分配内存,设置初始值(元空间中)。对于准备阶段,要注意特殊情况,如下代码:

//父类
public class SuperClass {
    //静态变量value
    public static String value = "我被加载了";

    //静态块,父类初始化时会调用
    static {
        System.out.println("父类初始化!");
    }
}

//子类
class SubClass extends SuperClass {
    //静态块,子类初始化时会调用
    static {
        System.out.println("子类初始化!");
    }
}

//主类、测试类
class NotInit {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

  运行结果:
结果
  所以对于类中的静态变量在类初始化的时候才会被加载,并不会提前设置初始值。
  别急着下结论,再看如下代码:

//父类
public class SuperClass {
    //静态变量value
    public final static String value = "我被加载了";

    //静态块,父类初始化时会调用
    static {
        System.out.println("父类初始化!");
    }
}

//子类
class SubClass extends SuperClass {
    //静态块,子类初始化时会调用
    static {
        System.out.println("子类初始化!");
    }
}

//主类、测试类
class NotInit {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

  运行结果:
结果
  这说明被final修饰的静态变量在准备阶段就会赋值,所以访问它的时候对应的类并没有初始化。

解析

  符号引用替换为直接引用:把引用加载到内存当中,具备其相应的指针。符号引用是一种表现形式,直接引用则是指针,地址偏移量。

初始化

  执行类构造器,子类构造器调用前会首先调用父类的构造器(需要先拿到继承父类的相关信息),初始化过程是原子性的,线程安全的。

类加载器ClassLoader

双亲委派机制

  ClassLoader是一个抽象类,其实例可以读入Java字节码将类装载到JVM当中,并且ClassLoader可以定制,用来满足不同的字节码流的获取方式,它负载类装载过程中的加载阶段。
  ClassLoader主要有四大类,分别是:
  1.BootStrap ClassLoader(启动ClassLoader)
  2.Extension ClassLoader(扩展ClassLoader)
  3.App ClassLoader(应用ClassLoader)
  4.Custom ClassLoader(自定义ClassLoader)
  从下到上依次继承,类加载流程如下图:
加载流程
  如果我们没有自定义类加载器,那么在运行应用程序的时候则默认使用App ClassLoader对类进行加载。例如要加载一个名为Test的类,首先默认会去App ClassLoader中查找,没有,去Extension ClassLoader查找,再没有,去Bootstrap ClassLoader查找,如果都没有,则会自顶向下尝试加载此类,如果都加载不到,才会报出ClassNotFoundException异常。这种加载机制被称为 双亲委派机制 ,但讲道理,我感觉叫父类委派机制更容易让人接受和理解。看一下JDK源码,一目了然,儿子没有找父亲,递归从父类依次查找,如果查到Bootstrap还不存在,调用findBootstrapClassOrNull方法,findBootstrapClassOrNull调用native方法findBootstrapClass自顶向下尝试加载:
双亲委派

证明类是自顶向下加载的

  执行以下代码,可想而知,结果必然为I am bootStrap,拿到它的class字节码文件,放入D盘的testClass目录下:
1
2
  然后修改代码为:
3
  添加启动参数:-Xbootclasspath/a:D:/testClass,-Xbootclasspath/a表示在默认的基础上添加,a表示append:
添加参数
  运行,结果如下:
运行结果
  添加了启动参数之后,结果依然为hello,I am bootStrap,这是因为运行main方法的时候由于是第一次运行,AppClassLoader中并没有加载此类,所以去父类中寻找,父类中自然也没有加载,都找不到,于是尝试从BootStrap Loader开始加载,而在BootStrap Loader的classpath里有这个类存在,所以BootStrap Loader加载此类并返回,所以最终结果是hello,I am bootStrap,这样进一步证明了类是自顶向下加载的。(注意不要使用maven项目测试,会有问题,一个简易的java Application程序即可)

破坏双亲委派机制

  双亲委派机制是JVM类加载的默认机制,但它并不是必须的,比较典型的例如Tomcat的WebappClassLoader就会先加载自己的Class,找不到之后才会去委托parent加载,也就是自底向上加载。还有我们平时常见的热加载(比如idea中的热加载,修改了代码不必重启),类发生变化的时候需要被监测到,很明显默认JVM是无法做到的,需要我们自己定义一个CLassLoader扫描变化,动态加载,这其实都是对双亲委派机制的破坏。

案例:手撸一个热加载器

  我们通过代码可以让我们更好的理解如何去破坏双亲委派机制。
  需求如下:监控Worker类的变化,如果有改动则重新加载此类调用其getVersion方法输出当前版本。
  Worker类代码如下:

public class Worker {
	public void getVersion(){
		System.out.println("version:A");
	}
}

  简单分析一下:如果要时刻获取Worker的变化,需要定时检测Worker.java的变化,Worker.java若发生变化可以理解为Worker.class发生变化,class文件发生变化需要使用类加载器去加载并返回Worker对象,自带的类加载器由于之前已经加载过,所以再次调用loadClass方法获取的还是之前装配的对象,显然不能满足我们的需求,所以需要我们去自定义类加载器,重写loadClass方法,打破双亲委派机制,从而去动态加载变化后的Worker.class
  这样一来,思路清晰了:
  1.自定义类加载器:重写loadClass方法,打破双亲模式,保证自己的类会被自己的classloader加载
  2.启动一个定时任务监听Worker.class的变化,如果发生变化加载Worker打印变化后的版本

  上代码:

  MyClassLoader.java

import java.net.URL;
import java.net.URLClassLoader;
/**
 * 继承URLClassLoader会根据路径下的某个文件去加载类
 */
public class MyClassLoader extends URLClassLoader {

    public MyClassLoader(URL[] urls) {
        super(urls);
    }

    // 打破双亲模式,保证自己的类会被自己的classloader加载
    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        //首先查找类是否已被加载
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                //尝试自己查找并加载类 打破双亲委派
                c = findClass(name);
            } catch (Exception e) {
            	//不需要抛出异常信息,因为打破了双亲模式,所以Object类会加载不到,可以参照JDK自带的loadClass方法细品,官方在此处做了说明
            }
        }
        //如果还是没有,去找父类
        if (c == null) {
            c = super.loadClass(name, resolve);
        }
        return c;
    }
}

  MyClassLoader.java

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class HelloMain {
    private URLClassLoader classLoader;
    private Object worker;
    private long lastTime;
    private String classDir = "D:\\ideaworkspace\\devDemo\\out\\production\\devDemo\\";

    public static void main(String[] args) throws Exception {
        HelloMain helloMain = new HelloMain();
        helloMain.execute();
    }

    private void execute() throws Exception {
        while (true) {
            //监测是否需要加载
            if (checkIsNeedLoad()) {
                System.out.println("检测到新版本,准备重新加载");
                reload();
                //一秒
                invokeMethod();
                System.out.println("重新加载完成");
            }

            System.out.println("本次检查更新完毕。。。");
            Thread.sleep(2000);

        }
    }

    private void invokeMethod() throws Exception {
        //通过反射方式调用
        //使用反射的主要原因是:防止Work被appclassloader加载
        Method method = worker.getClass().getDeclaredMethod("getVersion", null);
        method.invoke(worker, null);
    }

    private void reload() throws Exception {
        classLoader = new MyClassLoader(new URL[]{new URL(
                "file:" + classDir)});
        worker = classLoader.loadClass("Worker")
                .newInstance();

    }

    private boolean checkIsNeedLoad() {
        File file = new File(classDir + "Worker.class");
        long newTime = file.lastModified();
        if (lastTime < newTime) {
            lastTime = newTime;
            return true;
        }
        return false;
    }


}

  代码在关键的地方都进行了注释,拷贝下来到本地看一下应该可以很容易看懂,运行如下,每两秒会检查Worker.class是否变化:
运行效果
  如上,程序成功运行了并时刻监控着class的变化,你也许会问,这只是监听了class的变化,但是我需要的是监听java文件呀,道理是相同的,只是多了个编译的步骤,编译操作可以使用RunTime类来实现。好了,我要下班了,这个就留给你们来实现了,感兴趣的朋友可以尝试一下,通过此案例,重要的是我们要深入了解其原理和思想。

小结

  相信通过此篇博客,让你对类装载的过程和机制有了更深一层的理解,也正是因为有ClassLoader的存在,让我们的程序拥有了无限的可能。学海无涯,与诸君共勉!

发布了27 篇原创文章 · 获赞 99 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/m0_37719874/article/details/103975812