在
上篇文章中介绍了类的加载器以及加载过程,下面继续进一步理解类的加载器。
1. 双亲委派机制原理
双亲委派机制原理:如果一个类加载器收到了类加载的请求,该类加载器并不会立即去加载,而是把加载任务先委托给父加载器加载;如果父加载器还有父加载器,则会继续向上委托,一直委托到启动类加载器;如果父加载器可以完成加载Class字节码的任务,就成功加载返回,若需要加载的类不在父加载器加载范围内,子加载器尝试去加载,一直到向下委托到可以加载的加载器。
双亲委派的本质规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器进行加载,若扩展类加载器也加载不到,才会由系统类加载器进行加载。
2 双亲委派的优势与弊端
双亲委派的优势
- 双亲委派避免了类的重复加载,确保了一个类的全局唯一性。即当父类加载器加载了一个类,子类就没必要进行加载了;
- 确保了程序的安全性,防止核心API被篡改。
双亲委派的弊端 双亲委派虽然保证了类加载的职责比较明确,但也带来一个问题:顶层的ClassLoader加载的类无法访问底层ClassLoader加载的类。比如在java的核心类库(一般由BootstrapClassLoader加载器加载)提供一个接口,该接口的实现由应用加载器进行加载,比如该接口绑定了一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中加载,实现却由应用加载器加载,这就会导致工厂方法无法创建由应用类加载器加载的应用实例的问题。
3 违反双亲委派机制的行为
3.1 第一种违反双亲委派机制行为
从上文中双亲委派的优势和弊端中可以看出,双亲委派机制固然有它的优势,但并不是所有类的加载行为都会遵守双亲委派机制的。
- 并不是启动类加载器加载的类不可以访问应用加载的类。几个典型的场景为JNDI ,JDBC,JCE,JAXB,JBI等,以JNDI为例,JNDI服务属于java的核心类库中的服务,它存在的目的是对资源进行查找和集中管理,它需要调用其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供接口,根据双亲委派机制是不可能的,那么JAVA是如何解决这种违背双亲委派机制行为的呢?这还是拜双亲委派机制为这种违反的行为留了一个后门。如下加载器启动入口代码所示,当启动类加载器无法调用应用类加载器加载的类时,可以获取上下文加载器后,利用上下文加载器加载应用类代码后方便核心类库中接口调用。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
3.2 第二种违反双亲委派机制行为
第二种违反双亲委派机制行为也即热替换代码的实现。热替换指程序运行过程中修改了某个类不停止服务的行为,修改后的程序必须立即运行在系统之中。对于java来说并非天生支持热替换,当一个类已经加载到系统中,然后修改了类文件,无法让系统重新加载修改过的类。因此如果达到热替换,立即加载修改过的类文件可以借助ClassLoader。 当服务运行过程中修改了一个类文件,此时可以创建一个新的类加载器来加载修改过的类文件达到热替换的功能,但即使是同一个类文件,由不同的类加载器加载,生成的class对象也是不同的。
4. 沙箱机制
java安全模型的核心就是沙箱(sandbox),沙箱其实一个限制程序运行的环境。沙箱机制就是将java代码限定在JVM虚拟机特定的运行环境中,并且严格限制代码对本地系统资源的访问,通过这种措施来保证对代码的有限隔离,防止对本地系统的破坏。 可见沙箱机制优势:保证程序运行安全、保护java原声JDK代码。
沙箱主要限制系统的资源访问系统资源,系统资源主要包括CPU、内存、文件系统、网络,不同级别的沙箱对这些资源访问的限制也是很不同的。所有Java程序运行都可以指定沙箱,定制安全策略。
5. 自定义类加载器
自定义加载器的优势:
- 隔离加载类:比如对于一些框架或者中间件之类的模块,单独定义类加载器进行加载,与应用服务进行了隔离;
- 修改类的加载方式:并不是所有类的加载都需要遵循双亲委派模型,可以自定义加载器,按需进行加载;
- 扩展加载资源:比如从网络、文件或者数据库中进行加载类;
- 防止源码泄露:java代码容易被编译和篡改,可以对编译进行加密,使用时通过自定义的加载器对需要加载的类进行解密。
自定义加载器时可以继承java.lang.ClassLoader,继承ClassLoader后可以重写loadClass方法或者findClass方法。但由于loadClass方法中实现了双亲委派模型机制,重写了该方法容易使双亲委派模型失效,一般推荐使用实现findClass的方法。
自定义类加载器
package com.lzj;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
private String classPath;
public MyClassLoader(String classPath){
this.classPath = classPath;
}
public MyClassLoader(ClassLoader parent, String classPath){
super(parent);
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
String fileName = classPath + className + ".class";
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
try {
bis = new BufferedInputStream(new FileInputStream(fileName));
baos = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int length;
while((length = bis.read(data)) != -1){
baos.write(data, 0, length);
}
byte[] classCodes = baos.toByteArray();
Class<?> clazz = defineClass(null, classCodes, 0, classCodes.length);
return clazz;
} catch (IOException e){
e.printStackTrace();
} finally {
if (baos != null){
try{
baos.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (bis != null){
try{
bis.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
return null;
}
}
测试类加载器
package com.lzj;
public class ClassloaderTest {
public static void main(String[] args) {
MyClassLoader loader = new MyClassLoader(Thread.currentThread().getContextClassLoader().getParent(), "D:/myproject/java/src/com/lzj/");
try {
Class<?> clazz = loader.loadClass("Persion");
System.out.println(clazz.getClassLoader());
System.out.println(clazz.getClassLoader().getParent());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果如下所示,说明自定义的类加载器是MyClassLoader类型的,其父类加载器为ExtClassLoader类加载器
com.lzj.MyClassLoader@1540e19d
sun.misc.Launcher$ExtClassLoader@14ae5a5
如果创建MyClassLoader类加载器时不指定父类加载器,则MyClassLoader类加载器的父加载器默认为ApplicationClassLoader加载器,如下所示
package com.lzj;
public class ClassloaderTest {
public static void main(String[] args) {
MyClassLoader loader = new MyClassLoader("D:/myproject/java/src/com/lzj/");
try {
Class<?> clazz = loader.loadClass("Persion");
System.out.println(clazz.getClassLoader());
System.out.println(clazz.getClassLoader().getParent());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果如下所示
com.lzj.MyClassLoader@1540e19d
sun.misc.Launcher$AppClassLoader@58644d46
|