1、类加载器与加载的过程

矿用车联网技术架构图 矿车连接装置_矿用车联网技术架构图

2、类加载子系统的作用

矿用车联网技术架构图 矿车连接装置_矿用车联网技术架构图_02

   类加载子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件表示。

  ClassLoader只负责class文件的加载,至于它是否可以运行,则由Exceution Engine决定。

加载的类信息存放于一个称为方法区的内存空间。出了类的信息外,方法区还会存放运行时常量池的信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)。

3、类加载器ClassLoader角色

  

矿用车联网技术架构图 矿车连接装置_矿用车联网技术架构图_03

   class file 存在本地磁盘上,可以理解为设计师画在纸上是模板,而最终这个模板在执行是时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。

  class file 加载到JVM,被称为DNA元数据模板,放在方法区。

  在.class文件——JVM——最终成为元数据,此过程需要一个运输工具(类加载器),扮演一个快递员的角色。

4、类的加载过程

  

矿用车联网技术架构图 矿车连接装置_初始化_04

   按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:

分别是加载、(验证,准备,解析)链接、初始化、使用和卸载。

  

矿用车联网技术架构图 矿车连接装置_初始化_05

5、加载过程-加载(Loading)

  加载:

类的加载指的是将类的.class文件中的二进制数据读取到内存中,存放在运行时数据区的方法区中,并创建一个大的Java.lang.Class对象,

  用来封装方法区内的数据结构 在加载类时,Java虚拟机必须完成以下3件事情:

    1-通过一个类的是全限定名获取定义此类 二进制字节流

    2-将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(解析类的二进制数据流为方法区内的数据结构(Java类模型))

在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

 

   

矿用车联网技术架构图 矿车连接装置_赋值_06

 

  理解:

  我们也可以这样去理解:所谓装载(加载),简而言之就是将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型——类模板对象

Java类在JVM内存中的一个快照。JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中。

  这样JVM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用

  对于类的二进制数据流,虚拟机可以通过多种途径产生或获得(只要所读取的字节码符合JVM规范即可)

  加载字节码文件的方式:

  虚拟机可能通过文件系统读入一个class后缀的文件(最常见)

  读入jar、zip等归档数据包,提取类文件。

  事先存放在数据库中的类的二进制数据

  使用类似于HTTP之类的协议通过网络进行加载

  在运行时生成一段Class的二进制信息等

  Class实例的位置

  (类将.class文件加载至元空间后,会在堆中创建一个Java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类都对应有一个Class类型的对象)

 6、加载过程-链接(Linking)

1-验证

    确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性.

目的是确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。

文件格式验证,元数据验证,字节码验证,符号引用验证。

    格式检查:是否以魔术oxCAFEBABE开头,主版本和副版本是否在当前Java虚拟机的支持范围内,数据中每一项是否都拥有正确的长度等。

  

矿用车联网技术架构图 矿车连接装置_加载_07

   2-准备

    准备(静态变量,不能是常量)。

    为类变量分配内存并且设置该类变量的默认初始化值。

    这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式赋值。

这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。

    注意:Java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,故对应的,boolean的默认值就是false。

  

矿用车联网技术架构图 矿车连接装置_矿用车联网技术架构图_08

3-解析

    将常量池中的符号引号转换为直接引用的过程(简言之,将类、接口、字段和方法的符号引用转为直接引用),

    1-虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。

  当虚拟机运行起来时,需要从常量池中获得对应的符号引用,再在类加载过程中(初始化阶段)将其替换直接引用,并翻译到具体的内存地址中。

 

    2-符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

  符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中
  

    3-直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,

  同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

    4-不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行,在HotSpot VM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,

  但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行。

    5-符号引号有:类和接口的权限定名、字段的名称和描述符、方法的名称和描述符。
 

  解释什么是符号引号和直接引用?
    (1). 教室里有个空的位子没坐人,座位上边牌子写着小明的座位(符号引用),后来小明进来坐下去掉牌子(符号引用换成直接引用)。

    (2). 我们去做菜,看菜谱,步骤都是什么样的(这是符号引号),当我们实际上去做,这个过程是直接引用。

    (3). 举例:输出操作System.out.println()对应的字节码:invokevirtual #24 <java/io/PrintStream.println>。

  

矿用车联网技术架构图 矿车连接装置_赋值_09

   以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。

  通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。

7、加载过程-初始化(Initialization)

  1-为类变量赋予正确的初始化值。

初始化阶段就是执行类构造器方法< clinit >()的过程。此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码快中的语句合并而来。

public class ClassInitTest {
    private  static int num=1; //类变量的赋值动作
    //静态代码快中的语句
    static{
        num=2;
        number=20;
        System.out.println(num);
        //System.out.println(number); 报错:非法的前向引用
    }
    //Linking之prepare: number=0 -->initial:20-->10
    private static int number=10;

