ClassLoader中文翻译过来类加载器,顾名思义就是用来加载Java Class文件的。程序员写出来的java代码,必须首先编译成java class文件,然后通过ClassLoader将class文件加载到计算机内存中执行。其实本人也看了很多classloader的文章或书籍,对其中反复介绍的class字节码加载过程、双亲委派模型一直不是很了解,今天通过一个简单的classloader的demo来详细理解其中的原理。
ClassLoader Demo
public class MyClassLoader extends ClassLoader{
private static int counter = 0;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
System.out.println("===========counter==========" + counter);
if(counter>0){
throw new RuntimeException("已经初始化");
}
byte[] classBytes = IOUtils.toByteArray(new FileInputStream("/tmp/RouteMappingServiceImpl.class"));
counter++;
return defineClass(name,classBytes,0,classBytes.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@SneakyThrows
public static void main(String[] args) {
MyClassLoader loader = new MyClassLoader();
Class c = loader.loadClass("com.cmcc.coc.demo.service.RouteMappingServiceImpl");
RouteMappingService rms = (RouteMappingService) c.newInstance();
rms.print("测试classloader");
Class c1 = loader.loadClass("com.cmcc.coc.demo.service.RouteMappingServiceImpl");
RouteMappingService rms1 = (RouteMappingService) c1.newInstance();
rms1.print("测试classloader111");
}
}
这段代码实现了一个自定义的classloader的类,重写了classloader的findClass方法。
- 这段代码在执行过程中,若在
target目录下面删除RouteMappingServiceImpl.class文件,则执行结果如下:
- 删除了
RouteMappingServiceImpl.class文件之后,将相同的文件拷贝到/tmp目录下,执行结果如下:
运行结果分析
为什么counter=0
main函数中创建了一个classloader,同时使用这个classloader加载了两次类,但是findClass只执行了一次。这个主要是Java类加载的机制决定的。
何时加载类
- 遇到new,getstatic,putstatic,invokestatic这4条指令,注意这里的new并发java关键字new,而是字节码指令new。
- 读取或者设置静态字段的时候
- 调用类型的静态方法的时候
- 使用
java.lang.reflect包的方法对类型进行反射调用的时候 - 类初始化的时候,若父类未进行初始化,则会触发父类的初始化
- 包含main()方法的类。
注意classLoader的loadClass并不会触发类的加载。
在demo中调用
newInstance方法触发了类的加载,并且在类已经加载内存中之后,再次调用newInstance并不会再次调用类加载器。因此findClass只会执行一次。
ClassLoader双亲委派模型
终于说到大名鼎鼎的类加载机制了。Java的类加载的模型是自己先不加载,先调用父类加载器,父类加载器无法加载在调用子类的加载。那么为什么要这样设计呢,这个好像与继承的方式好像是相反的,其实这样设计最大的好处就是保证Java基础类的稳定性。比如java.lang.Object类,由于采用了双亲委派机制,就只能使用BootstrapClassLoader来进行加载,其他类加载器是无法加载到这个类,这样就确保了基础类的稳定性。
类加载层次
- Bootstrap ClassLoader:启动类加载器,JVM的一部分,C++实现。
- Extension ClassLoader:扩展类加载器:负责加载<JAVA_HOME>/lib/ext目录下面的类。
- Application ClassLoader:应用类加载器,负责加载用户的类,也就是classpath中的类。
- User ClassLoader:自定义类加载器。 这三个类并非Java中的继承关系,而是使用组合的方式实现,具体实现的代码在ClassLoader中。
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
}
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.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
这里面的parent的值如下:
是一个类的实例变量,在初始化中就会配置好,比如ApplicationClassLoader类,其parent就应该是Extension ClassLoader。这个与java中的继承是不一样的。
loadClass方法未被调用原因
当删除target目录中的RouteMappingServiceImpl文件之后,首先调用的是用户类加载器,用户类加载器调用AppClassLoader->Ext ClassLoader->BootstrapClassLoader,由于BootstrapClassLoader无法加载RouteMappingServiceImpl.class文件,因此BootstrapClassLoader->extClassLoader->appClassLoader,此时有两种情况
- classpath中存在RouteMappingServiceImpl.class文件,则appClassLoader完成类的加载,整个类加载过程结束,并不会调用我们写的findClass方法。
- classpath中不存在RouteMappingServiceImpl.class文件,则调用MyClassLoader.findClass方法完成类的加载,整个流程结束。 以上就是两种不同情况下打印不同语句的原因。
小结
Java的类加载器是一个很精妙的设计,开发人员可以设计不同的类加载器实现不同的加载方式,比如可以从网络或数据库中读取java class文件完成类的加载,也可以对java class文件进行加解密等等,通过这些可以极大方便的java的扩展,这一切的基础都需要Java程序员对类加载器基础有比较深刻的理解。