Java里的双亲委派机制
Java里的双亲委派机制
什么是双亲委派模型?
首先,先要知道什么是类加载器。简单说,类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。
如果站在JVM的角度来看,只存在两种类加载器:
-
启动类加载器(Bootstrap ClassLoader)
-
其他类加载器:由Java语言实现,继承自抽象类ClassLoader。
但是实际上却并不是这个样子的。
我们看一幅图描述的类加载器之间的关系:
BootStrapClassLoader:
启动类加载器,该ClassLoader是jvm在启动时创建的,用于加载 $JAVA_HOME/jre/lib下面的类库(或者通过参数-Xbootclasspath 指定)。
由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。
ExtensionClassLoader:
扩展类加载器,该ClassLoader是在sun.misc.Launcher里作为一个内部类ExtClassLoader定义的(即 sun.misc.Launcher$ExtClassLoader),
ExtClassLoader会加载 $JAVA_HOME/jre/lib/ext下的类库(或者通过参数-Djava.ext.dirs指定)。
ApplicationClassLoader:
应用程序类加载器,该 ClassLoader 同样是在sun.misc.Launcher里作为一个内部类AppClassLoader定义的(即 sun.misc.Launcher$AppClassLoader ),
ApplicationClassLoader会加载java环境变量CLASSPATH所指定的路径下的类库,
而CLASSPATH所指定的路径可以通过System.getProperty(“java.class.path”)获取;
当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的class目录)。
CustomClassLoader:
自定义类加载器,该 ClassLoader 是指我们自定义的 ClassLoader ,比如tomcat的 StandardClassLoader 属于这一类;
当然,大部分情况下使用 ApplicationClassLoader 就足够了。
所以说双亲委派模型就是说:如果一个类加载器收到了类加载的请求,它首先不会自己尝试去加载这个类,
而是把这个请求委派给父类加载器,每一个层次的类加载器都是加此,因此所有的加载请求最终到达顶层的启动类加载器,
只有当父类加载器反馈自己无法完成加载请求时(指它的搜索范围没有找到所需的类),子类加载器才会尝试自己去加载。
其实这也是双亲委派模型的一个过程。
双亲委派模型的系统实现
public abstract class ClassLoader {
/**
java.lang.ClassLoader的loadClass()方法中,
先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,
若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,
再调用自己的findClass()方法进行加载。*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已加载
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
// 从非空父类加载器
}
if (c == null) {
// 如果仍然找不到,则按顺序调用findclass
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// 这是定义类加载器;记录统计信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,
若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,
再调用自己的findClass()方法进行加载。
以上的代码就是双亲委派模型的一个代码实现的一个案例,其实原理说起来很简单,但是如果你不深入了解类加载机制,那么就比较难理解了
双亲委派模型原理
1-类加载器收到类加载的请求;
2-把这个请求委托给父加载器去完成,一直向上委托,直到启动类加载器;
3-启动器加载器检查能不能加载(使用findClass()方法),能就加载(结束);否则,抛出异常,通知子加载器进行加载。
4-重复步骤三;
其实在Java的日常应用程序开发中,类的加载几乎是由BootStrap类加载器,Extension类加载器,Application类加载器相互配合执行的,
在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,
也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,
Java虚拟机采用的是双亲委派模式即把请求交由父类处理。
为什么要使用双亲委派模型呢?
其实这个时候就有一道非常有意思的面试题了大家看一下
能不能自己写个类叫java.lang.System?
其实这也是阿里面试的一道很经典的面试题,我们来做个分析。
之前看过一个文章说的就是说这个通常不可以,但可以采取另类方法达到这个需求。所谓的另类方法指自己写个类加载器来加载java.lang.System达到目的。
但是实际上却是是不能的,为了输出,我们做个最简单的测试用Math类来进行测试,效果和原理类似,这个例子是我之前在看一个文章的时候随手记录下拉的,当时运行了一下,确实很有道理,大家可以参考一下。
包结构如下:
代码
package com.tq;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader {
public MyClassLoader() {
super(null);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try{
String className = null;
if(name.startsWith("java.lang")){
className = "/" + name.replace('.', '/') + ".class";
}else{
className = name.substring(name.lastIndexOf('.') + 1) + ".class";
}
System.out.println(className);
InputStream is = getClass().getResourceAsStream(className);
System.out.println(is);
if(is == null)
return super.loadClass(name);
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
package com.tq;
public class ClassLoaderTest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myLoader = new MyClassLoader();
Object obj = myLoader.loadClass("java.lang.Math").newInstance();
System.out.println(obj);
}
}
package java.lang;
public final class Math {
public static void main(String[] args) {
System.out.println("hello world");
}
}
package java.lang;
public class MyMath {
public static void main(String[] args) {
System.out.println("hello world");
}
}
我们在运行Math里面的输出方法的时候就会出现问题,
提示Math类没有main方法,这就是我们在之前预测根据双亲委托原则,Math类首先由启动类加载器去尝试加载,
它找到rt.jar中的java.lang.Math类并加载进内存(并不会加载我们自定义的Math类),然后执行main方法时,发现不存在该方法,
所以报方法不存在错误。也就是说,默认情况下JVM不会加载我们自定义的Math类。
java.lang.SecurityException: Prohibited package name: java.lang 他不允许我们使用java.lang的包名
我们继续运行下ClassLoaderTest类
我们看一下ClassLoader的源码
private ProtectionDomain preDefineClass(String name,ProtectionDomain pd){
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
通过代码实例及源码分析可以看到,对于自定义的类加载器,强行用defineClass()方法去加载一个以”java.”开头的类也是会抛出异常的。
总结
-
双亲委派模型最大的好处就是让Java类同其类加载器一起具备了一种带优先级的层次关系。这句话可能不好理解,我们举个例子。比如我们要加载java.lang.Object类,无论我们用哪个类加载器去加载Object类,这个加载请求最终都会委托给Bootstrap ClassLoader,这样就保证了所有加载器加载的Object类都是同一个类。如果没有双亲委派模型,那就乱了套了,完全可能搞出多个不同的Object类。
-
自上而下每个类加载器都会尽力加载.
-
不能自己写以”java.”开头的类,其要么不能加载进内存,要么即使你用自定义的类加载器去强行加载,也会收到一个SecurityException。