    public static void main(String[] args) {
        System.out.println(ClassInitTest.num);
        System.out.println(ClassInitTest.number);
    }
}

 

  

矿用车联网技术架构图 矿车连接装置_矿用车联网技术架构图_10

  3-若该类具有父类,Jvm会保证子类的< clinit >() 执行前,父类的< clinit >() 已经执行完成。clinit 不同于类的构造方法(init) (由父及子,静态先行)

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) {
        //这个输出2,则说明父类已经全部加载完毕
        System.out.println(Son.B);
    }
}

  4-Java编译器并不会为所有的类都产生<clinit>()初始化方法。哪些类在编译为字节码后,字节码文件中将不会包含<clinit>()方法?

    一个类中并没有声明任何的类变量,也没有静态代码块时。

    一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时。

    一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式 (如果这个static final 不是通过方法或者构造器,则在链接阶段)。

/**
 * @author TANGZHI
 * @create 2021-01-01 18:49
 * 哪些场景下,java编译器就不会生成<clinit>()方法
 */
public class InitializationTest1 {
    //场景1:对应非静态的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
    public int num = 1;
    //场景2:静态的字段,没有显式的赋值,不会生成<clinit>()方法
    public static int num1;
    //场景3:比如对于声明为static final的基本数据类型的字段,不管是否进行了显式赋值,都不会生成<clinit>()方法
    public static final int num2 = 1;
}

  5-static与final的搭配问题

    (使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行)  

/**
 * @author TANGZHI
 * @create 2021-01-01 
 *
 * 说明:使用static + final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
 * 情况1:在链接阶段的准备环节赋值
 * 情况2:在初始化阶段<clinit>()中赋值
 * 结论:
 * 在链接阶段的准备环节赋值的情况:
 * 1. 对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
 * 2. 对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行
 *
 * 在初始化阶段<clinit>()中赋值的情况:
 * 排除上述的在准备环节赋值的情况之外的情况。
 * 最终结论:使用static + final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或String类型的显式赋值,是在链接阶段的准备环节进行。
 */
public class InitializationTest2 {
    public static int a = 1;//在初始化阶段<clinit>()中赋值
    public static final int INT_CONSTANT = 10;//在链接阶段的准备环节赋值

    public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化阶段<clinit>()中赋值
    public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化阶段<clinit>()中赋值

    public static final String s0 = "helloworld0";//在链接阶段的准备环节赋值
    public static final String s1 = new String("helloworld1");//在初始化阶段<clinit>()中赋值

    public static String s2 = "helloworld2";
    public static final int NUM1 = new Random().nextInt(10);//在初始化阶段<clinit>()中赋值

  6-clinit()的调用会死锁吗? 

    虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,

    其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

    正是因为函数<clinit>()带锁线程安全的,因此,如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁。

    并且这种死锁是很难发现的,因为看起来它们并没有可用的锁信息

package com.xiaozhi;

/**
 * @author TANGZHI
 * @create 2021-05-25
 */
class StaticA {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("com.xiaozhi.StaticB");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticA init OK");
    }
}

class StaticB {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("com.xiaozhi.StaticA");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticB init OK");
    }
}

public class StaticDeadLockMain extends Thread {
    private char flag;

    public StaticDeadLockMain(char flag) {
        this.flag = flag;
        this.setName("Thread" + flag);
    }

    @Override
    public void run() {
        try {
            Class.forName("com.xiaozhi.Static" + flag);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(getName() + " over");
    }

    public static void main(String[] args) throws InterruptedException {
        StaticDeadLockMain loadA = new StaticDeadLockMain('A');
        loadA.start();
        StaticDeadLockMain loadB = new StaticDeadLockMain('B');
        loadB.start();
    }
}

8、加载过程-类的Using(使用) 

①. 任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便"万事俱备,只欠东风"就等着开发者使用了。

  ②. 开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法)或者使用new关键字为其创建对象实例。

9、加载过程-类的Unloading(卸载)

  ①. 类、类的加载器、类的实例之间的引用关系  

    1-在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。

    由此可见,代表某个类的Class实例与其类的加载器之间为双向关联关系。

    2-一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。

    此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。

  

矿用车联网技术架构图 矿车连接装置_矿用车联网技术架构图_11

 ②. 方法区的垃圾回收

    1-方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

    2-HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。

    3-判定一个常量是否"废弃”还是相对简单,而要判定一个类型是否属于"不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件。

   

矿用车联网技术架构图 矿车连接装置_矿用车联网技术架构图_12

③. 类的卸载    

    1-启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范)。

    2-被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。  

    3-开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。

    可以预想,稍微复杂点的应用场景中(比如:很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的

    (至少卸载的时间是不确定的)。