类生命周期
在Java 虚拟机中,类的生命周期包括7个阶段:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。
加载
类的加载过程主要完成3件事:
- 通过类的全限定名获取该类的二进制字节流,这个过程并没有规定从哪里获取,具有很大的操作空间。
- 将字节流的静态存储结构转为方法区或元空间的动态存储结构。
- 生成这个类的Class对象,作为该类各种数据的访问入口。
验证
确保字节流中包含的信息符合虚拟机的要求,不会危害到虚拟机。主要包括文件格式验证,验证是否以魔数开头,版本号是否被虚拟机接收;元数据验证,验证类的方法,字段是否与父类矛盾;字节码验证,通过数据流与控制流分析,确定程序语义,不会危害虚拟机。
准备
为类变量分配内存并设置默认零值,类变量是指 static 修饰的变量,如果类变量是常量,会初始化为定义的值。
解析
将能替换的符号引用替换为直接引用,为了支持多态,有的符号引用在这个阶段不能被替换掉。
初始化
在这个阶段虚拟机会收集赋值语句与静态语句合并为一个clinit方法执行,为类的静态赋予正确的初始值。静态语句的顺序由源文件的顺序决定。
执行一个类的clinit方法前,会先执行父类的clinit方法(故虚拟机中第一个执行的类是Object),但是接口不需要,接口的实现类也不需要。只有接口中定义的变量使用时,才会执行接口的clinit方法。
虚拟机会保证一个类的clinit方法在多线程环境下被正确的加锁和同步。
类初始化时机
主动引用
虚拟机规定在以下五种情况下,必须立即进行初始化:
- 使用 new 关键字实例化对象时,访问类的静态字段,调用类的静态方法时。
- 使用java.lang.reflect 包的方法对类进行反射调用时。
- 初始化一个类时,它的父类还没有被初始化。
- 虚拟机启动时,包含 main() 方法的主类会先被初始化。
- java.lang.invoke.MethodHandle 解析结果涉及静态字段与静态方法时,对应的类应该先被初始化。
被动引用
处理主动引用外的所有引用方式都不会立即触发初始化,称为被动以用,比如:
- 用过字类引用父类的静态字段,子类不会立即初始化
1 | Sytem.out.println(SubClass.value); // value是父类的静态字段 |
- 通过数组定义引用类,不会触发类的初始化,会触发数组类的初始化,数组类是虚拟机自动生成的,包含了数组的属性和方法。
1 | OneClass[] os = new OneClass[100]; |
- 引用一个类的常量也不会导致类的初始化,常量在编译阶段就存入常量池中。
1 | System.out.println(OneClass.MAX_VALUE); |
类加载器
类加载器可以加载类,每一个类加载器都有独立的命名空间,对于任意一个类,它的类加载器和它本身确定唯一性。
默认类加载器
启动类加载器 (Bootstrap ClassLoader)负责将存放在 JRE_HOME 下的lib目录中的指定类库加载,用户无法直接引用这个加载器,如果用户需要把加载请求委派给启动类加载器,用null代替即可。
扩展类加载器负责加载指定扩展目录下的类库。
应用程序类加载器,也叫系统类加载器,根据程序的类路径加载Java类,是程序默认的类加载器。用户自定义的加载器默认父加载器是应用程序类加载器。
双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,所有加载器都有父类加载器,在收到类加载请求时,优先将请求委派给父加载器,父加载器无法处理时,子加载器才会自己加载类。这样做的好处是Java类随着它的类加载器具备一种天然的优先级层次。
线程上下文类加载器
线程上下文类加载器能打破双亲委派模型的桎梏,在一些场景十分有用,比如web服务器。通过Thread.currentThread.setContextClassLoader() 与 Thread.currentThread.getContextClassLoader() 使用线程上下文类加载器。