JVM学习(二):基本原理之类的加载

转载于:https://www.itzhai.com/articles/how-class-file-load-into-jvm.html

类的生命周期:其中如果动态绑定或者晚期绑定,解析阶段不会再准备阶段之后立即执行

image-20200104201614588

1.加载

image-20200104234319822

1.1.职责:

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

1.2.何时触发加载CLASS文件(触发类的初始化):

  • 遇到new、getstatic、putstatic或者invokestatic字节码指令的时候,如果类还没有初始化。对应场景为:
    • new一个对象;
    • 读取或者设置一个类的静态字段;
    • 调用类的静态方法的时候;
  • 使用java.lang.reflect包的方法进行反射且类还没有初始化
  • 初始化类时,如果父类还未初始化则触发父类的初始化
  • 虚拟机启动时main方法所在的类会首先进行初始化
  • 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

这个时候通过类的全限定名称获取类的二进制字节流。

image-20210520223930352

此时这个字节流为静态存储结构,需要转换为方法区的运行时数据结构。结构如上图方法区中所示。每个类生成一个对应的结构,结构里面的信息详细介绍参考此文:The Java Virtual Machine

其中:

ClassLoader的引用指的是加载这个Class文件的ClassLoader实例的引用;

Class实例引用指的是类加载器在加载类信息并放到方法区之后,然后创建对应的Class类型的实例,并把该实例的引用保存到Class实例引用中。

1.3.获取二进制流的方式

主要有如下几种获取方式:

  • 压缩包,例如JAR、EAR、WAR包
  • 网络,如Applet
  • 动态代理
  • JSP生成
  • 数据库获取

类加载器:

  • 启动类加载器:由C++实现,没有对应的Java对象,在Java中只能用null表示

  • java.lang.ClassLoader:除了启动类加载器之外,其他加载器类都是其子类,有对应的Java对象,因此这些类加载器类需要通过另外的类加载器加载到jvm中,例如启动类加载器

  • 扩展类加载器(extension class loader):其父类加载器是启动类加载器,负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)

  • 应用类加载器(application class loader):父类加载器是扩展类加载器,负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的

  • 自定义的类加载器:自己定义的类加载器,可以对class文件进行加解密

  • 双亲委派模型:

    每当一个类加载器接受到类加载请求时,会将它先请求转发到父类加载器,当父类加载器查找不到所请求的类时,此时才轮到该类加载器尝试去加载

  • 命名空间:在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类

Java 9之前:

启动类加载器复制加载最基础、最重要的类,例如放在JRE的lib目录下jar包的类(以及由虚拟机参数 -Xbootclasspath 指定的类)

Java 9之后

扩展类加载器改名为平台类加载器,Java SE除了少数几个关键模块是由启动类加载器加载,其余都是由平台加载器加载

2.链接

image-20200105102811544

**概念:**将创建的类合并至Java虚拟机中,使其能够执行的过程,分为验证、准备、解析三个阶段

2.1.验证

class文件是可以被人为篡改的,虚拟机如果直接拿来执行,可能会把系统给搞崩溃了,所以一定要先对Class文件做严格的验证

确保被加载的类能够满足虚拟机的约束条件,验证内容如下:

  • 文件格式的验证:bytecode 的完整性(integrity)

  • 元数据验证:主要是语义校验,如:没有父类,继承了final类,接口的非抽象类实现没有完整实现方法等。

  • 符号引用验证:解析阶段发生的验证,当把符号引用转化为直接引用的时候进行验证。这主要是对类自身以外的信息进行匹配性校验,包括:

    • 全限定名是否可以找到对应的类
    • 指定类是否符合方法的字段描述符以及简单名称所描述的方法和字段
    • 校验类、字段和方法的可见性

2.2.准备

为被加载的类的静态字段分配内存,赋默认初值(零值),还会在此阶段构造其他跟类层次相关的数据结构,例如用来实现多态方法调用的动态绑定的方法列表

method table的数据结构,它包含了指向所有类方法(也包括也从父类继承的方法)的指针,这样再调用父类方法时就不用再去搜索了

注意:static final类型的常量value会在准备阶段被初始化为常量指定的值。

静态变量存储在内存的PremGen(方法区域)空间中,其值存储在Heap中

2.3.解析

解析阶段主要将常量池内的符号引用替换为直接引用

class文件被加载入jvm之前,这个类无法知道其他类及其方法对应的地址,此时编译器会生成一个符号引用,而链接阶段需要做的就是将符号引用指向真正的地址,如果符号引用还未被加载,此时则会触发这个类的加载,解析阶段未必会在链接时触发,但是在执行字节码之前,如果包含了符号引用,那就会触发解析

符号引用:字面量,引用目标不一定已经加载到内存中

