JVM基础篇——类加载子系统

类加载器和类的加载过程

1、类加载子系统负责从文件或者网络当中加载.class文件,通常.class文件在文件的打开头有特定的文件标识
2、Classloader只是负责class文件的加载,至于他能不能运行,交给Execution Engine决定
3、加载的类信息存放在一块叫做方法区的内存空间,除了类的信息外,方法区中还会存放运行时常量池信息,可能还包含字符串字面量或者是数字常量(这部分的常量是.class文件中常量池部分的内存映射)

what is 运行时常量池??? 就是常量池在运行时加载到内存里叫做运行时常量池

类加载器ClassLoader

JVM基础篇——类加载子系统_第1张图片
1、.class 文件存在于本地硬盘上,可以理解为设计师画在纸上的模板,最终这个模板运行的时候是加载到JVM中,根据这个文件创建n个一样的实例
2、.class文件加载到JVM当中,被称为DNA元数据模板,放在方法区
3、在.class 文件 -> JVM -> 成为元数据模板,此过程就要一个运输工具(类装载器Class Loader)扮演一个快递员的角色。

注意:这里说的生成n个一样的实例,不是说n个实例一模一样,而是属性方法都有,能调用的方法也都有,但是不是所有数据的值都是一样的。

类的装载过程图形理解
JVM基础篇——类加载子系统_第2张图片当JVM开始运行的时候,检查这个类是不是已经被加载,如果没有被加载,那么ClassLoader类加载器开始加载这个类,因为加载的过程当中也可能存在错误,所以,如果装载顺利,就开始执行下一步的链接,如果装载错误,就抛出异常。

类的加载详细过程

类的加载过程有三大步:加载、链接、初始化

加载

这个加载和刚才的加载不是一个东西,是加载三个步骤当中的一个步骤:
1、通过一个类的全限定名获取定义此类的二进制文件流
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

加载的几种方式
1、从本地系统中直接加载
2、通过网络获取,典型的应用场景:web applet
3、从zip压缩包当中读取,成为日后jar,war格式的基础
4、运行时计算生成,使用最多的是动态代理技术
5、由其他文件生成,典型场景:jsp应用
6、专有的书库当中提取,比较少见
7、从加密的文件当中获取,典型的防.class 文件被反编译的保护措施

链接

链接分为三个小的步骤:验证、准备、解析

验证(Verify)

  • 目的在于确保class文件的字节流当中的信息符合当前虚拟机的要求,保证被加载类的正确性,不会危害虚拟机的自身安全。
  • 四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证

准备(Prepare)

  • 为类变量(静态变量)设置分配内存,为其设置默认值,也就是零值
  • 不包含final修饰的static,因为final在编译的时候就分配了,准备阶段会显示初始化
  • 不会为实例变量进行初始化,类变量分配在方法区当中,实例变量随着java对象到java堆中。

解析(Resolve)

  • 常量池内的符号引用转换为直接引用的过程
  • 事实上,解析操作往往伴随着 JVM执行初始化之后在执行
  • 符号引用是用一组符号描述引用的目标(我怀疑是不是就是定义的实例的名称),符号引用的字面量形式明确定义在class文件格式当中,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
  • 解析动作主要针对类的接口、字段、类方法、接口方法、方法类型等,对应常量池当中的constant_class_info、constant_fieldref、constant_methodref_info等
初始化
  • 初始化阶段就是执行类构造器方法 ()的过程。
  • 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中指令按语句在源文件中出现的顺序执行。
  • clinit()不同于类的构造器。(关联: 构造器是虚拟机视角下的init())
  • 若该类具有父类,JVM会 保证子类的clinit()执行前,父类的clinit() 已经执行完毕。
  • 虚拟机必须保证一个类的clinit() 方法在多线程下被同步加锁。

我们来使用IDEA来详细解释一下类加载过程当中的初始化过程:

1、clinit() 构造方法:
下面是一段简单的测试代码:

public class HelloLoader {
    private int num;

    static {
        System.out.println("execute");
    }

    public static void main(String[] args) {
        new HelloLoader();
    }
}

使用jclasslib插件打开编译成功的class文件,打开是下面这个样子的:
JVM基础篇——类加载子系统_第3张图片

clinit()构造方法:
跟上面描述的一样,clinit()是在类当中存在类变量的赋值,或者是否存在静态代码块。构造器方法clinit()就是这两个的合并。

2、构造器方法的顺序

**修改:**那么把刚才的这段代码再次进行修改

class Loader {
    private static int x;
    static {
        x = 10;
    }
}
public class HelloLoader extends Loader {
    private int num;

    static {
        System.out.println("execute");
    }

    public static void main(String[] args) {
        new HelloLoader();
    }
}

那么在这次的初始化过程当中,在执行HelloLoader的clinit()的方法之前执行父类的clinit()方法。

3、保证clinit() 方法在多线程下被同步加锁
先看一下下面这些测试代码:

//ThreadTest.java 文件
public class ThreadTest {
    public static void main(String[] args) {
        Runnable r = () -> {
            System.out.println(Thread.currentThread().getName() + " start");
            new DeadThread();
            System.out.println(Thread.currentThread().getName() + " end");
        };
        new Thread(r, "thread 1").start();
        new Thread(r, "thread 2").start();
    }
}

//DeadThread.java文件
public class DeadThread {
    private static int num = 0;

    static {
        num++;
        System.out.println("number : " + num);
    }
}

这段代码执行的结果是:
thread 1 start
thread 2 start
number : 1
thread 2 end
thread 1 end

从结果看,无论是哪一个线程先执行,这个类只会加载一次,当一个线程在进行加载类的时候,因为同步加锁,所以其他的线程不能同时加载。

下面还有类加载器的具体解释,欢迎继续观看
JVM类加载器

你可能感兴趣的:(JVM基础篇——类加载子系统)