类加载器
作用:加载Class文件
原理:类加载器读取.class字节码文件,并将其转换成java.lang.Class类的一个实例
类加载器不是car.class,而是car.class.getClassLoader
其中实例化(new)出的实例(对象),它的名字(?)是放在Java栈里的,但是具体的数据是地方在堆中的
两者的地址相等,通过地址来通过引用从栈中找到对应的数据
类加载器分类
1.启动类(根)加载器 BootstrapClassLoader
2.扩展类加载器 ExtClassLoader(JDK9以后转化为PlatformClassLoader)
3.应用程序加载器 AppClassLoader
4.虚拟机自带的加载器
我们可以用程序来测试一下
public class Car {
public static void main(String[] args) {
Class<Car> carClass = Car.class;
System.out.println(carClass);
System.out.println(carClass.getClassLoader());
System.out.println(carClass.getClassLoader().getParent());
System.out.println(carClass.getClassLoader().getParent().getParent());
}
}
上面程序运行后,
应该首先输出Car的类,
然后输出Car类的类加载器,
然后通过.getParent获取类加载器的父类,
再用.getParent获取类加载器父类的父类
运行结果为
可以看到前三个信息都能正常输出,但是最后一个类加载器父类的父类为null,java中为null有两种情况,第一种是本身的值为null,另一种为本身不为null,但是无法获取到对应的信息。
这里为null是因为java调用不到,因为底层是拿C/C++写的
所以应用程序加载器,扩展类加载器和根加载器应该是层层递进的关系
这时候我们可以去找jre目录下的去寻找他们的位置
其中根加载器时rt.jar包中的(rt=runtime)位于jre/lib目录下的
扩展类加载器位于jre/lib/ext目录下
应用程序加载器就是我们自定义的类,其实也就是重写了rt.jar包中ClassLoader抽象类
双亲委派机制
有没有想过一个问题,当我们定义一个和rt.jar包中同包同名的类后,java虚拟机应该运行自己本身定义的类呢,还是运行我们定义的类呢?
我们可以测试一下,我们知道java.lang.String使我们平时是用的String类,我们自己定义一个java.lang.String类
给出我们定义的toString方法,返回一个Hello
然后我们定义主函数去调用String类的toString方法,如果输出了Hello,就说明系统运行了我们定义的String类,如果输出的不是Hello,或者运行失败了,说明系统还是调用的自己rt.jar包下的String类
当我们点击运行后
可以看到java告诉我们无法找到main方法,可是我们定义了main方法呀,这是什么问题呢?
这里就要引出我们的双亲委派机制了
双亲委派机制
1.目的是为了保证程序的安全,不会让程序员去随意重写根加载器中的类和方法
2.也避免了类的重复加载,因为JVM中区分不同类,不仅仅是根据类名,相同的class文件被不同的类加载器加载就是两个不同的类
当程序运行的时候,会向上找从应用程序加载器-->扩展类加载器-->根加载器
就算我们在扩展类加载器中定义同包同名的String,系统也是会去运行根加载器中的String
可以总结为:
类加载流程
1.类加载器收到类加载需求
2.将这个需求向上委托给父类加载器去完成,一直向上委托,查询类加载器的缓存(每个类加载器都有自己的缓存,检查是否加载就是检查类加载器的缓存是不是已经加载过这个类),直到根加载器
3.如果一直到根加载器的缓存中也没有找这个类,那么就会从当前类加载器的加载路径中检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则跑出异常,通知子类加载
4.如果都找不到,报ClassNotFound
总结为一句话就是:向上委派查找缓存,向下查找查找加载路径
注意:是向上委派到顶层的加载器为止,向下查找到发起的类加载器为止(不一定是AppClassLoader,可能是自定义的类加载器)
当判断一个类是由哪个加载器加载时,可以通过下面代码来判断
类名.getClass.getClassLoader.sout
这里的双亲委派这个词,其实并不是很准确,这里的双应该是代表应用类加载器向上委托了两次,亲代表上一层的意思