类的加载机制

一 类加载机制概念

java虚拟机把描述类的数据从class文件加载到内存,并对数据进行验证,准备,解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的加载机制。

Class文件由类加载器装载后,在JVM中将形成一份描述class结构的元信息对象,通过该元信息对象可以获知class的结构信息: 如构造函数,属性和方法等,java允许用户借由这个class相关的元信息对象间接调用class对象的功能,这就是我们经常见到的class类

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。其中验证、准备和解析三个部分统称为连接(Linking),这七个阶段的发生顺序如下图所示:

二 工作机制

类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示的对象组件。在Java中,类装载器把一个类装入JVM中,要经过以下步骤:

(1) 装载:查找和导入Class文件;

 (2) 链接:把类的二进制数据合并到JRE中;

    (a)校验:检查载入Class文件数据的正确性;

    (b)准备:给类的静态变量分配存储空间;

    (c)解析:将符号引用转成直接引用;

 (3) 初始化:对类的静态变量,静态代码块执行初始化操作

Java程序可以动态扩展是由运行期动态加载和动态链接实现的;比如:如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现(多态),解析过程有时候还可以在初始化之后执行;比如:动态绑定(多态)

如上图所示,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这个顺序来按部就班地开始,而解析阶段则不一定,它在某些情况下可以在初始化阶段后再开始。 
类的生命周期的每一个阶段通常都是互相交叉混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。

 三 各个过程的详解

1 加载

加载时整合类加载的第一个阶段,虚拟机需要完成下面三件事情:

1 通过一个类的全限定名来获取其定义的二进制字节流

2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

整个类加载过程中,除了在加载节点用户程序可以自定义类加载器参与之外,其余所有的动作完全由虚拟机主导和控制。

那么方法区是什么呢?

方法区是系统分配的一个内存逻辑区域,是用来存储类型信息的(可以理解为类的描述信息),包括:

(1) 常量池

(2) 类的全限定名,即全路径名,如java/lang/Object

(3) 字段,方法信息,类变量信息

等等

2 链接

(1) 验证

验证是为了确保class文件中的字节流符合虚拟机的要求,并且不会危害虚拟机的安全

(2) 准备 

jvm会在准备阶段给类变量分配内存并设置类变量的初始值。注意这里只是赋初始值,比如pubic static int value = 123 这句话中,在执行准备阶段的时候,会给value分配内存并设置初始值为0,而不是123

(3)解析 

jvm将常量池中的引用转换为直接引用的过程,对于一些还没有被用到的引用类型,其解析可能会推迟到使用的时候

3 初始化

类初始化是类加载的最后阶段(这里没有算上之后的使用和卸载)。在这个阶段,jvm才开始真正执行类定义中的java程序代码

(1) 在遇到new, getstatic, putstatic, invokestatic 这四条字节码指令时,如果类还没有进行过初始化,则需要先触发其初始化。常见的触发这些指令的java场景有:

    a) new关键字实例化对象

    b) 读取或设置一个类变量(static)时,final类变量除外,因为编译器已经把值放入常量池

    c) 调用一个类的静态方法时

(2) 使用java.lang.reflect包的方法对类进行调用时,如果类还没有进行过初始化,则需要先触发其初始化

(3) 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化

(4) 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先执行该主类

 三 类加载器的开放性和唯一性

1 开放性

虚拟机规范并没有指明二进制字节流要从一个Class文件获取,或者说根本没有指明从哪里获取、怎样获取。这种开放使得Java在很多领域得到充分运用,例如:

  • 从ZIP包中读取,这很常见,成为JAR,EAR,WAR格式的基础
  • 从网络中获取,最典型的应用就是Applet
  • 运行时计算生成,最典型的是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流
  • 有其他文件生成,最典型的JSP应用,由JSP文件生成对应的Class类 
    ……

2 唯一性

类加载器虽然只用于实现类的加载动作,但是对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性。通俗的说,JVM中两个类是否“相等”,首先就必须是同一个类加载器加载的,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要类加载器不同,那么这两个类必定是不相等的。

这里的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

关于类初始化有几个demo可以看下这篇 http://blog.csdn.net/ns_code/article/details/17845821 

四 双亲委派模型

1 类加载器

引导类加载器(bootstrap classloader): 它用来加载java的核心库,使用原生代码实现的(HotSpot虚拟机中使用C++实现),并不继承自java.lang.ClassLoader 

扩展类加载器(extensions classloader): 它用来加载java的扩展库, 它负责将JAVA_HOME/lib/ext或者由系统变量制定位置中的类库加载到内存。

系统/应用程序类加载器(system classloader): 它是根据kava应用的类路径(classpath)来加载java类。一般来说java应用的类都是由它来完成加载的。可以通过ClassLoader中的getSystemClassLoader()方法获取它

自定义类加载器: 除了上面系统提供的类加载器以外, 开发者可以通过继承java.lang.ClassLoader类实现自己的类加载器,以满足一些特殊需求。他们之前的层次关系被类加载器的双亲委派模式。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(inheritance).

2 双亲委派模型过程

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

3 双亲委派模型的系统实现

在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

protected synchronized Class<?> loadClass(String name,boolean resolve)throws ClassNotFoundException{
    //check the class has been loaded or not
    Class c = findLoadedClass(name);
    if(c == null){
        try{
            if(parent != null){
                c = parent.loadClass(name,false);
            }else{
                c = findBootstrapClassOrNull(name);
            }
        }catch(ClassNotFoundException e){
            //if throws the exception ,the father can not complete the load
        }
        if(c == null){
            c = findClass(name);
        }
    }
    if(resolve){
        resolveClass(c);
    }
    return c;
}
View Code

某一个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,则成功返回;如果父类加载器无法完成加载任务,将抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载,依次类推。

注意,双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。以Tomcat为例,每个web应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它首先尝试去加载某个类,如果找不到再代理给父类加载器,这与一般类加载器的顺序是相反的。这是java servlet规范中的推荐做法,其目的是使得web应用自己的类的优先级高于web容器提供的类。

原文地址:https://www.cnblogs.com/balfish/p/8533994.html