概述
本篇博客来介绍一下类装载器,即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自顶向下尝试加载:
证明类是自顶向下加载的
执行以下代码,可想而知,结果必然为hello,I am bootStrap,拿到它的class字节码文件,放入D盘的testClass目录下:
然后修改代码为:
添加启动参数:-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 {
//首先查找类是否已被加载(结果永远为空,因为我每次调用loadClass方法的时候都new了一个MyClassLoader,
//所以永远都找不到,如果把MyClassLoader改成一个静态变量,第二次load的时候就会返回Worker的Class对
//象,但我们的目的就是为了每次都重新加载,所以为null就对了!但实际上这就是句废话,写出来是为了
//加深理解)
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的存在,才让我们的程序拥有了无限的可能。学海无涯,与诸君共勉!