摘要:
**Java开发人员经常会在程序中遇到java.lang.ClassNotFoundException这个异常,而这个异常背后涉及的Java知识点就是我们今天要讲的主题,Java的类加载机制。 **
一、加载的五大过程
JVM类加载机制分为五个部分:加载、验证、准备、解析、初始化。下面我们就从这五个方面来看一下JVM是怎么进行类加载的。
1、加载
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口。注意这里不一定非得从一个Calss文件获取,也可以从压缩包(jar、war)中读取,在运行时动态计算生成(动态代理),或者由其他文件生成(JSP文件装换为对应的Class类。)
2、验证
验证阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3、准备
准备阶段是正式为类变量分配内存并且设置类变量的初始值阶段,即在方法区中分配这些变量锁使用的内存空间。这里需要简要说明一下类变量的初始值赋值。
// 1、变量A在准备阶段过后的初始值是0不是1998,将A赋值为1998的put static 指令是在程序被编译后,存放于类构造器<client>方法之中。 public static int A = 1998; // 2、这种情况在编译阶段会为B生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将B赋值为1998 public static final int B = 1998;
4、解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用。符号引用就是class文件中的:
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等类型的常量。
4.1 符号引用
符号引用与虚拟机实现的布局无关,引用的目标不一定要加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因此符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
4.2 直接引用
直接引用可以是指明目标的指针,相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经在内存中存在。
5、初始化
初始化阶段是类加载的最后一个阶段,前面的类加载阶段之后,处理在加载阶段可以自定义类加载器以外,其他操作都是由JVM主导。到了初始化阶段,才真正执行类中定义的Java程序代码。
5.1 类构造器
初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成。虚拟机会保证子方法执行之前,父类的方法已经执行完毕,如果一个类中没有静态变量赋值也没有静态代码块,那么编译器可以不为这个类生成方法。
注意一下几种情况不会执行类初始化:
通过子类引用父类的静态字段,只会触发父类的初始化,不会触发子类的初始化
定义对象数组,不会触发该类的初始化
常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
通过类名获取Class对象,不会触发类的初始化。
通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,这个参数是告诉虚拟机,是否要对类进行初始化。
通过ClassLoader默认的loadClass方法,也不会触发初始化动作。
二、类加载器
虚拟机设计团队将类加载动作放到JVM外部实现,以便于应用程序决定如何获取所需要的类,JVM提供了3种类加载
2.1 启动类加载器(BootStrap ClassLoader)
负责加载JAVA_HOME\lib目录中的类,或者通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
2.2 扩展类加载器(Extension ClassLoader)
负责加载JAVA_HOME\lib\ext目录中的类,或者通过java.ext.dirs系统变量指定路径中的类库。
2.3 应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上类库。
三、双亲委派模型
JVM通过双亲委派模型进行类加载,当然我们可以通过继承java.lang.ClassLoader实现自定义的类加载器。双亲委派的具体流程:当一个类收到了类加载请求,它首先不会尝试着自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都会传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(在这个类加载器路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载,采用双亲委派的好处是,一个类不管使用哪个加载器加载,最终都会依次委托到启动类加载器然后再逐级往下进行加载,这样就可以保证使用不同的类加载器加载最终得到的都是同一个Object对象。
四、 类加载机制存在的问题
4.1 类版本冲突
当类路径上存在同一个类的不同版本时,如果类加载器找到一个版本,则不再搜索加载另一个版本。
4.2 无法确定jar之间的依赖关系
现有的JAR标准中缺乏对jar文件之间依赖关系的支持,因此只有在运行时无法找到所需的类时,才会抛出java.lang.ClassNotFundException异常,这种运行时的异常,对开发人员来说是非常不友好的。
4.3 信息隐藏
如果一个jar在类路径上并且被加载,那么所有该jar中的公共类(public class)都会被加载,无法达到某些类不想加载则不被加载的效果,尽管在J2EE中改进了类加载机制,可以支持war包或者ear应用为单元进行加载,但是上述问题并没有很好的解决。
4.4 解决方案
OSGi是一个动态的Java模块(Module)系统,它规定了如何定义一个Module以及这些模块之间如何交互。每个OSGi的Java模块被称为一个bundle。每个bundle都有自己的类路径,可以精确规定哪些Java包和类可以被导出,需要导入哪些其它bundle的哪些类和包,并从而指明bundle之间的依赖关系。另外bundle可以被在运行时间安装,更新,卸载并且不影响整个应用。通过这种方式,分层的类加载机制变成了网状的类加载机制。在应用程序启动之前,OSGi就可以检测出来是否所有的依赖关系被满足,并在不满足时精确报出是哪些依赖关系没被满足。
五、OSGI
5.1 简介《OSGI》百度百科
OSGI(Open Service Gateway Initiative),是面向Java的动态模型系统,是Java动态化模块化系统的一系列规范。
5.2 动态改变构造
OSGI服务平台提供在多种网络设备上无需重启的动态改变构造的功能。为了最小化耦合度和促使这些耦合度可管理,OSGI技术提供一种面向服务的架构,它能使这些组件动态发现对方。
5.3 模块化编程与热插拔
OSGI旨在实现Java程序的模块化编程提供基础条件,基于OSGI的程序和可能可以实现模块级的热插拔功能,当程序升级更新时,可只停用、重新安装然后启动程序的其中一部分,这对企业级程序开发来说是非常具有诱惑力的特性。
OSGI描述了一个很美好的模块化开发的目标,而且定了实现这个额目标的所需要服务与架构,同时也很成熟的框架实现支持。但并非所有的应用都适合采用OSGI作为基础架构,它在提供强大功能同时,也引入额外的复杂度,因为它不遵循类加载的双亲委派模型。(这个技术现在已经不是主流方向了,RPC才是!)