本文主要是介绍Java的类加载机制-双亲委派,破坏双亲委派,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
思路:尝试着从这5个方面(what,where,when,how, why)描述这个过程。
- (what) 什么是类加载机制:
如果我们想要运行一个类,必须通过JVM把class文件加载到内存然后转换成一个Class对象的过程叫做类加载。
- (where) 类加载过程中会涉及到什么地方
这个我们就得用倒着的思路思考一下,生成的一个类会包含哪些东西:类中的成员方法、成员变量、类和接口的符号引用、以及类的父类等等,这些东西存在什么地方,那么类加载的过程就会使用什么地方。从这个思路出发,我们能够得到类加载过程中会涉及到的地方:方法区、堆、栈、本地方法栈、PC寄存器。
- (when) 什么时候会发生这个类加载过程
类加载的目的就是得到一个Class对象,因此,当我们在程序中 new Object() 或者调用 Class.forName()、类名.class、对象名.getClass() 的时候,会发生类加载过程。(静态块仅在类加载时执行一次,若类已加载便不再重复执行;而动态构造块在每次new对象时均会执行)
- (how) JVM 是怎么加载类的?
在回答这个问题之前,得先搞清楚,是谁加载类的或者用什么来加载类。在虚拟机的角度上,只存在两种不同的类加载器:
1.启动类加载器(Bootstrap ClassLoader),这个类加载器有c++语言实现,是虚拟机自身的一部分。
2.其他继承ClassLoader的类加载器,由Java语言实现。
从Java开发人员的角度来划分,可以划分的更细一些
1,启动类加载器(Bootstrap Classloader):这个类加载器负责将放置在\lib目录中的,或者被-Xbootclasspath参数所指定路径中的,并且是虚拟机能识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放置在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接使用。
2,扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
3,应用程序类加载器(Application ClassLoader):这个类加载器由sum.misc.Launcher.$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也被称为系统类加载器。它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
接下来就是要搞清楚用加载器怎么加载类的
这个过程Java采用了双亲委托机制,简单来讲:类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类(图片来源:http://www.hollischuang.com/archives/199)
到此,类加载的大致过程也差不多就这样了,但是我们就不想一想:JVM加载class文件的原理机制?
本质就是通过把二进制文件流加载到内存,然后解析成一个class对象,具体过程如下(下面引用:http://www.hollischuang.com/archives/199):1、装载:查找和导入Class文件(findClass()) 2、链接:其中解析步骤是可以选择的(defineClass()) (a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用 3、初始化:对静态变量,静态代码块执行初始化工作
- (why) 最后一问:那么为什么会产生出双亲委派模型呢?会被破坏不?
主要原因是安全。
黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器 加载系统的java.lang.String类,最终自定义的类加载器 无法加载java.lang.String类。
一共有三次破坏[后文引用自:https://blog.csdn.net/zhangcanyan/article/details/78993959]
第一次:双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前–即JDK1.2发布之前。由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则是JDK1.0时候就已经存在,面对已经存在 的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK1.2之后的java.lang.ClassLoader添加了一个新的proceted方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。JDK1.2之后已不再提倡用户再去覆盖loadClass()方法,应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型的。
第二次:双亲委派模型的第二次“被破坏”是这个模型自身的缺陷所导致的,双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢。
这并非是不可能的事情,一个典型的例子便是JNDI服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”之些代码,该怎么办?
为了解决这个困境,Java设计团队只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。了有线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
第三次: 双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的,例如OSGi的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。
至此,总结完毕。致敬分享精神!
这篇关于Java的类加载机制-双亲委派,破坏双亲委派的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!