From:深入理解Java虚拟机
- 目录
BiBi - JVM -0- 开篇
BiBi - JVM -1- Java内存区域
BiBi - JVM -2- 对象
BiBi - JVM -3- 垃圾收集算法
BiBi - JVM -4- HotSpot JVM
BiBi - JVM -5- 垃圾回收器
BiBi - JVM -6- 回收策略
BiBi - JVM -7- Java类文件结构
BiBi - JVM -8- 类加载机制
BiBi - JVM -9- 类加载器
BiBi - JVM -10- 虚拟机字节码
BiBi - JVM -11- 编译期优化
BiBi - JVM -12- 运行期优化
BiBi - JVM -13- 并发
类加载器定义
虚拟机在类加载阶段中的【通过一个类的全限定名来获取描述此类的二进制字节流】,这个过程放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个过程的代码模块称为【类加载器】。
类加载器最初是为了满足Java Applet的需求而开发的,现在Java Applet技术已经落伍了,但类加载器技术却得到了广泛应用。如:类层次划分、OSGi、热部署、代码加密等领域。
任意一个类,都需要由加载它的类加载器和这个类本身,共同确定其在Java虚拟机中的唯一性。每一个类加载器,都有一个独立的类名空间。即,判断类是否相等、instanceof是基于同一个类加载器而言的。
小例子:
Object obj = myLoader.loadClass("com.ljg.Test").newInstance();
System.out.println(obj.getClass()); //com.ljg.Test
System.out.println(obj instanceof com.ljg.Test); //false
尽管obj.getClass()
返回com.ljg.Test,但instanceof结果为false。因为虚拟机中存在两个Test类,一个由系统应用程序类加载器加载的,另一个由我们自定义的myLoader类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类。
双亲委派模型
-
从Java虚拟机角度讲,加载器分两种:
1)启动类加载器,使用C++语言实现【针对HotSpot而言】,是虚拟机自身的一部分。
2)其它的类加载器,使用Java语言实现,独立于虚拟机之外,并且都继承抽象类ClassLoader。 -
从Java开发人员角度讲,有三种重要的类加载器:
1)启动类加载器
加载<java_home>\lib目录中的,并且是虚拟机识别的类。启动类加载器无法被Java程序直接引用。
2)扩展类加载器
由ExtClassLoader实现,加载<java_home>\lib\ext目录中的类,开发者可以直接使用扩展类加载器。
3)应用程序类加载器
由AppClassLoader实现,是getSystemClassLoader()方法的返回值,所以一般称为【系统类加载器】。他负责加载用户类路径上所定义的类,开发者可以直接使用这个类加载器。如果用户没有自定义类加载器,会默认使用个这个类加载器。
双亲委派模型的过程:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的【启动类加载器】,只有当父加载器反馈自己无法完成这个加载请求时【即它的搜索范围中没有找到所需的类】,子加载器才会尝试自己去加载。
注意:类加载器之间的父子关系一般不是继承,而是使用组合关系来复用父加载器。
双亲委派模型的好处
Java类具有一种优先级的层次关系。如:java.lang.Object,无论哪个类加载器要加载这个类,最终都会委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
注意:如果自己定义的类与rt.jar类库中类重名【权限的类名相同】,将会正常编译,但是自己定义的类将无法被加载运行。
双亲委派伪代码
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
//首先,检查请求的类是否已经被加载过了
Class c = findLocalClass(name);
if (null == c) {
try {
if (null != parent) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类加载器抛出异常,说明父类加载器无法完成加载请求
}
if (null == c) {
//父类无法加载时,调用本身的findClass方法来进行加载
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
}
return c;
}
破坏双亲委派模型
问题:双亲委派很好地解决了各个类加载器的基础类的统一问题【越基础的类由越上层加载器进行加载】,但若基础类要回调用户的代码,该怎么办?如:在JNDI服务中,它的代码由启动类加载器去加载,但JNDI的目的是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能认识这些代码。
解答:Java提供了【线程上下文类加载器】,通过setContextClassLoader()方法进行设置,如果创建线程时没有设置,将从父类中继承,如果在应用程序全局范围内都没有设置的话,那这个类加载器默认是应用程序类加载器。通过【线程上下文类加载器】父类加载器可以请求子类加载器去完成类加载的动作。这种行为实际上就是通过了双亲委派模型的层次结构来逆向使用类加载器。
-
动态性追求导致双亲委派模型被破坏
如:热更新【Hot Swap】、热部署【Hot Deployment】
模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起更换以实现代码的热更新。
Tomcat中,JasperLoader的加载范围仅仅是JSP文件所编译出来的一个Class,它出现的目的就是被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
网友评论