本文和大家聊聊Java类加载器这档子事。
什么是类加载器?
咱们先来给他下一个通俗点的定义:
将java字节码(.class文件)转换成类对象(java.lang.Class),加载到JVM内存。
Java中有哪几种类加载器?
类加载器可分为四种:
Bootstrap ClassLoader:由C++代码实现,加载sun.boot.class.path所指定的目录,或
Ext ClassLoader:加载系统变量 java.ext.dirs所指定的目录,或
App ClassLoader:加载用户类路径下的文件。
Custom ClassLoader:开发者自己折腾的类加载器,按需进行类文件加载。(例如:网络类加载器,通过网络加载.class文件)
类加载器如何工作?
第一步,类加载器向JVM查询目标对象(请求加载的对象)是否已经加载过,若已经加载过,则直接返回此对象;否则,进行第二步。
第二步,获得当前类加载器的父类加载器,递归执行这一步骤,由下往上,直至到达顶级父类加载器(即Bootstrap ClassLoader),进行第三步。
第三步,从顶级父类加载器,由上而下,在自己的搜索范围内检索目标对象的类文件(.class),若检索到,则读取该文件的字节码转换成Class对象到JVM,返回目标对象;否则,抛出ClassNotFoundException。
有读者老爷说了,听你白扯了半天,对类加载器的认知还是不清楚,有没有实质性的东西?下面咱们来点实的。
类加载器的委派模式
从上文中类加载器的工作步骤,可以看出,递归获取父类加载器,由下往上,直达顶级父类加载器,在从顶级父类加载器,自上而下,逐级在自己的搜索范围内检索目标对象类文件,转换成Class对象(若检索到)的这种行为,称为类加载器的委派模式。
描述递归获取父类加载器,由下往上,直达顶级父类加载器的代码如下:
//代码段截取自ClassLoader.loadClass(String name, boolean resolve)
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
}
描述从顶级父类加载器,自上而下,逐级在自己的搜索范围内检索目标对象类文件,转换成Class对象的代码如下:
//代码段截取自ClassLoader.loadClass(String name, boolean resolve)
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);//在自己的搜索范围内进行检索,若找到,转换成Class对象
.....................................
.....................................
}
if (resolve) {
resolveClass(c);
}
return c;
真正实现类加载的是defineClass方法,代码如下:
//代码段截取自URLClassLoader.findClass(final String name)
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res); //执行类加载工作
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
回顾类加载器的委派模式,我们发现,发起类加载请求的类加载器,不一定会最终执行类的加载操作,可能其父类加载器来执行类加载。
发起类加载请求的类加载器称为初始类加载器;完成类加载操作的类加载器称为定义类加载器。(为啥叫定义类加载?因名而得呗-defineClass)
有读者老爷发问了,为什么要设计这种委派模式?
委派模式的作用
个人理解,设计这种委派模式的用意有两点:
第一,避免对象的重复加载
第二,保护Java基础类型的行为
关于第一点,自然不必多说。
对于第二点,咱们设想一下,在没有委派模式的场景中,用户自己编写了一个java.lang.String类,并将其放置在程式的类路径(ClassPath)中,那系统中就存在了多个不同的String类,这使得Java的基础类型的行为无法得到保证,程式也会变得很混乱。
细心的读者可能会从上面的例子中发现,同一个类被两个不同的类加载器加载会出现问题,产生这种问题的原因是什么?在后面的类加载器的隔离性中会进行解释
类加载器的层级结构是怎样的?
如下是类加载器层级示意图:
口说无凭,咱们来验证一下类加载器的层级结构,执行如下代码:
package com.hys;
public class Main {
public static void main(String[] args) {
ClassLoader c1 = User.class.getClassLoader();
System.out.println(c1);
ClassLoader c2 = c1.getParent();
System.out.println(c2);
ClassLoader c3 = c2.getParent();
System.out.println(c3);
}
}
执行结果:
sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@140e19d
null
Process finished with exit code 0
执行结果符合咱们的预期。
有读者老爷发问了,怎么就符合预期了?结果中有个值为null,是个啥?Bootstrap ClassLoader呢?
原因在于顶级类加载器是Bootstrap ClassLoader,此加载器是C++代码编写,所以,使用Java获取的时候,返回了null
类加载器的隔离性
JVM如何判断一个类的唯一性?
JVM通过加载该类的类加载器和类名称来确定一个类的唯一性。
这意味着我们使用两个不同的类加载器加载同一个类,而在JVM层面上却认为这是两个完全不同的类,从另一个层面看,相当于类被类加载器所包裹与另个类加载器中的类进行了隔离。
如何证明这种隔离的存在?执行如下代码:
package com.hys;
public class Main {
public static void main(String[] args) {
try{
User u1 = new User();
User u2 = new User();
// u1和u2都是同一个类加载器加载的
System.out.println("The same classloader: u1.isInstance(u2)" + u1.getClass().isInstance(u2));
MyClassLoader c1 = new MyClassLoader();
Class clazz = c1.loadClass("com.hys.User");
// clazz是MyClassLoader类加载器加载的
System.out.println("The different classloader: clazz.isInstance(u1)" + clazz.isInstance(u1));
}catch (Exception ex){
ex.printStackTrace();
}
}
}
执行结果:
The same classloader: u1.isInstance(u2)true
The different classloader: clazz.isInstance(u1)false
Process finished with exit code 0
自定义类加载器
自定义类加载器,一般是继承ClassLoader类,覆盖findClass(String name)方法,在此方法中做查询与读取.class文件的操作,如下代码:
package com.hys;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String mRootDir;
public MyClassLoader() {
}
public MyClassLoader(String rootDir) {
mRootDir = rootDir;
}
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException();
}else{
return defineClass(name, data, 0, data.length);
}
}
private byte[]loadClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
String classPath;
if(mRootDir == null || mRootDir.isEmpty())
classPath = className.replace('.', File.separatorChar) + ".class";
else
classPath = mRootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
return classPath;
}
}
我是青岚之峰,如果读完后觉的有所收获,欢迎点赞加关注