Jvm类加载(面试看这篇足矣)

一、前景

每个开发人员对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然和大部分开发人员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题。

二、Java代码执行流程

在这里插入图片描述
我们编写的java文件都是保存着业务逻辑代码。

  • java编译器将 .java 文件编译成扩展名为字节码文件(.class 的文件)。
  • 然后类加载器加载字节码文件
  • Java虚拟机加载运行(执行引擎处理),最后经过系列处理(编译)成机器指令
  • 最后交付于操作系统(CPU)处理

三、类的加载过程

当需要某个类的时候,java虚拟机会加载 .class 文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程被称为类的加载。
在这里插入图片描述

(1)加载(Loading)

  • 该步的主要目的就是将字节码从不同的数据源(class文件、jar包、war包,甚至网络、其它文件生成[比如将JSP文件转换成对应的Class类])转化为二进制文件加载到内存中。
  • 并生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的入口。
  • 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
    附:

如需了解Java.lang.Class对象,请参考我这篇文章https://blog.csdn.net/weixin_42754971/article/details/113706745

对于"方法区运行时数据结构"是指常量池的里的数据,可参考我这篇文章https://blog.csdn.net/weixin_42754971/article/details/113704735
在方法区中有一个非常重要的部分就是常量池,用来存储编译期间生成的字面量和符号引用。其关于Java虚拟机栈会为每一个即将执行的方法创建一个叫做“栈帧”的区域,该区域用来存储该方法运行时需要的一些信息。其中调用一些信息就是通过调用方法区中的常量池的数据结构

(2)验证(Verify)

  • 目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,Class文件在文件开头有特定的文件标识。
  • 主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。
    在这里插入图片描述
    如图便是Class.文件用Binary Viewer软件打开的(应该是编译过的)十六进制文件,注意开头的文件标识即可。

(2)准备(Prepare)

  • 为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,即零值(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5)
  • 这里不包含final修饰的static ,因为final在编译的时候就已经分配了。准备阶段会显式初始化
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。

(3)解析(Resolve)

  • 把常量池中的符号引用转换为直接引用的过程
  • 解析操作往往会伴随着JVM在执行完初始化之后再执行
  • 符号引用就是一组符号来描述锁引用的目标。是指向目标的指针,偏移量或者能够直接定位的句柄。
  • 解析动作针对与类和接口、字段、类方法等

四、初始化

到初始化阶段,才真正开始执行类中定义的 Java 程序代码

在这里插入图片描述

  • 对于<clinit()> 方法。对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成该方法。
    例:
    静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如下程序:
public class Test {
    
    
    static {
    
    
        // 给变量赋值可以正常编译通过
        i = 0;
        // 这句编译器会提示"非法向前引用"
        System.out.println(i);
    }

    static int i = 1;
}

附:
forName和loaderClass区别

  • Class.forName()得到的class是已经初始化完成的。
  • Classloader.loaderClass得到的class是还没有链接(验证,准备,解析三个过程被称为链接)的。

五、类加载的时机

  • 创建类的实例,也就是new一个对象
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(Class.forName(“com.lyj.load”))
  • 初始化一个类的子类(会首先初始化子类的父类)
  • JVM启动时标明的启动类,即文件名和类名相同的那个类

除此之外,下面几种情形需要特别指出:

对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

猜你喜欢

转载自blog.csdn.net/weixin_42754971/article/details/113749100
今日推荐