java类加载过程

类的生命周期分为如下7个阶段,其中加载过程主要是前5个阶段

 

 

其中加载,验证,准备,初始化,卸载阶段的启动顺序是固定的。

一.加载

1.执行时间(when)

  jvm虚拟机规范中并没有进行强制约束,由各虚拟机自行实现

2.执行内容(what)

  1)获取class文件的二进制字节流

  2)将字节流代表的静态数据结构转化为方法区(jdk8后是元数据区)的运行时数据结构

  3)在方法区(jdk8后是元数据区)生成class对象,作为访问入口

3.如何执行(how)
  1)普通类加载是由类加载器(ClassLoader)完成,遵循双亲委派机制
  2)数组类本身由jvm直接在内存中动态构建,但其元素类仍由类加载器加载
4.是否必须
  是
 
二.验证
1.为什么要验证
  并不是class文件都有源码编译而来,可由包括二进制编码在内的任何形式,所有常说的“受检异常”在其他方式下可能没有强制校验,造成代码不可控
2.执行时间(when)
  加载启动之后,过程可能与加载阶段有交叉
3.执行内容(what)
  1)文件格式验证:例如是否是class文件(0xCAFEBABE开头),jdk版本号是否可接受,常量池中是否有不支持的类型,编码检查等
  2)元数据验证:例如除Object类外是否都有父类,final类是否有继承,final方法是否有重写,实现类是否实现了接口中的所有方法等
  3)字节码验证:保证类的方法在运行时,不会出现危害jvm的情况;例如保证操作数栈的数据类型与执行指令相匹配,指令不会跳转到方法体以外的指令上,有效的类型转换(父类不能赋值给 子类)等
  4)引用符号验证:发生在将符号引用转化为直接引用-转化发生在解析阶段;引用目标是否能找到,权限是否足够(public, protected, default, private)
4.是否必须:
  否,例如之前已经经过反复执行的代码说明已经通过验证,可使用-X verify:none来关闭
 
三.准备
1.执行时间(when)
  验证之后(如果未关闭的话)
2.执行内容(what)
  正式为类变量(static)分配内存并进行初始化,jdk8后,类变量随着class对象一起分配到堆中;注意,此时并没有为实例变量分配内存空间,实例变量会随着创建实例的时候一起在堆中分配
  如果是常量(static final),则分配内存后直接设置为目标值;因为常量字段在编译时会被设置一个ConstantValue属性并设置为目标值,所以在准备阶段直接设为ConstantValue属性值
3.是否必须:
  否,如果不存在类变量或常量,则不需要准备阶段
 
四.解析
1.执行内容(what)
  将常量池中的符号引用替换为直接引用,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用
  符号引用:用任意形式字面量表示所引用的目标
  直接引用:可以指向目标的指针,偏移量或句柄等
 
五.初始化 
1.介绍
  加载过程的最后一步,直到初始化,jvm才开始执行java代码,将主导权移交给程序,前几个阶段都由jvm主导(自定义classloader除外)
2.执行内容(what)
  执行static变量赋值动作和static代码块,执行顺序和源文件中出现的顺序保持一致
  父类中的static变量赋值动作和static代码块执行会优先于子类;接口不同,接口不存在static代码块,但可能存在赋值语句,父接口的赋值语句不会随着子接口的赋值语句被调用而执行
  多线程环境下,初始化动作通过加锁保证数据一致性;其当第一个线程完成初始化后,其他线程不再进行初始化操作。
3.执行时间(when)
  1)new对象时,调用静态方法时,读取或设置类字段时(final除外)
  2)reflect反射时,如果没有初始化过,则需要初始化
  3)初始化子类时,如果父类没有初始化过,则优先初始化父类
  4)执行main方法
  5)动态语言解析
  6)如果接口包含default方法实现,若该接口的子类被初始化,则需要优先初始化该接口
  有其仅有这6中方式会触发初始化(主动引用),除此之外,所有对类型的引用都不会触发初始化,称作被动引用。
4.被动引用和其他情况举例如下
  1)通过子类引用父类的static字段,子类不会初始化
  
public class SuperClass {
    public static String name = "name";
    static {
        System.out.println("superclass static method");
    }
}

public class SubClass extends SuperClass{
    static {
        System.out.println("SubClass static method");
    }
}

public class ClassloadTest {
    public static void main(String[] args) {
        System.out.println(SubClass.name);
    }
}

结果:
superclass static method
name

  2)定义类数组时,不会触发初始化

SuperClass类同上
public class ClassloadTest {
    public static void main(String[] args) {
        System.out.println("ClassloadTest");
        SuperClass[] arr = new SuperClass[10];
    }
}

结果:
ClassloadTest

 

  3) 引用类常量(编译期)时,不会触发初始化

 

  在编译期如果能确定常量值,通过传播优化,将常量直接存放到类ClassloadTest的常量池中,故对常量NAME的引用实际是引用自身常量池的NAME常量

 

public class SuperClass {
    public final static String NAME = "name";
    static {
        System.out.println("superclass static method");
    }
}

public class ClassloadTest {
    public static void main(String[] args) {
        System.out.println(SuperClass.NAME);
    }
}

结果:
name

 

  4) 引用类常量(运行期)时,会触发初始化

 

 

public class SuperClass {
    public final static String NAME = UUID.randomUUID().toString();
    static {
        System.out.println("superclass static method");
    }
}

public class ClassloadTest {
    public static void main(String[] args) {
        System.out.println(SuperClass.NAME);
    }
}

结果:
superclass static method
1ccebfe0-6026-40aa-b698-499c6dc6afd6

  5)接口初始化规则, 常量(编译器)引用

    a:接口变量默认都是public static final

    b:接口不能有static方法快

    c:当一个接口初始化时,并不要求其父接口都完成了初始化只有在真正使用到父接口的时候(如引用到接口中所定义的常量时),才会初始化(但子接口初始化,父接口一定会被加载)。

 

public interface SuperInterface {
    String STR = UUID.randomUUID().toString();

    Thread t = new Thread() {
        {
            System.out.println("SuperInterface");
        }
    };
}

public interface SubInterface extends SuperInterface{
    String NAME = "name";
    Thread t = new Thread() {{
        System.out.println("SubInterface");
    }};

}

public class IClassloadTest {
    public static void main(String[] args) {
        System.out.println(SubInterface.NAME);
    }
}


结果:
name

 

  6) 接口常量(运行期)引用,不会导致初始化父类;实现类同样不会导致接口初始化

将上述SubInterface修改如下

public interface SubInterface {
    String NAME = UUID.randomUUID().toString();

    Thread t = new Thread() {{
        System.out.println("SubInterface");
    }};
}

结果:

SubInterface
0ec42aee-8e55-4329-9a91-22d3da3cad5e

 

引用:

1.《深入理解java虚拟机》

2. https://blog.csdn.net/u013096088/article/details/79439482

posted @ 2020-04-11 20:46  Katsu  阅读(217)  评论(0编辑  收藏  举报