虚拟机类加载机制
类加载机制是指,虚拟机把描述类的数据从class文件(一串二进制的字节流,无论以何种形式存在)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。其中,产生class对象的时机是在加载阶段完成后,这个阶段完成后字节流就存储在方法区中,内存中也实例化了一个java.lang.Class对象,这个对象并没有明确规定是在堆中,hotspot的class对象就是存放在方法区中。在java语言里面,类型的加载、连接和初始化都是在程序运行期间完成的,java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接的特点实现的。
类从被加载到虚拟机内存中开始,直到卸载出内存为止,整个生命历程可以分为7个阶段:加载、验证、准备‘解析、初始化、使用和卸载。如下图所示。
其中,加载、验证、准备和初始化和卸载这5个阶段的开始顺序是固定的,但是进行顺序或完成顺序并不是固定的。它们之间通常都是互相交叉混合进行,通常会在一个阶段执行的过程中调用、激活另一个阶段。但是解析阶段是不定的顺序的,它在某些情况下可以在初始化阶段之后再开始,这样做是为了支持java语言的动态绑定。这些阶段中, 加载和连接和初始化阶段属于类加载过程。类的卸载阶段则是垃圾回收的过程。
java虚拟机规范中并没有进行强制约束什么情况下需要开始类加载的第一个阶段:加载。但是对于初始化阶段,则规定了有且只有5种情况必须立即对类进行初始化,自然而然,加载、验证、准备阶段就要在此前开始。
当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
总的来说,就是需要使用到这个类的某些信息,而这个类又没有被初始化的时候。上述的5中行为称为主动引用,除此以外的类引用都是被动引用,都不会触发初始化。有3种被动引用的例子。
详细代码可参考:https://blog.csdn.net/u012834750/article/details/70834735
加载阶段需要完成3件事情。
其中,数组类的加载过程比较复杂。因为数组类本身不通过类加载器创建,而是由java虚拟机直接创建。
1)如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型,就是指数组元素类型)是引用类型,那就递归采用本节定义的加载过程去加载这个组件类型,数组C将在加载该组建类型的类加载器的类名称空间上被标识(一个类必须与类加载器一起确定唯一性)。
2)如果数组的组件类型不是引用类型(例如int[]数组),java虚拟机将会把数组C标记为与引导类加载器关联。
3)数组类的可见性与它的组建类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。
整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这里所说的类变量是指被static修饰的变量,不包括实例变量。这里所说的初始值“通常情况下”是数据类型的零值。例外情况就是被final修饰的字段,该字段的初始值取决于代码。
解析阶段是虚拟机将常量池里的符号引用替换为直接引用的过程。这里解释一下符号引用和直接引用的意思。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。这里介绍前面4种。
1. 类或接口的解析。假设当前所处的类为d,如果要把一个从未解析过得符号引用N解析为一个类或接口c的直接引用。需要完成以下3个步骤。
1) 如果c不是一个数组类型,那么僵代表N的全限定名传递给d的类加载器去加载这个类。
2) 如果c是一个数组类型,并且数组的元素类型是对象。那么就去加载这个数组元素类型,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
返回直接引用之后,还要进行符号引用验证,验证d是否有权限访问此类。
2. 字段解析。首先解析字段所属的类或接口的符号引用。
1)查找自身,看c本身是否含有这个简单名称和字段描述符都匹配的字段。有则返回直接引用。
2)按继承关系从下往上递归查找接口。找到则返回,查找结束。
3)按继承关系从下往上递归查找父类。直到java.lang.Object。
4) 否则查找失败。抛java.lang.NoSuchFieldError异常。
当查找成功,需要进行权限验证。
3. 类方法解析。首先也是先解析方法所属的类或接口的符号引用。
1)如果发现c是接口,抛异常。
2) 查找自身。
3) 在父类中查找。
4) 在接口中查找,找到则说明类c是个抽象类,抛java.lang.AbstractMethodError异常。
5) 查找失败,抛异常。
查找成功则进行权限验证。
4. 接口方法解析。同样首先解析方法所属的类或接口的符号引用。
1) 如果c是个类则抛异常。
2) 查找自身。
3) 查找父接口,直到java.lang.Object类。
4) 查找失败。
由于接口中所有方法默认都是public的,所以不需要验证权限。
初始化阶段才真正开始执行类定义中定义的java程序代码。这个阶段简单来说就是执行类构造器<clinit>()方法的过程。那么<clinit>()方法怎么来的呢?
在加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”的这个动作是放到java虚拟机外部实现的,实现这个动作的代码模块称为“类加载器”。
类加载器与类的关系:对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。也就是说,每一个类加载器,都对应着一个独立的类命名空间。当使用class对象的equals()方法,包括instanceof 关键字做对象所属关系判定情况时,都需要注意到类加载器的影响。
从java虚拟机角度讲,类加载器只存在两种不同的类型。
1. 自身的一部分的加载器:启动类加载器,这个类加载器用c++语言实现。
2. 所有的其他的类加载器。这些类加载器由java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。
从java开发人员角度看,可以分为3种类加载器。
1)启动类加载器,负责加载存放在%JAVA_HOME%\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库加载到虚拟机的内存中,启动类加载器无法被java程序直接引用。用户在编写自定义类加载器时,如果需要把加载器请求委派给引导类加载器,那直接使用null代替即可。
2)Extension ClassLoader:扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
3)Application ClassLoader:应用程序类加载器,由sun.misc.Launcher $App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型
用我们自定义的加载器的话,会有如上图所示的层次关系。这种关系是用组合来实现的。双亲委派模型并不是说有两个层次的父类,而是parents,指有很多代的意思。
双亲委派模型工作过程:
(1)如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
(2)每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器。
(3)如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。
双亲委派 模式的类加载机制的优点是java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。
第一次:jdk1.2之前存在的loadclass()方法。补救措施是findClass()方法。
第二次:当基础类想要调用回下层的用户代码时无法委派子类加载器进行类加载,例如JNDI服务。为了解决这个问题JDK引入了ThreadContext线程上下文,通过线程上下文的setContextClassLoader方法可以设置线程上下文类加载器。