看到网上有一道面试题:能不能装载自定义的 java.lang.String?
答案是否定的,我们能自定义一个java.lang.String,但是加载不进来。
我相信很多人在网上看到这样的答案“可以,但在应用的时候,需要用自己的类加载器去加载”。这个回答是错误的,现在我们来分析一下jvm在装载一个类的时候,是如何进行的。
双亲委派模型
当虚拟机接受到一个类的加载请求时,它将这个加载请求委派给父类加载器进行加载,只有当父类加载器自己无法完成加载请求时,子类加载器才会尝试自己加载。
那么系统自带的加载器有哪些呢?
1.启动类加载器BootstrapClassLoader:
是嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负载加载JAVA_HOME/lib下的类库,启动类加载器无法被应用程序直接使用。
2.扩展类加载器Extension ClassLoader:
该加载器器是用JAVA编写,且它的父类加载器是Bootstrap,是由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext目录中的类库。开发者可以这几使用扩展类加载器。
3.统类加载器App ClassLoader:
系统类加载器,也称为应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件。它的父加载器为Ext ClassLoader。
各个类加载器之间是组合关系,并非继承关系。
双亲委托模式的优点
1.避免重复加载。当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
2.安全性。如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患。
重写String
回到上面的面试题,我重写了一个java.lang.String的类
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("Hello String");
}
}
运行之后会抛一个异常:
错误: 在类 java.lang.String 中找不到主方法, 请将主方法定义为:
public static void main(String[] args)
因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类,加载该自定义的String类,该自定义String类使用的加载器是AppClassLoader。
根据优先使用父类加载器原理,AppClassLoader加载器的父类为ExtClassLoader,所以这时加载String使用的类加载器是ExtClassLoader,但是类加载器ExtClassLoader在jre/lib/ext目录下没有找到String.class类。
然后使用ExtClassLoader父类的加载器BootStrap,父类加载器BootStrap在JRE/lib目录的rt.jar找到了String.class,发现已经加载过了,于是不会再加载我们自定义的类。
打破双亲委托机制
有些人看到这,就会想,那我们自定义一个类加载器,但是不实现双亲委托机制就好了,强行加载,就算重复了也不管。但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。并且这个方法被final修饰的,是改不了的。