Java语言支持动态加载和动态连接,这意味着我们可以编写一个面向接口的应用程序,在运行时再指定具体的实现类。这种支持动态扩展的语言特性使得Java应用程序具备高度的灵活性。本文将对Java的类加载机制做简要的分析,包括类加载的过程和类加载器。
类加载的过程
下图描绘了Java中类的生命周期:
类的完整生命周期包括:加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)总共7个阶段。其中验证,准备,解析3个阶段被统称为连接(Linking)。这3个阶段,再加上加载和初始化阶段,即为完整的类加载过程。接下来我们简要介绍下这5个阶段的具体操作。
- 加载
在加载阶段,虚拟机会完成以下3件事情:
通过一个类的全限定名来获取定义此类的二进制字节流;
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口; - 验证
验证阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。检验动作大致包括:文件格式验证,元数据验证,字节码验证,符号引用验证。 - 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都在方法区进行分配。 - 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 - 初始化
初始化阶段是根据程序代码初始化类变量和其他资源的过程。前面提到准备阶段会为类变量设置初始值,那里的初始值和这里的初始化类变量并不是一个意思。用一个简单的例子来说明:
public static int value = 123;
变量value在准备阶段会被设置为0(初始值通常是零值),在初始化阶段才会被赋值为123。
对于初始化阶段,虚拟机严格规定了有且只有5种情况必须立即对类进行“初始化”:
1.遇到new,getstatic,putstatic或者invokestatic这4条字节码指令时,如果类没有进行过初始化,则要触发初始化。生成这4条指令的常见场景是:使用new关键字实例化对象,读取或设置一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外),调用一个类的静态方法。
2.使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行过初始化,则要触发初始化。
3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则触发其父类的初始化。
4.当虚拟机启动时,用户指定的执行主类需要先初始化;
5.当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则要触发初始化。
笼而统之地说,一个类只有在真正被用到的时候才会被初始化,这就是Java动态特性在类加载方面的体现。
类加载器
上段提到在加载阶段虚拟机会“通过一个类的全限定名来获取定义此类的二进制字节流”,实现这个动作的模块被称为“类加载器”。
Java中有4种类加载器:
1.启动类加载器(Bootstrap ClassLoader),负责加载$JAVA_HOME/lib目录中的类;
2.扩展类加载器(Extension ClassLoader),负载加载$JAVA_HOME/lib/ext目录中的类;
3.应用程序加载器(Application ClassLoader),负责加载用户ClassPath上所指定的类;
4.用户自定义的类加载器(User ClassLoader),某些时候,用户并不想使用默认的应用程序加载器来加载自己的程序,Java允许用户实现自定义的类加载器来满足这种需求;
上述类加载器的关系一般如下图所示:
图中展示的层次关系被称为类加载器的双亲委派模型。这个模型的工作过程是:如果一个类加载器收到了类加载的请求,它会把请求委派给父类加载器去完成,只有当父类加载器无法完成加载请求时,子加载器才会自己去加载。
使用双亲委派模型的好处是Java系统类库的类不会被用户自定义的同名类替换。例如java.lang.Object,无论哪一个类加载器加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。