JVM之类的加载器详解
- 命名空间与类的唯一性
(1)何为类的唯一性 对于任意一个类,都需要有加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等 (2)命名空间 ① 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成 ② 在用一个命名空间中,不会出现类的完整名字相同的两个类 ③ 在不同的命名空间中,有可能会出现类的完整名字相同的两个类 (3)类加载的三个基本特征 ① 双亲委派模型:不是所有类都遵守这个模型,启动类加载器所加载的类型,是可能要加载用户代码的,是利用所谓的上下文加载器 ② 可见性:子类加载器可以访问父类加载器加载的类型,但是反过来不行,父加载器的类型对于子加载器是可见的 ③ 单一性:由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载,类加载器邻居间,统一类型仍然可以被加载多次,因为互不可见 - 类的加载器分类
JVM 支持两种类型的类加载器,分别是启动类加载器和自定义类加载器 (1)启动类加载器说明 ① 这个类加载使用C/C++语言实现,嵌套在JVM内部 ② 它用来加载Java的核心库,用于提供JVM自身需要的类 ③ 并不继承java.lang.ClassLoader,没有父加载器 ④ 出于安全考虑,Bootstrap启动类加载器只加载包名为java,javax,sun等开头的类 ⑤ 加载扩展类和应用程序类加载器,并指定为他们的父加载器 (2)扩展类加载器 ① Java语言编写,由sun.misc.Launcher$ExtClassLoader实现 ② 继承于ClassLoader类 ③ 父类加载器为启动类加载器 ④ 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库,如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载 (3)系统类加载器的说明 ① Java语言编写,由sun.misc.Launcher$AppClassLoader实现 ② 继承于ClassLoader类 ③ 父类加载器为扩展类加载器 ④ 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库 ⑤ 它是用户自定义类加载器的默认父加载器 ⑥ 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器 (4)用户自定义类加载器 ① 自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源 ② 通过类加载器可以实现非常绝妙的插件机制 ③ 自定义加载器能够实现应用隔离 ④ 自定义类加载器通常需要继承于ClassLoader (5)获取不同类加载器的方法 ① 获得当前类的ClassLoader:clazz.getClassLoader(); ② 获得当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader(); ③ 获得系统的ClassLoader:ClassLoader.getSystemClassLoader(); - ClassLoader源码解析
(1)loadClass()的解析 (2)protected findClass(String name) 查找二进制名称为name的类,返回结果为java.lang.Class类的实例 ,需要注意的是ClassLoader类中并没有实现findClass方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道是findClass方法通常和defineClass方法一起使用。一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass方法生成类的Class对象 (3)protected defineClass(String name,byte[] b,int off, int len) 该方法是用来将byte字节流解析成JVM能够识别的Class对象,通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方法实例化对象 (4)两个类加载方法的区别 ① Class.forName():是一个静态方法,该方法在将Class文件加载到内存的同时,会执行类的初始化 ② ClassLoader.loadClass():是一个实例方法,该方法将Class文件加载到内存的同时不会执行类的初始化,知道这个类第一次使用时才进行初始化 - 双亲委派模型
(1)优势 ① 避免类的重复加载,确保一个类的全局唯一性,Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父类已经加载了该类时,就没有必要子ClassLoader再加载一次 ② 保护程序安全,防止核心API被随意篡改 (2)代码支持 双亲委派机制在java.lang.ClassLoader.loaderClass(String,boolean)接口中体现,该接口的逻辑如下: ① 先在当前加载器的缓存中查找有无 目标类,如果有,直接返回 ② 判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name,false)接口进行加载 ③ 如果当前加载器的父加载器为空,则调用findBootstrapClassOrNull(name)接口,让引导类加载器进行加载 ④ 如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载,该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类 (3)弊端 通常情况下,启动类加载器中的类为系统核心类,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没问题,但是系统类访问应用类就会出现问题,顶层的ClassLoader无法访问底层的ClassLoader所加载的类 (4)破坏双亲委派机制 ① 第一次破坏:为了兼容一些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码 ② 第二次破坏:越基础的类由越上层的加载器进行加载,但有时候基础类型又要调用回用户的代码,为了解决这一问题**,线程上下文类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则 ③ 第三次破坏:由于用户对程序动态性的追求,像代码热替换,模块热部署等,在OSGI环境下,类加载器不在双亲委派模型推荐的树状结构,而是进一步发展为更为复杂的网状结构。 当收到类加载请求是,OSGI将按照下面的顺序进行类搜索: 以java.开头的类,委派给父类加载器加载,否则将委派列表名单内的类,委派给父类加载器加载 (5)热替换的实现 热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中。* 对于java而言,如果一个类已经加载到系统中,通过修改类文件,并无法让系统再来加载并重定义这个类,在Java中实现热部署的一个 可行方法就是运用ClassLoader - 沙箱安全机制
(1)概念 Java安全模型的核心就是Java沙箱,沙箱是一个限制程序运行的环境,沙箱机制就是将Java代码限定在虚拟机中特定的运行范围中,并且严格限制代码对本地系统资源访问。 (2)作用 ① 保证程序安全 ② 保护Java原生的JDK代码 (3)各个JDK版本的沙箱机制 - 自定义类加载器
(1)自定义类加载器
public class MyClassLoader extends ClassLoader {
private String byteCodePath;
public MyClassLoader(String byteCodePath) {
this.byteCodePath = byteCodePath;
}
public MyClassLoader(ClassLoader parent, String byteCodePath) {
super(parent);
this.byteCodePath = byteCodePath;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
try {
String fileName = byteCodePath + className + ".class";
bis = new BufferedInputStream(new FileInputStream(fileName));
baos = new ByteArrayOutputStream();
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
byte[] byteCodes = baos.toByteArray();
Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
return clazz;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (baos != null)
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (bis != null)
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
(2)测试
public class MyClassLoaderTest {
public static void main(String[] args) {
MyClassLoader loader = new MyClassLoader("d:/");
try {
Class clazz = loader.loadClass("Demo1");
System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());
System.out.println("加载当前Demo1类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
|