写在最前
我们都知道类都是通过类加载器被加载进虚拟机中的,那这个类加载器有哪些呢?我们平时写的代码又是通过什么类加载器被加载进虚拟机中的呢?类加载器的工作模式又是什么呢?带着疑问一起去学习下双亲委派模型与类加载器。
一.类加载器
1.什么是类加载器
我们都知道java文件的生命周期有7个主要步骤,第一步就是“通过全限定名来获取描述类的二进制字节流”,实现这个动作的代码就是“类加载器”了。
2.类加载器的作用
1.将类加载进虚拟机中,这也是类加载器的功能点。
2.用于区分类型信息,虚拟机中判断两个类型信息是否相等必须满足两个条件,首先类必须是相同的类,第二加载他们的类加载器也必须相同,否则这两个类依然不是相等的。下面我们自己实现个类加载器,去加载一个类,然后与系统使用默认加载器加载的该类进行比较看下。
public class TestClassLoader { public static void main(String[] args){ ClassLoader classLoader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { InputStream in = getClass().getResourceAsStream(name.substring(name.lastIndexOf(".")+1)+".class"); if (in == null){ return super.loadClass(name); } byte[] by = new byte[in.available()]; in.read(by); return defineClass(name,by,0,by.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("类未找到"); } } }; try { Object obj = classLoader.loadClass("sgcc.supplier.pojo.model.queues.TestClassLoader").newInstance(); System.out.println(obj.getClass()); System.out.println(new TestClassLoader().getClass()); System.out.println(obj instanceof sgcc.supplier.pojo.model.queues.TestClassLoader); System.out.println(new TestClassLoader() instanceof sgcc.supplier.pojo.model.queues.TestClassLoader); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
上述代码实现了一个简单的类加载器,我们使用这个类加载器加载了当前类,然后与默认加载器加载的类型相比结果如下:
class sgcc.supplier.pojo.model.queues.TestClassLoader class sgcc.supplier.pojo.model.queues.TestClassLoader false true Process finished with exit code 0
从上方输出结果可以看到无论是使用我们自己编写的类加载器加载的obj,还是使用默认加载器加载的新建对象类型信息一模一样,然后我们分别用他们与类型信息比较然后发现,我们自己加载的是false,默认的是true。到这里也验证了上面的说法,只有相同类加载器加载的同一个类型,他们才是相同的,不然他们就不是同一个类型,类加载器的这个特性,也是双亲委派模型存在的意义,往下卡就会知晓。
3.常见的类加载器
1.启动类加载器(Bootstrap Class Loader)
该启动器使用C++语言实现,主要用于加载<JAVA_HOME>\lib下的类(主要以jar包的形式存在),该类加载器不能直接被开发人员使用,也是类加载器中唯一不能被开发人员直接使用的类加载器,他的主要作用就是加载java自身的类库。
2.扩展类加载器(Extension Class Loader)
使用java代码实现的ExtClassLoader该类就是扩展类加载器,继承自ClassLoader,主要用于加载<JAVA_HOME>\lib\ext目录下的类(主要以jar包的形式存在),该目录下是支持用于对java类库进行扩展的一个类库,用户可以将自己扩展的类放入到这个目录中,就会被扩展类加载器加载进虚拟机,我们使用时就可以直接调用。该类是java代码实现是可以直接被开发人员使用的。
3.应用程序类加载器(Application Class Loader)
使用java代码实现的AppClassLoader该类就是应用程序类加载器,继承自ClassLoader,主要用于加载用户类路径上所有的类库,如果开发中不指定类加载器,那么默认使用这个类加载器。此外应用程序类加载器有时也被称为“系统类加载器”。
除了这三种类加载器以外,还可以自己定义类加载器,自己定义也很简单,我们只需要继承ClassLoader这个抽象类,并实现loadClass方法即可。
二.双亲委派模型
1.为什么需要双亲委派模型
1.用一个类加载器加载所有类不行吗
我们可以发现类加载器大致有四种,启动类加载器这是加载java类库,扩展类加载器这是加载扩展类库的,应用程序类加载器这是加载我们自己写的类的,我们也可以自己实现类加载器,这样就会有多个类加载器存在,我都用一个类加载器加载不就行了?为什么要整这么多呢?答案肯定是不能只用一个类加载器去加载所有的类的,我们前面说类加载器的作用时已经说过,类加载器还会被用来区分类型。被同一个类加载器加载的相同类就会被认为是同一个类,如果我们编写了和java类库相同的类来使用同一个类加载器,很容易就会把java类库提供的基础类搞得混乱,对虚拟机来说也是不安全的,所以才会有不同的类加载器加载不同场景的类库。
2.多个类加载器加载同一个类行吗
这么加载首先肯定没有问题,就像前面说的类加载器的作用那样,不同类加载器加载的同一个类,会被认定为不同的类型,假设我们使用了不同的类加载器加载了Object这个类,那么我们使用时就没有办法比对两个对象是不是相等了。因为无论如何他们都不会相等。所以使用多个类加载器加载同一个类也是不友好的。
3.基于上面的原因所以我们需要“双亲委派模型”
看了上面两个问题,相信很多人都已经明白,使用一个类加载器加载所有类库不现实,使用多个类加载器随意加载类库也不行,所以java在1.2时期引入了“双亲委派模型”。
2.什么是双亲委派模型
如下图展示的那样的类加载器之间的层次关系就被称为“双亲委派模型”,文字描述就是:所有的类加载器都应有自己的父类加载器,除了顶层的启动类加载器。
3.双亲委派模型的工作过程
如果一个类加载器收到了类加载的请求,他首先不是自己去加载这个类,而是将来类交给他的父类去进行加载,直到传递给了启动类加载器,若是启动类加载器加载不了则会告诉子类加载器,这是子类加载器才会去尝试加载, 若还是加载不了则继续交给子类。这一块的工作流程我们配合各个类加载器的加载范围就会很好理解,比如是一个Object类,该类属于java自身类库进行加载,所以无论是哪个类加载器收到了加载请求最后都会交由启动类加载器进行加载,启动类夹杂器一看正好是在我的加载范围内他就加载了,如果是我们自己编写的类,启动类加载器是不会加载的,最后还是会给到应用程序类加载器进行加载。
4.双亲委派模型的优点
1.使用双亲委派模型不会存在java类库被篡改的情况,保证了虚拟机的安全。
2.保证了同一个类只会被加载一次,各个类会随着类加载器的加载具备层次关系,不会出现类型紊乱。
5.破坏双亲委派模型
双亲委派模型并不是一个具有强制性约束性的模型,而是官方推荐给开发者的一种类加载器的实现方式,我们也可以完全不按照这个推荐模式来,知道JDK9java模块化出现为止,双亲委派模型出现过3次较大规模的被破坏情况。
第一次“破坏”
因为双亲委派模型是JDK1.2才出现的,而在这之前类加载器已经出现了,面对已经存在的用户自己定义的类加载器的代码,双亲委派模型的设计不得不作出了一些妥协去兼容这些已经存在的代码,无法再以技术手段避免loadClass方法被覆盖的可能性,只能新增了一个pretected的findClass方法,在调用父类加载器时,调用该方法执行自己的类加载逻辑。
第二次“破坏”
通过双亲委派模型的工作流程我们可以发现,越基础的类他的类加载器就会越靠近启动类加载器,因为这些类都是最容易被调用到的,比如Object、InputStream等等这些都是交由启动类加载器加载的,那么如果有基础类型又要调用会用户代码怎么办呢?这是到底使用什么类加载器加载这个基础类型呢?于是有了线程上下文类加载器(Thread Context ClassLoader),这个类加载器是每个线程都会有一个,可以使用Thread中的setContextClassLoader进行设置,未设置则默认使用父线程的。若是全局都未设置,则使用应用程序类加载器。这是一种父类加载器去请求子类加载器去加载类的模式,是完全与双亲委派模型相反的操作。在JDK6时这一点已经被修复了。
第三次“破坏”
就是JDK9引入模块化系统了,模块化系统要求java代码可以像外设设备一样,可以随意更换,无需在进行重启系统,这是后双亲委派模型就已经不再适用了,这时候java系统中的每一个模块都要求有一个类加载器,当需要更换模块时,就把该模块与其对应的类加载器一起替换掉。
全文总结
这篇文章主要介绍了类加载器与类加载器的一般工作模式双亲委派模型,JDK9之前我们经常用到的就是启动类加载器、扩展类加载器(ExtClassLoaader)、应用程序类加载器(AppClassLoader)还要我们自己定义的类加载器。这些类加载器通过双亲委派模型将我们写的程序中所有的类型信息分层次加载进了虚拟机中,保证了我们代码的正常有效的执行。