类加载时机
类的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载。加载可以有实现方式,比如本地加载类、或者通过网络加载类,具体实现自由。对于初始化阶段,虚拟机规范规定有且只有5中情况,如果没有初始化,则必须对类进行初始化:
- 遇到new,getstatic,putstat或invokestatic指令时
- 使用反射进行调用时
- 当初始化类时,其父类没有初始化,则需触发父类初始化
- 用户指定了一个主类,vm会先初始化这个主类
- 一个MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄, 并且这个方法句柄对应的类没有初始化时
类加载过程
加载过程包括:加载,验证,准备,解析和初始化
加载
加载阶段要做的就是
- 通过一个类的全限定名来获取类的二进制字节流;
- 将这个字节流转化为方法区的运行时数据结构;
- 生成这个类的Class对象,作为方法区的这个类的各种数据的入口
验证
验证是保证Class文件的字节流中包含的信息符合虚拟机的规定,包括
- 文件格式验证,验证字节流是否符合Class文件格式的规范,如是否以魔数0XCAFEBABE开头
- 元数据验证,对字节码描述的信息进行语义分析,如 这个类是否继承了final描述的类
- 字节码验证,通过数据流和控制流分析,确定程序语义是合法的,如方法体内的类型转化是否有效
准备
准备阶段是正式为类变量分配内存,并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里仅包括类变量,而不是实例变量。如 public static int val = 123;在准备阶段 val的初始值为0,而不是123。
解析
解析阶段是虚拟机讲常量池内的符号引用替换为直接引用。符号引用是用一组符号描述所引用的目标对象,跟内存布局无关;而直接引用可以是指向目标对象的指针、相对偏移量或者一个间接定位的目标的句柄,跟内存布局有关。
解析包括:类或接口的解析、字段解析、类方法解析、接口方法解析等
初始化
初始化阶段是真正执行类中定义的Java字节码,这个阶段是执行类构造器 clinit()方法初始化类变量的阶段,但clinit()并非必需,如果没有静态代码块或者类变量,就可以没有clinit()
双亲委派模型
类加载器
Java虚拟机有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个是使用C++实现的,是虚拟机自身的一部分;另一种是所有其他的类加载器,由Java语言实现,继承自java.lang.ClassLoader。
在Java程序中,比较两个类是否相等,只有在这两个类是有同一个类加载器加载的前提下才有意义。
- 启动类加载器:负责加载JAVA_HOME\lib目录下的,或被-Xbootclasspath参数所指定的类库
- 扩展类加载器:负责加载JAVA_HOME\lib\ext目录中,或被java.ext.dirs系统变量所指定的类库
- 应用程序类加载器:负责加载用户类路径上所指定的类库
双亲委派模型
如图展示的类加载器之前的这种层次关系,成为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器必须有自己的父类加载器。
双亲委派模型要求:如果一个类加载器收到了类加载的请求,它首先把请求委派给父类加载器,每个层次的加载器都是如此,只有当父类加载器反馈无法完成这个加载请求,子加载器才会尝试自己去加载。
双亲委派模型并非一个强制性的约束模型,有3次规模是破坏双亲委派模型的
- 引入双亲委派模型时,为了兼容ClassLoader原有的loadClass方法
- 模型自身的缺陷:此模型很好的解决了各个类加载器的基础类的统一问题,越基础的类由越上层的加载器加载,但也有基础类调回用户代码的情况,如JNDI服务,为了解决这个问题,Java语言提供了线程上下文加载器,此加载器可以通过setContextClassLoader进行设置,作为应用程序的加载器
- 用户对程序动态性的追求:如代码热替换