类加载过程
- 加载(加载IO流文件,并存储类信息在方法区)->
- 验证(格式验证,语义分析,操作验证)->
- 准备(为类中的所有静态变量分配内存空间,并为其设置一个初始值)->
- 解析(将常量池中的符号引用转为直接引用)->
- 初始化(将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作)
- static的代码(包括静态代码块)是在类加载的初始化阶段执行的。
- final修饰的变量是在类加载的加载阶段执行的,比static靠前
- 引自: https://www.cnblogs.com/xiaoxian1369/p/5498817.html
触发类加载的时机
- 当遇到字节码指令 new 、getstatic / put static (调用类的属性) 、invokeStatic(调用类的方法)时。
- 当反射调用类时。
- 引用一个小案例
-
public class Book { public static void main(String[] args) { staticFunction(); } static Book book = new Book(); static { System.out.println("书的静态代码块"); } { System.out.println("书的普通代码块"); } Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } public static void staticFunction(){ System.out.println("书的静态方法"); } int price = 110; static int amount = 112; }
上面这个例子的输出结果是:
书的普通代码块 书的构造方法 price=110,amount=0 书的静态代码块 书的静态方法
下面我们一步步来分析一下代码的整个执行流程。
在上面两个例子中,因为 main 方法所在类并没有多余的代码,我们都直接忽略了 main 方法所在类的初始化。
但在这个例子中,main 方法所在类有许多代码,我们就并不能直接忽略了。
- 当 JVM 在准备阶段的时候,便会为类变量分配内存和进行初始化。此时,我们的 book 实例变量被初始化为 null,amount 变量被初始化为 0。
- 当进入初始化阶段后,因为 Book 方法是程序的入口,根据我们上面说到的类初始化的五种情况的第四种(当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类)。所以JVM 会初始化 Book 类,即执行类构造器 。
- JVM 对 Book 类进行初始化首先是执行类构造器(按顺序收集类中所有静态代码块和类变量赋值语句就组成了类构造器 ),后执行对象的构造器(按顺序收集成员变量赋值和普通代码块,最后收集对象构造器,最终组成对象构造器 )。
-
对于 Book 类,其类构造方法()可以简单表示如下:
static Book book = new Book(); static { System.out.println("书的静态代码块"); } static int amount = 112;
于是首先执行
static Book book = new Book();
这一条语句,这条语句又触发了类的实例化。于是 JVM 执行对象构造器 ,收集后的对象构造器 代码:{ System.out.println("书的普通代码块"); } int price = 110; Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +", amount=" + amount); }
于是此时 price 赋予 110 的值,输出:「书的普通代码块」、「书的构造方法」。而此时 price 为 110 的值,而 amount 的赋值语句并未执行,所以只有在准备阶段赋予的零值,所以之后输出「price=110,amount=0」。
当类实例化完成之后,JVM 继续进行类构造器的初始化:
static Book book = new Book(); //完成类实例化 static { System.out.println("书的静态代码块"); } static int amount = 112;
即输出:「书的静态代码块」,之后对 amount 赋予 112 的值。
- 到这里,类的初始化已经完成,JVM 执行 main 方法的内容。
-
public static void main(String[] args) { staticFunction(); }
即输出:「书的静态方法」。
从上面几个例子可以看出,分析一个类的执行顺序大概可以按照如下步骤:
- 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。
- 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。
- 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。
- 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。
-
如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。