类加载时机
- 类的生命周期:加载、验证、准备、解析、初始化、使用、卸载。其中加载、验证、准备、初始化、卸载的顺序是固定的。虚拟机没有强制规定类加载的时机。但规定当遇到主动引用时,必须要初始化。
- 遇到new、getstatic、putstatic、invokestatic指令时需要初始化
- 使用反射机制解析一个类时需要初始化
- 父类先于子类初始化(但不会要求实现的接口先初始化)
- 虚拟机执行时主类要初始化
- MethodHandle实例最后的解析结果为REF——getStatic、REF_putStatic、REF_invokeStatic方法的句柄时要初始化
- 与之相对的概念被动引用:如通过子类访问父类的类变量不会引发子类初始化,数组引用的类型、类常量被引用
类加载过程
-
加载类
- 通过一个类的全限定名获取的类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行数据结构
- 创建一个Class对像,作为方法区这个类的各种数据的访问入口
- 加载数组
- 如果数组类型是引用类型,那就递归采用类加载过程去加载这个组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识
- 如果类型不是引用类型,虚拟机将会把数组与引导加载器关联
- 数组类可见性与其组件类型的可见性一致,如果类型不是引用类型,数组类的可见性默认为public
- 加载与连接是交叉进行的
-
验证
- 文件格式验证:主要验证字节流是否符合Class文件格式的规范
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
- 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,是对类自身以外的信息进行匹配性校验。
-
准备
- 正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都在方法区中分配,为类常量分配内存并赋值。
-
解析
- 是虚拟机将常量池内的符号引用替换为直接引用的过程。
- 类或接口的解析:将符号引用代表的类及其父类和实现的接口加载到方法区同时进行验证,最后返回其直接引用
- 字段解析:会依次在类本身、类实现的接口中、类继承的父类中通过简单名称和字段描述符查找字段,找到了就返回其的直接引用
- 类方法解析:会依次在类本身、类继承的父类中通过简单名称和描述符查找字段,找到了就返回其的直接引用。否则在类实现的接口中查找,如果找到了则抛出Abstract Method异常
- 接口方法解析:依次在本接口和父接口中查找。
-
初始化
- 初始化阶段是执行类构造器<clinit>()方法的过程
- <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,可以赋值,但不能访问。
- <clinit>()方法与类的构造方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类<clinit>()方法执行之前,父类地<clinit>()方法已经执行完毕
- 所以父类地静态语句块要优于子类静态语句块
- <clinit>()方法对于类或接口来说并不是必需的。如果类中没有静态语句块,也没有类变量赋值操作,则不会生成<clinit>()方法
- 执行接口中的<clinit>()方法不需要先执行父接口的<clinit>()方法,接口的实现类在初始化时也不会执行接口中的<clinit>()方法
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁,多个线程同时去初始化一个类,那么只有一个线程执行,其他线程将会阻塞。
-
类加载器
- 每一个类都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。
- 启动类加载器:加载<JAVA_HOME>\lib目录中的或者被-Xbootclasspath参数所指定的路径中的类
- 扩展类加载器:加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
- 应用程序类加载器:负责加载用户路径(ClassPath)上所指定的类库。