JVM-类加载机制(Java类的生命周期)
1、什么是类加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
2、类加载器
2.1、类加载器的分类:
对于虚拟机的角度来看,只存在两种类加载器: 启动类加载器(Brootstrap ClassLoader)和“其他类加载器”。启动类加载器是由C++写的,属于虚拟机的一部分,其他类加载器都是由java语言实现,独立于虚拟机外部,全部继承自抽象类java.lang.ClassLoader。
从开发的角度来看,有三种类加载器:
1)启动类加载器(Bootstrap ClassLoader):这个类加载器主要是负责加载${JAVA_HOME}/lib目录的jar(比如rt.jar、resources.jar)或者被-Xbootclasspath参数所指定的路径中的jar。(调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。)
2)扩展类加载器(Extension ClassLoader):它负责加载${JAVA_HOME}/lib/ext目录或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
3)应用类加载器(Application ClassLoader):这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,也就是调用ClassLoader.getSystemClassLoader()可获取该类加载器,所以又叫系统类加载器。它负责JVM启动时加载来自命令java中的-classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。加载用户类路径(ClassPath)上所指定的类库。用户自定义的任何类加载器都将该类加载器做为它的父类加载器。
类加载器的层次结构:
2.2、类加载器(ClassLoader)加载类的原理:
ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。
3、类的加载过程(生命周期)
类的加载过程分为5个步骤:加载、连接(验证,准备,解析)、初始化、使用、卸载
Java类的生命周期就是指一个class文件从加载到卸载的全过程。
1.加载
在Java我们经常会接触到一个词--类加载,它和这里的加载并不是一回事。通常我们所说的类加载指的是类的生命周期中加载、连接、初始化三个阶段。
在加载阶段,JVM做什么工作呢?其实很简单,就是找到需要加载的类并把类的信息加载到JVM的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息
的入口。
类的加载方式比较灵活,有以下几种:
(1) 根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容。
(2) 从jar文件中读取。
(3) 从网络中获取:比如10年前十分流行的Applet。
(4) 根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。
(5) 从非class文件中获取,其实这与直接从class文件中获取的方式本质上是一样的,这些非class文件在jvm中运行之前会被转换为可被jvm所识别的字节码文件。
对于加载的时机,各个虚拟机的做法并不一样,但是有一个原则,就是当jvm“预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载。比如说,在一段代码中出现了一个类的
名字,jvm在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些jvm会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的jvm实现。我们常用
的hotspot虚拟机是采用的后者,就是说当真正用到一个类的时候才对它进行加载。
加载是类加载(Class Loading)的一个阶段,在加载阶段,虚拟机需要完成以下3件事:
1)通过一个类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访 问入口
在加载阶段,可以使用系统提供的引导类加载器来完成,也可以由用户的自定义加载器去完成,开发人员可以通过自己定义类加载器去控制字节流的获取方式(即重写一个
类加载器的loadClass()方法)(详细参考后面的类加载器)
数组对象本身不是通过类加载器创建,它是由java虚拟机直接创建的。但是数组类与类加载器任然有很密切的关系,因为数据的元素类型最终是靠类加载器去创建。
数组情况有些不同,加载阶段完成以后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,然后在内存中实例化一个java.lang.Class类的对象(对HotSpot虚拟机而言,Class对象比较特殊,
它虽然是对象,但是存放在方法区里面),这个对象将作为程序方法方法区中的这些类型数据的外部接口。
加载阶段与连接阶段的部分内容(如一部分字节码问价格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。
2.连接
2.1 验证
验证是连接阶段的第一个阶段,这个阶段的目的就是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。如果验证到输入的字节流
不符合Class文件格式的约束,虚拟机就抛出java.lang.VerifyError异常或其子类异常。
Class文件并不一定要求用Java源码编译而来,可以使用任何途径产生。
验证阶段分为四个步骤:文件格式验证、元数据验证、字节码验证、符号引用验证
1)文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
2)元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。
3)字节码验证:这个阶段是最复杂的阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
4)符号引用验证:对于虚拟机类加载机制来说,验证是一个非常重要但是不是一定必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,那么实施阶段可以考虑
使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
2.2 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。首先,这个阶段进行内存分配的仅包含类变量(用static修饰的变量),而不
包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中;其次,这里的初始值通常情况下是数据类型的零值。
例如:
public static int value = 123;
那变量value在准备阶段的过后的初始值是0,而不是123。因为这个时候还未执行任何Java方法,value值的赋值为123的动作将在初始化阶段才会执行。
在一些特殊情况下,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantVlaue属性所指定的值
例如:
public static final int value = 123;
编译时,javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123(参考例子1)
2.3 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
那什么是符号引用,什么又是直接引用呢?
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的。
比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就
可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它对同一个符号引用进行多次解析请求是
很常见的事情,虚拟机可以对第一次解析的结果进行缓存从而避免解析动作重复进行。解析动作主要针对 类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号
引用进行。
1)类或接口的解析
2)字段解析(demo:)
3)类方法解析
4)接口方法解析
字段解析的demo:
package org.burning.sport.jvm; public class FieldResolution { interface Interface0 { int A = 0; } interface Interface1 extends Interface0 { int A = 1; } interface Interface2 { int A = 2; } static class Parent implements Interface1 { public static int A = 3; } static class Sub extends Parent implements Interface2 { /** * 如果把这个变量A注释掉,会在main方法中报错,大概意思是在父类和实现的接口 * 都有变量A,这里没有Sub类中没有变量A 编译器认为是含糊不清的。 */ public static int A = 4; } public static void main(String[] args) { System.out.println(Sub.A); } }
3.初始化
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
初始化阶段是执行类构造器<clinit>()方法的过程。静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。(例子2,例子3,例子4)
类初始化的步骤:
1) 假如这个类还没有被加载和连接,那就先进行加载和连接
2)假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类
3)假如类中存在初始化语句,那就依次执行这些初始化语句
类的初始化时机:
主动使用(六种)
- 创建类的实例,就是通过new关键字实例化对象
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射。如:Class.forName()
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
除了上述六种情形,其他使用Java类的方式都被看作是被动使用,不会导致类的初始化。
当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。(例子5)
● 在初始化一个类时,并不会先初始化它所实现的接口
● 在初始化一个接口时,并不会先初始化它的父接口
因此,一个父接口并不会因为它的子接口或者实现类的初始化而初始化,只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
例子1:
package org.lee.think.in.java.type.info.chapter14;//: typeinfo/ClassInitialization.java import java.util.*; class Initable { static final int staticFinal = 47; static final int staticFinal2 = ClassInitialization.rand.nextInt(1000); static { System.out.println("Initializing Initable"); } } class Initable2 { static int staticNonFinal = 147; static { System.out.println("Initializing Initable2"); } } class Initable3 { static int staticNonFinal = 74; static { System.out.println("Initializing Initable3"); } } /** * */ public class ClassInitialization { public static Random rand = new Random(47); public static void main(String[] args) throws Exception { //仅适用.class语法来获得对类的引用不会引发初始化 Class initable = Initable.class; System.out.println("After creating Initable ref"); // 没有触发初始化。如果一个static final值是“编译器常量”, // 那么这个值不需要对Initable类进行初始化就可以被读取 System.out.println(Initable.staticFinal); System.out.println("=============="); // 触发初始化。staticFinal2不是一个编译器常量。 System.out.println(Initable.staticFinal2); System.out.println("===================="); // 触发初始化。这个变量是static域不是final的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个域分配存储空间) // 和初始化(初始化存储空间) System.out.println(Initable2.staticNonFinal); System.out.println("====================="); //Class.forName()立即就进行了初始化 Class initable3 = Class.forName("org.lee.think.in.java.type.info.chapter14.Initable3"); System.out.println("====================="); System.out.println("After creating Initable3 ref"); System.out.println(Initable3.staticNonFinal); } } /* Output: After creating Initable ref 47 ============== Initializing Initable 258 ==================== Initializing Initable2 147 ===================== Initializing Initable3 ===================== After creating Initable3 ref 74 *///:~
例子2:
public class JVMTestMain { static { i = 0; //给变量i复制可以正常编译通过 // System.out.println(i); //这句编译器会提示 "非法向前引用" } static int i = 1; }
例子3:
/** * @ProjectName: base-project * @Description: 类加载的整个过程分析 */ public class JVMTestMain2 { public static void main(String[] args) { /** * 在class Singleton中,当new Singleton()在静态变量之前时 * 输出结果是1,0 而不是1,1呢? 为什么呢? * 类加载的过程是 加载,验证,准备,解析,初始化这么几个过程 * 准备阶段: * 对于静态变量,在类的准备阶段,虚拟机是给他们自动赋一个初始值的 * 这里就会给a,b赋值为0,引用变量singleton赋值为null * 初始化阶段: * 1.先是给引用变量赋值为 new Singleton()所以在创建Singleton对象的时候 * 在Singleton的构造器中a自增为1,b自增也为1 * 2.给静态变量赋值.a没有被复制,就保持为1,b被复制为0,所以就变为0 * 所以最后的结果是1,0 * * 当 private static Singleton singleton = new Singleton(); * 在变量下面的时候,结果就会变成1,1 */ Singleton singleton = Singleton.getInstance(); System.out.println(singleton.a); System.out.println(singleton.b); } } class Singleton { private static Singleton singleton = new Singleton(); public static int a; public static int b = 0; private Singleton() { a++; b++; } public static Singleton getInstance() { return singleton; } }
例子4:
package org.burning.sport.jvm; /** * @ProjectName: base-project * @Description: 静态变量的生命语句,以及静态代码块都被看作类的初始化语句, * Java虚拟机会按照初始化语句在类文件中的先后顺序 * 来依次执行它们 */ public class JVMTestMain3 { private static int a = 1; static{ a = 2; } static { a = 3; } public static void main(String[] args) { System.out.println(a); //打印结果是3 } }
例子5:
class Parent { static int a = 1; static { System.out.println("Parent static block"); } } class Child extends Parent { static int a = 2; static { System.out.println("Child static block"); } } public class JVMTestMain5 { static { System.out.println("JVMTestMain5 static block"); } public static void main(String[] args) { System.out.println(Child.a); } } /* output: JVMTestMain5 static block Parent static block Child static block 2 */
https://gitee.com/play-happy/base-project
参考:
[1] 《Think in Java》,埃克儿,机械工业出版社
[2] 《深入理解Java虚拟机:JVM高级特性与最佳实践》,周志明 ,机械工业出版社
[3] 微信公众号,Java知音,https://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247484796&idx=1&sn=c71933213a5435f3a7dfd1ddea34dfc8&chksm=ebd63a50dca1b346ed41498c71f11e1081a675d6f0d4a95e68325e57155cb44f2d1bf4e1efa7&scene=21#wechat_redirect