[关闭]
@frank-shaw 2015-10-29T11:08:40.000000Z 字数 3860 阅读 2477

类加载器

java.类加载


类的初始化过程

JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)链接又分为三个步骤,如下图所示:

什么时候需要类的初始化?

什么时候需要开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制约束,这点交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范中严格规定了只有这五种情况必须立即对类进行“初始化”(自然,加载、验证、准备阶段自然需要在此之前开始)

  1. 遇到new getstatic putstatic invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发初始化。常见场景:使用new关键字实例化对象的时候、读取或者设置一个类的静态字段的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,发现其父类没有初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动的时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机需要先初始化这个主类。
  5. 在JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic REF_putStatic REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

除了这5种之外,其他场景都不会触发其初始化。书中《深入理解Java虚拟机》指出了3个例子说明不会触发初始化的。

  1. 通过子类引用父类的静态字段,不会导致子类初始化;
  2. 通过数组定义来引用类,不会触发此类的初始化;
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

深入理解类加载的过程

1) 装载:

a.通过一个类的全限定名来获取定义此类的二进制字节流。
b.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
c.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各个数据的访问入口。

加载完成之后,虚拟机外部的二进制字节流就按照虚拟机所需要的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class对象,这个对象将作为程序访问方法区中的这些类型数据的访问入口。

2)链接:

验证:确保被加载类的正确性;
准备:为类的静态变量分配内存,并将其初始化为默认值;(只包含类变量,不包含实例变量)
解析:把类中的符号引用转换为直接引用;

3)初始化:为类的静态变量赋予正确的初始值;

那为什么我要有验证这一步骤呢?首先如果由编译器生成的class文件,它肯定是符合JVM字节码格式的,但是万一有高手自己写一个class文件,让JVM加载并运行,用于恶意用途,就不妙了,因此这个class文件要先过验证这一关,不符合的话不会让它继续执行的,也是为了安全考虑吧。

准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。

类加载器

而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的

虚拟机设计团队把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为"类加载器"。

类加载器虽然只是用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否"相等",只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

这里所指的相等,包括代表类的Class对象的equals()方法、isassignalbleFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果。

双亲委派模型

从虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Boostrap ClassLoader),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都是由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader.

从Java开发人员的角度来看,类加载器还可以划分得更加细致一些,绝大部分Java程序都会使用到以下3种系统提供的类加载器。

我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些加载器之间的关系一般如下图所示,这种层次关系被称为类加载器的双亲委派模型。

双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是都使用组合关系来复用父加载器的代码.

工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

好处:
Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Object,它放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类.

判断两个类是否相同是通过classloader.class(this.getClass.getClassLoader();)这种方式进行的,所以哪怕是同一个class文件如果被两个classloader加载,那么他们也是不同的类

双亲委派模式很好地解决了各个类加载器的基础类统一问题,越基础的类由越上层的类加载器进行加载,但是这个基础类统一有一个不足,当基础类想要调用回下层的用户代码时无法委派子类加载器进行类加载。为了解决这个问题JDK引入了ThreadContext线程上下文,通过线程上下文的setContextClassLoader方法可以设置线程上下文类加载器。
JavaEE只是一个规范,sun公司只给出了接口规范,具体的实现由各个厂商进行实现,因此JNDI,JDBC,JAXB等这些第三方的实现库就可以被JDK的类库所调用。线程上下文类加载器也没有遵循双亲委派模型。

实现自己的类加载逻辑
只需要继承ClassLoader,并覆盖findClass方法。
在调用loadClass方法时,会先根据委派模型在父加载器中加载,如果加载失败,则会调用自己的findClass方法来完成加载。
如何返回这个类呢?findClass这个方法就是根据name来查找到class文件,在loadClass方法中用到,所以我们只能重写这个方法了,只要在这个方法中找到class文件,再将它用defineClass方法返回一个Class对象即可。(defineClass这个方法很简单就是将class文件的字节数组编程一个class对象,这个方法肯定不能重写,内部实现是在C/C++代码中实现的)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注