浅析:JVM类加载机制

707 阅读4分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

最近接到这么一个需求,就是可以在项目运行过程中动态加载class文件(这种需求正常项目应该很难遇到吧,所以面试这点问的也不是很多,基本都问垃圾机制),你可以理解为热加载;这需求拿到手肯定一脸懵,不过不怕,我特意请教大佬了解到要注意到两个关键点

  1. 类的加载机制
  2. 双亲委派机制

本节我们按顺序说说类加载机制,另一个下节摇摆

什么是类加载

类加载是将编译的.class文件中的二进制数据读入到内存中,运行时将其数据结构放入到方法区中,然后在堆区存放Class对象(java.lang.Class对象)封装了类在方法区内的数据结构,后提供访问方法区内的数据结构的接口

获取二进制文件可通过网络、数据库、本地文件、动态加载而不一定偏要从class文件中获取;

看了类加载的定义,我们来看下广义上定义的类的生命周期

类加载过程

加载

就是获取二进制文件,通过全限定名获取二进制字节流,堆中生成一个代表该类的Class对象,并向开发人员提供了访问方法区内的数据结构的接口

验证

确保class文件的正确性,不会影响到JVM的安全

首先我们看下我们的.class文件长啥样子

  • **文件格式验证:**是否以0xcafe babe开头、主次版本号是否在当前虚拟机的处理范围内、常量池中的常量是否有不被支持的类型等。

  • **元数据验证:**描述信息是否符合java规范

  • **字节码验证:**通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的

  • **符号引用验证:**确保解析动作能正确执行

这一段总结起来就是代码的校验;

准备

给类的静态变量分配存储空间,并初始化,这一步也正是为什么静态方法不能调用普通方法/变量的原因,在方法区中JVM会为静态变量(static修饰)分配内存,并赋值对应数据类型的数据值,注意这里仅仅是赋系统定义的初始值,而不是显式的调用我们的代码值,比如默认值0L;

这里延伸一下,也是我写这篇文章过程查阅资料觉得重要的点;

  1. static修饰的变量或者全局变量,系统系统会赋默认值,局部变量使用的话需要我们手动赋值,否则编译报错;null也是一种赋值方式哟
  2. static和final修饰的变量,在该阶段就已经赋值,在javac编译时生成ConstantValue属性,类加载直接把ConstantValue的值赋给该字段,所以如果你用static final 你得给它赋上你需要的值;该值存于常量池;

解析

将常量池中的符号引用转化为直接引用的过程,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

执行代码阶段。

这里举几个例子

  1.  public class Test1 {
     
         public static void main(String[] args) {
             System.out.println(Sonc.i);
         }
     }
     class Superc{
         public static int i = 0;
         static{
             System.out.println("執行superc");
         }
     }
     class Sonc extends Superc{
         static{
             System.out.println("執行sonc");
         }
     }
     结果是:執行superc
         0
     这里可以看成是主动引用和被动引用,比如我们创建一个实例,就是主动引用,这里子类引用父类字段,
     父类初始化,子类没有,属于被动引用
     
     
     
    

为什么Sonc里的代码没执行呢,static修饰的值在解析阶段已经运行,我们此时获取的i值与Sonc是没有关系的,而static块是初始化阶段的,所以不会触发子类的初始化

2.如果对于上例,主函数变为

1. Superc[] s = new Superc[2];
这个结果是:啥也不输出

2. Superc ss = new Superc();这个结果是:執行superc3. Superc[] s = new Superc[2];
        Superc ss = new Superc();
        System.out.println(s.getClass());
        System.out.println(ss.getClass());这个结果是:執行superc
            class [Lcom.xxx.controller.login.Superc;
            class com.xxx.controller.login.Superc
注意点是这个数组,我们可以看到虚拟机生成的类是Lcom.xxx.controller.login.Superc

3.对于static final的

public class Test1 {
    public static void main(String[] args) {
        System.out.println(Sonc.i);
    }
}
class Superc{
    public static final int i = 0;
    static{
        System.out.println("執行superc");
    }
}
class Sonc extends Superc{
    static{
        System.out.println("執行sonc");
    }
}
这里输出是: 0
所有的代码块都没执行,而是直接取了常量池的值,所以块内代码一个都没执行;

使用

这个就是上面的代码使用,没啥说的

卸载

简单而言就是System.exit();

至此,类的加载机制就算结束,下节说说双亲委派模型