本文共 2332 字,大约阅读时间需要 7 分钟。
完整的流程图如下所示
第一个环节:加载阶段
1、通过一个类的全限定名获取定义此类的二进制字节流,ClassLoader。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
第二个环节:链接阶段
验证 Verify
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
准备 Prepare
为类变量分配内存并且设置该类变量的默认初始值,即零值。
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
public class HelloApp { private static int a = 1; // 准备阶段为0,在下个阶段,也就是初始化的时候才是1 public static void main(String[] args) { System.out.println(a); }}
上面的变量a在准备阶段会赋初始值,但不是1,而是0。
解析 Resolve
将常量池内的符号引用转换为直接引用的过程。
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
第三个环节:初始化阶段
初始化阶段就是执行类构造器法<clinit>()的过程。
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
-
也就是说,当我们代码中包含static变量的时候,就会有clinit方法
public class Main { private int b = 1; public static void main(String[] args) { int a = 0; }}
显然上面的代码不会生成<clinit>方法
构造器方法中指令按语句在源文件中出现的顺序执行。
而<init>就是我们熟知的构造器
<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
public class ClinitTest1 { static class Father { public static int A = 1; static { A = 2; } } static class Son extends Father { public static int b = A; } public static void main(String[] args) { System.out.println(Son.b); }}
我们输出结果为 2,也就是说首先加载ClinitTest1的时候,会找到main方法,然后执行Son的初始化,但是Son继承了Father,因此还需要执行Father的初始化,同时将A赋值为2。我们通过反编译得到Father的加载过程,首先我们看到原来的值被赋值成1,然后又被复制成2,最后返回.
虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。即保证静态代码仅仅被执行一次。
public class DeadThreadTest { public static void main(String[] args) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 线程t1开始"); new DeadThread(); }, "t1").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 线程t2开始"); new DeadThread(); }, "t2").start(); }}class DeadThread { static { if (true) { System.out.println(Thread.currentThread().getName() + "\t 初始化当前类"); while(true) { } } }}
线程t1开始
线程t2开始 线程t2 初始化当前类
从上面可以看出初始化后,只能够执行一次初始化,这也就是同步加锁的过程
转载地址:https://blog.csdn.net/weixin_58104242/article/details/122562220 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!