ClassLoader的原理及应用
前言
先来看Java程序是怎么工作的
我们都知道Java是跨平台的,是因为不同平台下的JVM能将字节码文件解释为本地机器指令,JVM是怎么加载字节码文件的?答案就是ClassLoader,先来打印看一下ClassLoader对象
public class ClassLoaderDemo1 {
public static void main(String[] args) {
// null
System.out.println(String.class.getClassLoader());
ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
while (loader != null) {
// sun.misc.Launcher$AppClassLoader@58644d46
// sun.misc.Launcher$ExtClassLoader@7ea987ac
System.out.println(loader);
loader = loader.getParent();
}
}
}
要理解这个输出,我们就得说一下双亲委派模式,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。检查和加载过程以及系统提供的ClassLoader的作用如下图。
LZ原来面试的时候就被问到,如果在你项目中建一个java.lang.String的类,那系统中用的String类是你定义的String类,还是原生api中的String类,用双亲加载来解释就很容易理解用的是原生api中的String类
类加载器的关系如下:
- 启动类加载器,由C++实现,没有父类
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader系统类加载器(AppClassLoader)
- 自定义类加载器,父类加载器肯定为AppClassLoader。自定义类加载器,父类加载器肯定为AppClassLoader
源码解析
一般只需要理解ClassLoader 这3个方法即可
loaderClass:实现双亲委派
findClass:用来复写加载
defineClass:本地方法,最终加载类只能通过defineClass
// 从这方法开始加载
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// 实现双亲委派
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 先从缓存查找该class对象,找到就不用重新加载
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
// 这里体现了自顶向下尝试加载类,当父类加载加载不到时
// 会抛出ClassNotFoundException
// 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();
// 如果都没有找到,通过自己的实现的findClass去加载
// findClass方法没有找到会抛出ClassNotFoundException
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;
}
}
// 复写加载
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
自定义类加载器
为什么要编写自己的类加载器?
- 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。
- 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑
- 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。
当继承ClassLoader时,只要重写findClass方法即可,还可以继承URLClassLoader,代码更简洁,这里不再赘述,下面写一个从指定文件中加载class文件的FileClassLoader
public class DemoObj {
public String toString() {
return "I am DemoObj";
}
}
javac生成相应的class文件,放到指定目录,然后由FileClassLoader去加载
public class FileClassLoader extends ClassLoader {
// class文件的目录
private String rootDir;
public FileClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
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;
}
public static void main(String[] args) {
String rootDir="E:\\Code\\study-java\\src\\main\\java";
FileClassLoader loader = new FileClassLoader(rootDir);
try {
// 传入class文件的全限定名
Class<?> clazz = loader.loadClass("com.st.classloader.DemoObj");
// com.st.classloader.FileClassLoader@7ea987ac
System.out.println(clazz.getClassLoader());
// I am DemoObj
System.out.println(clazz.newInstance().toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
应用
可以对class文件进行加密和解密,实现应用的热部署,防止类重名等。这里只对Tomcat中的ClassLoader进行分析
在解释防止类重名作用前先抛出一个问题,Class对象的唯一标识能否只由全限定名确定?答案是不能,因为你无法保证多个项目间不出现相同的全限定名的类。比如和JDK原生类重名或者你的Web项目和Tomcat类重名(全限定名重名无法沟通,无法约束)。JVM判断2个类是否相同的条件是:(1)全限定名相同(2)由同一个类加载器加载
写个例子验证一下,先自定义一个FileClassLoader1,直接defineClass,不委托父类加载器进行加载
public class FileClassLoader1 extends ClassLoader {
private String rootDir;
public FileClassLoader1(String rootDir) {
this.rootDir = rootDir;
}
public Class<?> myLoadClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
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;
}
}
验证即使同一个class文件被不同classloader加载,也会被认为是不同的类
public class ClassLoaderDemo2 {
public static void main(String[] args) throws Exception {
String rootDir="E:\\Code\\study-java\\src\\main\\java";
FileClassLoader1 loader = new FileClassLoader1(rootDir);
Class<?> clazz = loader.myLoadClass("com.st.classloader.DemoObj");
// com.st.classloader.FileClassLoader1@12a3a380
System.out.println(clazz.getClassLoader());
// sun.misc.Launcher$AppClassLoader@58644d46
System.out.println(DemoObj.class.getClassLoader());
Object object = clazz.newInstance();
// I am DemoObj
System.out.println(object);
// 从这里可以看到,虽然class文件一样,但是由不同的classloader加载,则为不同的类
// false
System.out.println(object instanceof DemoObj);
}
}
Tomcat中就定义了很多ClassLoader来防止重名。在Tomcat中提供了一个Common ClassLoader,它主要负责加载Tomcat使用的类和Jar包以及应用通用的一些类和Jar包,例如CATALINA_HOME/lib目录下的所有类和Jar包。Tomcat会为每个部署的应用创建一个唯一的类加载器,也就是WebApp ClassLoader,它负责加载该应用的WEB-INF/lib目录下的Jar文件以及WEB-INF/classes目录下的Class文件。由于没有应用都有自己的WebApp ClassLoader,这样就可以使不同的Web应用之间相互隔离,彼此之间看不到对方使用的类文件。即使不同项目下的类全限定名有可能相等,也能正常工作。
而对应用进行热部署时,会抛弃原有的WebApp ClassLoader,并为应用创建新的WebApp ClassLoader。
文章转载自公众号:Java识堂