直接引用:直接指向目标的指针,或者相对偏移量,或是一个能简介定位到目标的句柄。直接引用和虚拟机实现的内存布局相关

解析主要针对以下七类符号引用进行:

  • 类或接口 CONSTANT_Class_info
  • 字段 CONSTANT_Fieldref_info
  • 类方法 CONSTANT_Methodref_info
  • 接口方法 CONSTANT_InterfaceMethodref_info
  • 方法类型 CONSTANT_MethodType_info
  • 方法句柄 CONSTANT_MethodHandle_info
  • 调用限定符 CONSTANT_InvokeDynamic_info

符号引用解析的过程或校验的过程中,可能又会触发另一个类的加载。

3.初始化

image-20200105112555664

类中的静态字段会在链接的准备阶段赋上初始值,然后在初始化阶段才会真正的赋值

也就是说除了代码中定义的常量(static final修饰的基本类型)的直接赋值操作,以及所有静态代码块中的代码,会被Java编译器防置于同一方法中,命名为 < clinit >

类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。

3.1.<CLINIT>方法

概念:编译器收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的方法,主要给类变量做初始化工作的方法

关于<CLINIT>方法的注意事项

  • 顺序问题:静态语句块后面的静态变量,静态语句块中可以赋值,但不可以访问

  • 继承顺序问题:无需显示调用,虚拟机会保证子类的<clinit>方法执行前,父类的<clinit>方法已经执行完毕

  • 接口的<clinit>方法:然接口不能有静态语句块,但是可以给静态变量初始化值,所以也可以生成<clinit>方法;

  • 接口继承:除非使用到父接口的变量,否则执行子接口的<clinit>方法不需要先执行父接口的<clinit>方法

除此之外,还有< init >方法:init是对象构造器方法句、调用的构造方法合并产生

init和clinit方法执行目的不同

init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化。

<init>方法是在一个类进行对象实例化时调用的。实例化一个类有四种途径:

1 调用new操作符
2 调用Class或java.lang.reflect.Constructor对象的newInstance()方法
3 调用任何现有对象的clone()方法
4 通过java.io.ObjectInputStream类的getObject()方法反序列化

那么,类的初始化何时会被触发呢?JVM 规范枚举了下述多种触发情况

  1. 当虚拟机启动时,初始化用户指定的主类;
  2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
  5. 子类的初始化会触发父类的初始化;
  6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
  7. 使用反射 API 对某个类进行反射调用时,初始化这个类;

除了上述的情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”

  • 通过子类引用父类的静态字段,对于父类属于“主动引用”的第一种情况,对于子类,没有符合“主动引用”的情况,故子类不会进行初始化。代码如下:

    //父类  
    public class SuperClass {  
        //静态变量value  
        public static int value = 666;  
        //静态块,父类初始化时会调用  
        static{  
            System.out.println("父类初始化!");  
        }  
    }  
    
    //子类  
    public class SubClass extends SuperClass{  
        //静态块,子类初始化时会调用  
        static{  
            System.out.println("子类初始化!");  
        }  
    }  
    
    //主类、测试类  
    public class NotInit {  
        public static void main(String[] args){  
            System.out.println(SubClass.value);  
        }  
    }  
    
  • 通过数组来引用类,不会触发类的初始化,因为是数组new,而类没有被new,所以没有触发任何“主动引用”条款,属于“被动引用”。

  • 静态常量在编译阶段就会被存入调用类的常量池中,不会引用到定义常量的类,这是一个特例,需要特别记忆,不会触发类的初始化,也就会说static final修饰的String类型和基本类型的常量不会被初始化

3.1.clinit和init的理解

从上面我们可以明白clinit是在类初始化时候调用的,init是在类实例化的时候调用的,初始化通过lvm的锁机制保证只会执行一次,但是实例化可以执行多次,因此clinit只会执行一次,而init会执行多次,这也是最大的区别。并且这两个方法都是在编译为字节码的时候就确立好了。

总结下:

1,<init>方法等同于类的构造函数,在遇到new指令时,自然会调用该方法
2,java类中存在static成员变量,或者有static{}的代码块时,jvm加载java类时,会触发方法,完成static成员变量的初始化,或者执行static{}的代码逻辑

最后我们来总结下类的初始化顺序,由JVM的类加载顺序可以得到:<clinit>()方法先于<init>()方法,因此:

单纯类的情况下的执行顺序(无继承):

1.静态变量或静态代码块(看源码顺序)

2.实例变量或实例代码块(看源码顺序)

3.构造方法

有父类情况下的执行顺序:

1.父类静态变量或静态代码块(看源码顺序)

2.子类静态变量或静态代码块(看源码顺序)

3.父类实例变量或实例代码块(看源码顺序)

4.父类构造函数

5.子类实例变量或实例代码块(看源码顺序)

6.子类构造函数

# JVM 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×