类加载都经历了啥

前言

前面介绍了字节码的读法,下面就是把字节码存入到内存中,那么他又是怎么加载的这些字节码文件的呢?java

大纲

什么是类加载

虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验解析和初始化,最终造成能够被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制数据结构

咱们从上面的定义能够看出来他要经历 加载到内存-->数据校验-->解析-->初始化多线程

固然上面的过程并不详细,详细的过程以下:函数

类的生命周期总共细分为7个阶段

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化(这个步骤后面不归类加载)
  • 使用
  • 卸载

个人全部文章同步更新与Github--Java-Notes,想了解JVM,HashMap源码分析,spring相关能够点个star,剑指offer题解(Java版),设计模式。能够看个人github主页,天天都在更新哟。

邀请您跟我一同完成 repo


加载

加载过程是第一步,他须要完成三个步骤

  • 经过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构
  • 内存中生成一个表明这个类的 java.lang.Class 对象,做为方法区这个类的各类数据的访问接口

加载这个过程的实现多种多样,虚拟机规范也并无对其进行明确的约束,好比第一句 "经过全限定名获取二进制流"。没有规定用设么方法获取,从哪里获取。

那么就出现了多种多样的实现方式

  • 从zip中获取,成为了往后 war,jar等格式的基础
  • 从网络中获取,典型应用 Applet
  • 由其余文件生成,JSP应用
  • 等等…….

数组类

对于一个数组类型和一个非数组类型,后者的可控性在这个阶段对于开发人员更强,

  • 由于咱们可使用系统提供的引导类加载器,

  • 也可使用本身定义的类加载器来完成(重写 loadClass完成),

非数组类

  • 自己不禁类加载器建立,由虚拟机直接建立
  • 可是他的元素类型(去掉全部维度,去掉全部[]int[]是int,int[][]是int)是由类加载器建立
  • 组件类型是去掉一维的类型,好比int[][]int[]

规则以下:

  • 组件是引用类型
    • 那就递归前面定义的加载过程去加载这个组件类型
    • 可见性:和组件类型一致
  • 若是不是引用类型(如int[]的组件类型是基本类型)
    • 与引导类加载器相关联
    • 可见性:默认为public

加载过程可能和验证阶段的一部分交叉进行,可是两个的开始时间还是保持固定的先后顺序

验证(非必要)

这个过程不必定非要进行,若是已经反复验证过,实施阶段能够经过 -Xverify:none 来关闭来进行优化。

这个阶段主要是保护虚拟机不会由于载入有害的字节流而崩溃

主要完成下列四个阶段的验证:

  • 文件格式验证

    • 是否以魔数 0xCAFEBABE开头
    • 主次版本号是否在当前虚拟机处理范围内
    • 常量池的常量中是否有不被支持的常量类型(检查常量 tag标志)
    • ……..

    只有经过了这个阶段,字节码才载入到内存中,后面的3个验证再也不对字节码进行操做,直接操做方法区的存储结构

  • 元数据验证

    • 是否有父类,(除Object类,都有父类)
    • 是否继承了不能被继承的类(如被final修饰)
    • 若是这个类不是抽象类,是否实现了其父类或接口中要求实现的全部方法
    • …….
  • 字节码验证

    主要是确保被校验的方法在运行时不会作出危害虚拟机的事件

    • 保证任意时刻操做数栈的数据类型与指令代码序列都能配合工做,如不会出现相似这样的操做,操做栈中放了一个int 类型的数据,可是使用时却使用long类型进行载入到本地变量表
    • 保证跳转指令不会跳转到方法体之外的字节码指令上
    • …….
  • 符号引用验证

    符号引用转化为直接引用,这个转化动做将发生在解析阶段。主要校验下列内容

    • 符号引用中经过字符串描述的全限定名是否能找到对应的类
    • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
    • …….

    若是不能经过验证,会抛出java.lang.IncompatibleClassChangeError异常类型的子类,如

    • Java.lang.IllegalAccessError
    • java.lang.NoSuchFieldError
    • Java.lang.NoSuchMethodError

准备

准备阶段是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存在方法区中分配

这里有两个注意点;

  • 这里是类变量(被static修饰的变量)不是实例变量

  • 设置初始值并不等于初始化,只是将其在内存中的值设置为"零值"

    ​ 例如 public static int value = 123;

    ​ 准备阶段完成以后是value 是0而不是123;

    由于这个时候还没有执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法中,因此这个会在初始化阶段执行。初始化阶段会讲到

  • 若是是被final修饰的类变量,那么准备阶段完成以后他就是123

    • public static final int value = 123;

    由于被final修饰的值是存放在 字节码文件的 ConstantValue属性表中,若是不了解这个能够看看个人这篇文章,能看懂的字节码

    各个数据类型的初始值以下表

解析

解析是虚拟机将常量池内的符号引用转换为直接引用的过程

