类加载

一、类加载机制

JVM把class文件加载到内存,并对数据进行校验、准备、解析、初始化,最终形成JVM可以直接使用的Java类型的过程。‘

 
类加载全过程

1、加载

将class字节码文件加载到内存中,并将这些数据转换成方法区中的运行时数据(静态变量、静态代码块、常量池等),在堆中生成一个Class类对象代表这个类(反射原理),作为方法区类数据的访问入口。

 
类的加载原理

2、链接

将Java类的二进制代码合并到JVM的运行状态之中。
• 验证
确保加载的类信息符合JVM规范,没有安全方面的问题。验证字节码文件的准确性,包含文件格式,元数据,符号引用,字节码等等
• 准备
正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注意此时的设置初始值为默认值,具体赋值在初始化阶段完成。给类中的静态变量分配内存,并赋予初始值
• 解析
虚拟机常量池内的符号引用替换为直接引用(地址引用)的过程。

3、初始化

初始化阶段是执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。

初始化过程就会对类中的静态变量初始化为指定的值,执行静态代码块,执行构造器

  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先初始化其父类。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

 

二.类加载器的种类

1.引导类加载器:负责加载JRE核心类库,jre包下rt.jar,charsets.jar等,C++语言编写,无法直接访问

2.扩展类加载器ExtClassLoader:负责加载JRE扩展目录ext中的jar包
3.系统类加载器AppClassLoader:负责加载classPath路径下的类包
4.自定义类记载器:负责加载用户自定义下的类包
实现自定义类加载器:extends ClassLoader,重写loadClass方法进行类的加载

 

 

三.类加载机制(重点)
1.全盘负责委托机制
当进行类加载的时候,如果手动指定了ClassLoader,那么该类所依赖和引用的类也由这个类加载器进行加载
User->UserParent
指定User使用特定的类加载器,那么跟User类有依赖和引用关系的类也用这个类加载器进行加载

 

“全盘负责”是指当一个ClassLoader装载一个类时,除非显示地使用另一个ClassLoader,则该类所依赖及引用的类也由这个CladdLoader载入。

例如,系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所依赖的类及引用的类也载入,依此类推。“全盘负责”机制也可称为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的类加载器。

以上步骤只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载class字节码文件生成Class对象由“双亲委派”机制完成。



2.双亲委派机制

“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

“双亲委派”机制加载Class的具体过程是:

  1. 源ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。
  2. 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖父类加载器。
  3. 依此类推,直到始祖类加载器(引用类加载器)。
  4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。
  5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。
  6. 依此类推,直到源ClassLoader。
  7. 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。

 

“双亲委派”机制只是Java推荐的机制,并不是强制的机制。

我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。


1.沙箱安全机制:自定义的String.class不会被加载,这样可以防止核心API库被随意篡改
2.避免类重复加载:当附加在其加载了该类是,就没有必要子类加载器也进行加载

双亲委派模型优点

Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都会委派给出于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类(该类具有系统的Object类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中,哈哈,那就热闹了),并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也就无法保证了,应用程序也将变得一片混乱


3.如何破坏双亲委派机制
为什么要破坏双亲委派机制:父加载器需要委托子加载器在其进行加载

如何破坏:

1.重写ClassLoad类中的loadClass方法,指定加载哪一个类
2.手动调用系统类加载器
Thread.currentThread().getContextClassLoader();
3.重写findClass

 

  双亲委托模型并不是一个强制性的约束,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过3个较大规模的“被破坏”的情况。

第一次“被破坏”

       双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在JDK 1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。

        JDK 1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

第二次“被破坏”

        双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好的解决各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为他们总是作为被用户代码调用的API,但事实往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办?

        这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,他的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,他需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码啊!那该怎么办?

        为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

        有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这汇总行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

第三次“被破坏”

        双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换(HotSwap)、模块热部署(HotDeployment)等,说白了就是希望引用程序能像我们的计算机外设那样,接上鼠标、U盘,不用重启机器就能立即使用,鼠标有问题或要升级就换个鼠标,不用停机也不用重启。对于个人计算机来说,重启一次其实没有什么大不了的,但对于一些生产系统来说,关机重启一次可能就要被列为生产事故,这种情况下热部署就对软件开发者,尤其是企业级软件开发者具有很大的吸引力。

        Sun公司所提出的JSR-294、JSR-277规范在与JCP组织的模块化规范之争中落败给JSR-291(即OSGI R4.2),虽然Sun不甘失去Java模块化的主导权,独立在发展Jigsaw项目,但目前OSGi已经称为了业界“事实上”的Java模块话标准,而OSGi实现模块化热部署的关键则是他自定义的类加载器机制的实现。每一个程序模板(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

        在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

  1. 将以java.*开头的类委派给父类加载器加载。
  2. 否则,将委派列表名单内的类委派给父类加载器加载。
  3. 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
  6. 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
  7. 否则,类查找失败。

        上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。

        虽然使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为,但这里“被破坏”并不带有贬义的感情色彩。只要有足够意义和理由,突破已有的原则就可认为是一种创新。正如OSGi中的类加载器并不符合传统的双亲委派的类加载器,并且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议,但在Java程序员中基本有一个共识:OSGi中对类加载器的使用是很值得学习的,弄懂了OSGi的实现,就可以算是掌握了类加载器的精髓。

 


四.监控类加载过程
在当前启动类当中加入-verbose:class参数,启动则可以看到整个类加载的过程

五.热部署
时时检测类,如果类发生更改则自动进行重新编译,编译之后重新加载该类

posted @ 2020-03-12 17:41  EXTRA·  阅读(133)  评论(0编辑  收藏  举报