Java类加载器子系统
1、类加载的过程
类加载的过程包括了加载,验证,准备,解析和初始化这5个步骤。
①. 加载:找到字节码文件,读取到内存中。类的加载方式分为隐式加载和显示加载两种。隐式加载指的是程序在使用new关键词创建对象时,会隐式的调用类的加载器把对应的类加载到jvm中。显示加载指的是通过直接调用class.forName()方法来把所需的类加载到jvm中。
②. 验证:验证此字节码文件是不是真的是一个字节码文件,毕竟后缀名可以随便改,而内在的身份标识是不会变的.在确认是一个字节码文件后,还会检查一系列的是否可运行验证,元数据验证,字节码验证,符号引用验证等。Java虚拟机规范对此要求很严格,在Java 7的规范中,已经有130页的描述验证过程的内容。
③. 准备:为类中static修饰的变量分配内存空间并设置其初始值为0或null。可能会有人感觉奇怪,在类中定义一个static修饰的int,并赋值了123,为什么这里还是赋值0。因为这个int的123是在初始化阶段的时候才赋值的,这里只是先把内存分配好。但如果你的static修饰还加上了final,那么就会在准备阶段就会赋值。
④. 解析:解析阶段会将java代码中的符号引用替换为直接引用。比如引用的是一个类,我们在代码中只有全限定名来标识它,在这个阶段会找到这个类加载到内存中的地址。
⑤. 初始化:如刚才准备阶段所说的,这个阶段就是对变量的赋值的阶段。
如上过程都是在JVM执行的过程中自己完成的,我们无需干涉。
2、类与类加载器
每一个类,都需要和它的类加载器一起确定其在JVM中的唯一性。换句话说,不同类加载器加载的同一个字节码文件,得到的类都不相等。我们可以通过默认加载器去加载一个类,然后new一个对象,再通过自己定义的一个类加载器去加载同一个字节码文件,拿前面得到的对象去instanceof,会得到的结果是false。
3、双亲委派机制
JVM中内置的类加载器:
类加载时使用了双亲委派模式:
加载规则,优先使用爷爷(启动类加载器)加载,如果没有加载到再使用它爹(扩展类加载器)加载,如果他爹也没有加载到,才到自己(应用程序类加载器或者自定义类加载器)加载,如果自己也没有加载到才报ClassNotFountException。在这过程中只要上一级加载到了,下一级就不会加载。
这么做的目的:不让我们轻易覆盖系统提供的功能,也要让我们扩展我们的功能。
类加载器一般有4种,其中前3种是必然存在的。
① 启动类加载器:加载<JAVA_HOME>\lib下的。
②扩展类加载器:加载<JAVA_HOME>\lib\ext下的。
③应用程序类加载器:加载Classpath下的。
④自定义类加载器
而双亲委派机制是如何运作的呢?
我们以应用程序类加载器举例,它在需要加载一个类的时候,不会直接去尝试加载,而是委托上级的扩展类加载器去加载,而扩展类加载器也是委托启动类加载器去加载。启动类加载器在自己的搜索范围内没有找到这么一个类,表示自己无法加载,就再让扩展类加载器去加载。同样的,扩展类加载器在自己的搜索范围内还是没有找到,就交给应用程序类加载器去加载。如果最终应用程序类加载器还是没找到,那就会直接抛出ClassNotFountException异常了。
而为什么要这么麻烦的从下到上,再从上到下呢?
这是为了安全着想,保证按照优先级加载。如果用户自己编写一个名为java.lang.Object的类,放到自己的Classpath中。没有这种优先级保证,应用程序类加载器就把这个当做Object加载到了内存中,从而会引发一片混乱。而凭借这种双亲委派机制,先一路向上委托,启动类加载器去找的时候,就把正确的Object加载到了内存中,后面再加载自行编写的Object的时候,是不会加载运行的。
结论:JDK自带的类是没法覆盖的,而引入的三方的JAR是可以自己定义相同的类来覆盖。