两者异同

  • 符号引用
    • 符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可
    • 符号引用与内存布局无关,引用的目标并不必定已经加载到内存中
  • 直接引用
    • 直接引用是能够直接指向目标的指针相对偏移量或是一个能间接定位到目标的句柄
    • 直接引用和虚拟机布局相关,若是有直接引用,那么引用的目标必定存在内存中

初始化(重要)

初始化相对来讲比较重要,由于他是类加载的最后一步,也是开始真正执行类中定义的Java代码,前面的步骤除了能够本身定义类加载器以外都是由虚拟机主导或者控制的

初始化的时机(有且只有)

主动引用:

  • 遇到newgetstaticputstaticinvokestatic这四条命令的时候,若是类没有进行初始化,则进行初始化
  • 使用 java.lang.reflect包的方法对类进行反射调用的时候,若是没有,则初始化
  • 当初始化一个类时,若是发现父类没有进行初始化,则须要先触发其父类的初始化
  • 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机先初始化这个主类
  • 当使用 JDK1.7之后的动态语言支持时,若是一个 java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行初始化,则先触发初始化

这5类场景被称为主动引用,除此以外,全部引用类的方法都不会触发初始化,成为被动引用

《深刻理解Java虚拟机》这本书,这段话放在了前面,不过我以为这个应该放在后面,由于没有以前的准备阶段,咱们并不知道初始化阶段是执行类构造器<clinit>()。因此他举得例子有些就看不懂,为啥要用static代码块

<clinit>()包含的内容、执行顺序等等,这些也须要先了解

Clinit()方法

  • 内容
    • 他是由编译器自动收集类中的全部类变量的赋值动做静态语句块(static{})
  • 执行顺序
    • 编译器的收集顺序由语句在源文件中出现顺序决定
    • 静态语句块中只能访问到定义在静态语句块以前的变量
    • 和类的构造函数(实例构造器<init>()方法)不一样,他不须要显示的调用父类构造器,虚拟机会保证子类构造器方法执行前,父类构造器先执行。
      • 也就是说,父类的静态代码块中的内容必定是先于类的变量赋值。后面会有示例
  • 类和接口的区别
    • <clinit>()方法对于类或接口来讲并非必须的,若是一个类中没有对变量的赋值操做,那么编译器能够不为这个类生成<clinit>()方法
    • 执行接口的clinit方法不须要先执行父类接口的<clinit>方法。只有当父接口中定义的变量使用时,父接口才被初始化(对比主动引用的第三条)
    • 接口的实现类在初始化时也不会执行接口的<clinit>()方法
  • 加锁
    • 虚拟机会保证多线程环境下一个类的 <clinit>()会被正确的加锁、同步,
    • 若是多线程同时初始化一个类,只有一个线程会去执行<clinit>()方法,其余线程会被阻塞
    • 若是执行时间很长,那么会形成线程阻塞

举例

执行顺序

package classInit;

/** * 由于父类初始化必定要在子类的<clinit>()方法前,因此输出 2,参考执行顺序的第三条 */
public class ClinitTest {
    static class Parent{
        public static int  A = 1;
        static {
            A = 2;
        }
    }
    static class Sub extends Parent{
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}
复制代码

多线程阻塞

package classInit;

public  class DeadLockClass {
    static class DeadLoopClass {
        static {
            if (true) {
                System.out.println(Thread.currentThread() + "init DeadLockClass");
                while (true) {

                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = () -> {
            System.out.println(Thread.currentThread()+"start");
            DeadLoopClass dlc = new DeadLoopClass();
            System.out.println(Thread.currentThread()+"end");
        };


        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}
复制代码

由执行结果可知,线程1已经进入阻塞;

被动引用的例子1(我把类所有放在一个代码块中)

package classInit.example1;

public class SuperClass {
    static {
        System.out.println("superClassInit");
    }
    public static int value = 123;
}

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

/** * 不会触发子类的初始化,由于他并无在那5个状况中 * 子类引用父类的静态字段,不会致使子类触发 */
public class ClassInit {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

复制代码

并无触发子类的初始化,由于子类并不符合上述5中条件

package classInit.example2;

import classInit.example1.SuperClass;


/** * 经过数组定义来引用类,不会触发此类的初始化 */
public class NotInit {
    public static void main(String[] args) {
        SuperClass[] superClasses = new SuperClass[10];
    }
}

复制代码
package classInit.example3;

public class ConstClass {
    static {
        System.out.println("ConstClass init");
    }
    public static final String HELLOWORLD = "hello world";

}


/** * 没有输出 "ConstClass init", * 由于常量在编译阶段经过常量传播优化,已经将常量放进了 NotInit 类的常量池中 * 之后NotInit对常量的引用实际都被转化为 NotInit 对自身常量池的引用 * * 也就是说实际上 NotInit 的class文件中并无ConstClass的符号引用入口 * 这两个类在编译成Class文件以后就已经不存在联系了 */
public class NotInit {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
    }
}
复制代码

至此,类加载的过程已经所有结束

相关文章
相关标签/搜索