第7章 虚拟机类加载机制

1. 类的生命周期

类的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载7个阶段。如下图所示


其中,加载,验证,准备,初始化,使用这5个阶段是确定的,而解析阶段则不确定,它在某些情况下可以在初始化之后再进行。这是为了支持java的动态绑定特性。 
1.1 加载
在加载阶段,虚拟机要完成以下三件事情:
1. 通过一个类的全限定名来获取此类的二进制流
2. 将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构
3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
1.2 验证
验证阶段会完成下面的四个检验动作:文件格式验证,元数据验证,字节码验证,符号引用验证。
1.3 准备
准备阶段为类变量分配内存,并设置类变量的初始值。类变量所使用的内存在方法区中进行分配(类变量指的是类中被static修饰的变量)。
public static int value = 123; // value就是一个类变量,解析阶段赋值为0;
public static final int value2 = 45; // value2是一个常量,解析阶段赋值为45;
类变量在经过准备阶段后的初始值为各种0,比如上面的value在经过准备阶段后值为0而不是123.将value赋值为123是在初始化阶段中执行<clinit>()方法中进行。
类常量在经过准备阶段后会被初始化为指定的值。
1.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号引用可以是任意形式的字面量,只要使用时能无歧义地定义到目标即可。符号引用所引用的目标不一定已经加载到内存中。符号引用与虚拟机实现的内存布局无关。
直接引用:直接引用可以是直接指向目标的指针,相对偏移量或者一个能够间接定位到目标的句柄。直接引用与虚拟机的内存布局是相关的,符号引用在不同的虚拟机上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。
动态绑定的过程就是在程序运行到此处时才开始进行解析,而静态绑定就是按照上图所示的在准备过程之后就开始解析。由此解析阶段发生的时间是不确定的。
1.5 初始化
类初始化阶段是类加载过程的最后一步。在前面的步骤中,基本是由虚拟机主导和控制的,到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量赋值动作和静态语句块顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,可以在静态语句块中赋值,但是不能访问。
<clinit>()方法的特点:
<clinit>()方法不需要显式的调用父类的构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的 <clinit>()已经实现。
<clinit>()方法对于类或者接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 <clinit>()方法。
接口中的 <clinit>()方法不需要先执行父接口中的 <clinit>()方法。只有当接口中定义的变量使用时,父接口才会初始化。
虚拟机会保证 <clinit>()方法在多线程环境中会被正确的加锁,同步。如果多个线程同时去初始化一个类,那么只会有一个线程会执行这个类的 <clinit>()方法,其余的线程阻塞,直到活动线程执行 <clinit>()方法完毕。并且需要注意的是,一旦在一个线程执行了 <clinit>()方法,其余阻塞的线程即使被唤醒了也不会再次执行 <clinit>()方法了,因为同一个类加载器下,一个类只会初始化一次。
在下面的5种情况中必须对类进行初始化:
1. 使用new关键字实例化对象,读取或者设置类的静态字段(被final修饰,已经在编译期吧结果放入常量池的静态字段除外),调用类的静态方法。
2. 对类进行反射调用的时候,如果没有初始化,则要初始化
3. 初始化某个类时,如果父类还没有初始化,则需要先初始化父类
4. 当虚拟机启动时,包含main方法的类会被初始化
5. 当时用JDK 1.7的动态语言支持时

2. 类加载器

完成”通过一个类的全限定名来获取描述此类的二进制字节流“动作的代码模块成为类加载器
1.1 类与类加载器的关系
对于任意一个类,都需要由加载它的类加载器和类本身一同确立该类在虚拟机中的唯一性。如果比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,但是只要加载它们的类加载器不同,那么这两个类就必定不相等。

3. 双亲委派模型

系统提供的类加载器有以下三种:启动类加载器(Bootstrap ClassLoader),扩展类加载器(Extension ClassLoader),应用程序类加载器(Application ClassLoader).
2.1 三种系统类加载器
启动类加载器:这个类负责将存放在<JAVA_HOM-E>\lib中,并且被虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。该加载器是使用C++实现的,是虚拟机自身的一部分。
扩展类加载器:这个类加载器负责将<JAVA_HOME>\lib\ext中的类库加载到内存中。开发者可以直接使用这个类加载器。
应用程序类加载器:这个类加载器负责加载ClassPath上所指定的类库。开发者可以直接使用这个类库,如果没有自定义过自己的类加载器,那么这个类加载器就是程序默认的类加载器
2.2 双亲委派模型
双亲委派模型的图示:
 
类加载器之间的父子关系一般不会以继承的关系来实现的,而是使用了 组合的关系来复用父加载器的。
双亲委派模型的工作过程:如果一个类加载器收到了加载类的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载类的请求最终都应该传送到顶层的启动类加载器中,只有当父加载器无法完成这个加载请求时,子加载器才会尝试亲自去加载。
双亲委派模型的好处是Java类随着他的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object,它存放在rt.java中,无论哪一个类加载器要加载这个类,最终都是先委派给启动类加载器进行加载,所以Object类在程序的各种类加载器环境中都是同一个类。


4. 破坏双亲委派模型

4.1 通过继承ClassLoader并且覆盖loadClass方法来规定自己需要的类加载方式。
  
  1. // 默认的loadClass方法中的逻辑,这个方法是public且没有被标记为final,所以可以进行覆盖。
  2. public Class loadClass(String name, boolean resolve)
  3. protected synchronized Class<?> loadClass(String name, boolean resolve)
  4. throws ClassNotFoundException
  5. { // First, check if the class has already been loaded Class c = findLoadedClass(name);
  6. // 检查class是否已经被加载过了  
  7. if (c == null)
  8. {
  9. try {
  10. if (parent != null) {
  11. c = parent.loadClass(name, false); //如果没有被加载,且指定了父类加载器,则委托父加载器加载。
  12. } else {
  13. c = findBootstrapClass0(name);//如果没有父类加载器,则委托bootstrap加载器加载 }
  14. } catch (ClassNotFoundException e) {
  15. // If still not found, then invoke findClass in order
  16. // to find the class.
  17. c = findClass(name);//如果父类加载没有加载到,则通过自己的findClass来加载。 }
  18. }
  19. if (resolve)
  20. {
  21. resolveClass(c);
  22. }
  23. return c;
  24. }
我们在定义自己的ClassLoader的时候,如果想要遵循双亲委派模型的话就只覆盖findClass()方法即可;如果不想遵循双亲委派机制,那么就要同时覆盖loadClass方法和findClass方法

4.2 线程上下文类加载器(Thread Context ClassLoader).
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等.  SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。 线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是应用程序加载类。有了线程上下文类加载器,SPI的这些服务就可以使用上下文类加载器来加载SPI实现类代码,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器。
比如JNDI,DriverManager由BootstrapLoader加载,调用的Driver具体实现由线程上下文类加载器加载,BootstrapLoader调用线程上下文类加载器。也就是父类加载器请求子类加载器去完成类加载的动作。
4.3 OSGi

参考博客:


博客中的图片大部分来自网络, 侵通删!






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值