概述
类加载机制是指虚拟机将描述类的数据从Class文件中加载到内存,并进行数据验证、解析、初始化等过程,最后形成可以直接被虚拟机使用的java类型。在java语言中类的加载、链接、初始化等过程并不是在编译时期完成,而是在运行时期才进行的,这样做的好处在于可以为语言提供了动态扩展的特性(可以在运行期间动态接收二进制的字节码文件解析运行),坏处在于增加了性能的开销。
类加载过程
类在jvm中的整个生命周期包括:
加载(loading)-->验证(verification)-->准备(preparation)-->解析(resolution)-->初始化(initialization)-->使用(using)-->卸载(unloading)
其中验证、准备、解析并称为链接阶段。并且加载、验证、准备、解析、初始化阶段的开始时机是确定的,当然这里不需要等待前一个完成才执行后一个。而解析过程则不一定了,因为Java语言可能会存在运行时动态解析。
加载
加载过程实际上就是通过二进制字节流生成Class对象的过程,整个过程可以分为以下阶段:
- 通过类的全限定名来获取定义该类二进制字节流,这里的字节流并不要求一定是从Class文件获取,可以从压缩包(jar,war)、其他文件生成(jsp)、网络、计算生成(proxy)等等途径获得。
- 将这个字节流所表示的静态存储结构转换为方法去的动态运行时数据结构。数据存储格式由虚拟机自行实现。
- 在内存中实例化一个java.lang.Class对象,作为方法区中该类的数据访问入口。在HotSpot虚拟机中,Class对象在JDK1.7前是存储在方法区中,在JDK1.7之后Class实例存放在堆中。
在加载过程中的获取二进制字节流阶段既可以使用JDK提供了类加载器进行加载,也可以使用我们自定义的类加载器加载。但是如果是加载一个数组类就有些不一样了。
数组类本身并不通过类加载器加载,其由虚拟机直接创建,但是数组中承载的对象还是由类加载器加载:
如果数组类承载的对象时引用类型那么就递归(数组可以嵌套)的调用加载过程对类进行加载,同时该数组将在加载被承载对象的类加载器的类名称空间中被标识。如果数组中的内容并不是引用类型(基本数据类型),那么Java虚拟机将会将该数组标记为与引导类加载器关联。
验证
验证是链接过程的开始,这一阶段的目的是为了保证Class文件的字节流中包含的信息符合虚拟机的要求,并且不会危害虚拟机的安全。
在加载一节我们说过二进制的字节流并不一定必须是通过java代码编译而来,统一通过多种形式,甚至可以直接使用十六进制数据构造。所以我们必须要对数据进行验证,否则随意的数据可能导致虚拟机崩溃。在java虚拟机规范中对数据的约束和规范规则较多,大致可以分为4种:
- 文件格式验证:主要验证字节流是否符合Class文件规范以及能否被当前版本的虚拟机处理,包括魔数验证,版本号等等。目的是为了保证输入的字节流能够正确的解析和存储于方法区内,在格式上符合描述一个java类型信息的要求。通过验证后,会将字节流信息存储于内存中的方法区中。
- 元数据验证:对字节码描述的信息进行语义分析,保证不存在不符合java语言规范的元数据。如该类是否有父类,父类是否继承了不允许继承的类等等。
- 字节码验证:通过数据流和控制流来确定程序语义是否是合法的、符合逻辑的。该阶段是对类的方法体进行校验。例如保证指令的跳转不会跳到方法体外的字节码指令上,保证类型转换时有效地等等。但是即使通过了字节码验证也并不能表示程序就是绝对无措的。因为我们的检验程序的程序本身是可能有错或不足的。
- 符号引用验证:该验证是在解析阶段进行的(类加载的过程可能是相互交叉的)。符号引用验证的目的是为了确保解析动作能正常执行。如果无法通过符号引用验证,像java.lang.NoSuchMethodError等都是在该阶段抛出的。
需要注意的是该校验并不是每一次都需要的,假设我们的程序在测试环境下多次测试都是正常的,那么我们在生产环境下可以通过-Xverify:none参数来关闭大部分的验证措施,用以提高虚拟机的类加载时间。
准备
准备阶段是正式为类变量分配内存和设置类变量初始值的阶段,这些变量所需的内存都是你在方法区进行分配。
需要注意的是这里分配内存的仅仅是类变量(static)而不包括实例变量。实例变量内存分配和赋初值是在对对象实例化阶段进行的。同时这里的赋初值也并不是设置我们在程序中定义好的值,而是零值,比如int类型的零值为0,应用类型零值为null。
但是如果是constantValue(同时被final和static进行修饰)的值则是在该阶段进行赋值的,其他类型正式赋值是在初始化阶段进行的。
解析
解析是虚拟机将常量池中的符号引用替换为直接引用的过程。解析主要的对类,接口,字段,类方法,接口方法,方法类型等符引用进行。
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能够无歧义的定位到目标即可。
- 直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。
初始化
初始化过程实际上就是对变量赋值(不是赋初值)的过程。包含所有类变量的赋值以及静态代码语句块的执行代码,包括对父类的初始化。
类加载器
在前面的加载过程中有说到“通过类的全限定名来获取该类的二进制字节码流”,这一过程的实现就是通过类加载器完成的。
类与类加载器
对于任意的一个类,都需要由该类本身和加载该类的类加载器来确定其在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。这句话的意思就是要判断两个类是否一样,不能仅仅比较两个类是否通过同一个Class文件生成的,假如两个类通过同一个Class文件生成但是各自加载他们的类加载器不一样,那么这两个类也是不相等的,在使用equals方法和instanceof关键字等时都遵循这个原则。
几种类加载器
这里介绍几种已经内置的类加载器(都是针对于HotSpot vm):
- BootStrap ClassLoader(启动类加载器):该类加载器负责加载<JAVA_HOME>/lib目录和由-Xbootclasspath参数指定的路径下的类库。但是并不是说把任意类库放在lib目录下都会被加载,必须是虚拟机内定义好的名字的类库。同时在HotSpot VM中BootStrap ClassLoader是由C++实现,所以在java程序中我们无法获取到该类加载器的实例,如果我们自定义的类加载器中需要用到BootStrap ClassLoader的话可以直接使用null代替。
- Extension ClassLoader(扩展类加载器):该类加载器负责加载<JAVA_HOME>/lib/ext目录下或者由java.ext.dirs变量指定的路径下的类库,该类加载器我们可以直接使用。
- Application ClassLoader(应用类加载器):该类加载器负责加载用户路径(ClassPath)下的类库。该类加载器也是默认的类加载器。
我们也可以继承ClassLoader类并重写findClass方法进行自定义类加载器。
双亲委派模型(Parents Delegation Model)
上面介绍的不同类加载器之间也并不是各自独立运作的,其相互之间存在着一些关系。
像上图这种层级关系被称为双亲委派模型,双亲委派模型是如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时,子加载器才会尝试自己去加载。实际上双亲委派模型的并不是指每一个类加载器都有两个父加载器,parents是指可以有多个父加载器,当然也可以只有一个。这里的翻译有点怪怪的。
双亲委派模型的好处在于通过不同层次的类加载器加载的类先天的带有一个层次关系,保证同一个类不会被多次加载,例如类java.lang.Object,它由启动类加载器加载。双亲委派模型保证任何类加载器收到的对java.lang.Object的加载请求,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
双亲委派模型的实现:
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ synchronized (getClassLoadingLock(name)) { Class c = findLoadedClass(name); //如果该类没有被加载则通过类加载器加载 //已经被加载就返回 if (c == null) { long t0 = System.nanoTime(); try { //判断父加载器是否为空 为空表示使用BootStrap ClassLoader加载 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //父加载器无法加载该类 下面有当前类加载器自己加载 } if (c == null) { long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }复制代码
双亲委派模型并不是一个强制性的约束,我们的实现理论上可以不遵循该模型理论。如果有必要的话可能会对双亲委派模型进行破坏(让父类加载器主动将类加载行为交给子类进行或者直接由当前类加载器加载不交给父类),比如JNDI,OSGI等。