概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。如下图所示:

类加载的过程
接下来我们详细了解一下,类在虚拟机中加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
加载
“加载”是“类加载”(Class Loading)过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事情:
1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
虚拟机规范的这3点要求其实并不算具体,因此虚拟机实现与具体应用的灵活度都是相当大的。比如它没有指明二进制字节流要从一个Class文件中获取,这也使得开发人员可以提供多种花样,例如:ZIP、JAR、WAR、EAR,网络中(例如,Applet)、反向代理*$Proxy、JSP还有数据库中,虽然这种方式极其少见。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
Java语言本身是相对安全的语言(相对于C/C++来说),使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的事情,如果这样做了,编译器将拒绝编译。
验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,不过从执行性能角度上讲,验证阶段的工作量在虚拟机的类加载系统中又占了相当大的一部分。
从整体上看,验证阶段大致上会完成4个阶段:文件格式验证(魔数、主次版本号等等)、元数据验证(语义分析)、字节码验证、符号引用验证。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。初始化阶段,才真正开始执行类中定义的Java程序代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主管计划去初始化变量和其他资源。
类加载器
虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何取获取所需要的类。实现这个动作的代码模块成为“类加载器”。
类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类的来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
双亲委派模型
说到双亲委派,那我们需要先了解一下在虚拟机中都存在哪些常用的类加载器。绝大部分Java程序都会使用到以下3中系统提供的类加载器。如下图所示:

1、启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;这个类加载器负责将存放在<JAVA_HOME>\lib目录中的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
2、扩展类加载器(Extension ClassLoader),这个类加载器负责加载<JAVA_HOME>\lib\ext目录中的类库,开发者可以直接使用扩展类加载器。
3、应用程序类加载器(Application ClassLoader),这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,一般情况下程序中默认的类加载器。
双亲委派模型(Parents Delegation Model),要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的父类一般不会以继承(Inheritance)的关系实现,而是使用组合(Composition)关系来复用父类加载器的代码。
双亲委派工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,直到传送顶层的启动类加载器,只有当父类加载器反馈自己无法完成这个加载请求(不在加载器搜索范围之内)时,子加载器才会尝试自己去加载。
破坏双亲委派模型
双亲 委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个莫醒醒,但也有例外。
虽然双亲委派很好地解决了各个类加载器的基础类统一的问题,但模型自身也存在缺陷。那就是父类加载器无法加载子类加载器路径中的类,很好的例子便是JDBC、JNDI、JCE。
为了解决这个问题,Java设计团队引入了一个线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以使得父类加载器请求子类加载器去完成类加载动作,这样便打破了双亲委派的层次结构。
还有一种情况被破坏是由于用户对程序动态性的追求而导致的,也就是所说的代码热替换、模块热部署等。在这种实现热部署的情况下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展成为更加复杂的网状结构。
总结
这一篇为大家详细介绍了类的加载过程,还有类加载器的工作原理。
网友评论