「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
最近接到这么一个需求,就是可以在项目运行过程中动态加载class文件(这种需求正常项目应该很难遇到吧,所以面试这点问的也不是很多,基本都问垃圾机制),你可以理解为热加载;这需求拿到手肯定一脸懵,不过不怕,我特意请教大佬了解到要注意到两个关键点
- 类的加载机制
- 双亲委派机制
本节我们按顺序说说类加载机制,另一个下节摇摆
什么是类加载
类加载是将编译的.class文件中的二进制数据读入到内存中,运行时将其数据结构放入到方法区中,然后在堆区存放Class对象(java.lang.Class对象)封装了类在方法区内的数据结构,后提供访问方法区内的数据结构的接口
获取二进制文件可通过网络、数据库、本地文件、动态加载而不一定偏要从class文件中获取;
看了类加载的定义,我们来看下广义上定义的类的生命周期
类加载过程
加载
就是获取二进制文件,通过全限定名获取二进制字节流,堆中生成一个代表该类的Class对象,并向开发人员提供了访问方法区内的数据结构的接口
验证
确保class文件的正确性,不会影响到JVM的安全
首先我们看下我们的.class文件长啥样子
-
**文件格式验证:**是否以0xcafe babe开头、主次版本号是否在当前虚拟机的处理范围内、常量池中的常量是否有不被支持的类型等。
-
**元数据验证:**描述信息是否符合java规范
-
**字节码验证:**通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
-
**符号引用验证:**确保解析动作能正确执行
这一段总结起来就是代码的校验;
准备
给类的静态变量分配存储空间,并初始化,这一步也正是为什么静态方法不能调用普通方法/变量的原因,在方法区中JVM会为静态变量(static修饰)分配内存,并赋值对应数据类型的数据值,注意这里仅仅是赋系统定义的初始值,而不是显式的调用我们的代码值,比如默认值0L;
这里延伸一下,也是我写这篇文章过程查阅资料觉得重要的点;
- static修饰的变量或者全局变量,系统系统会赋默认值,局部变量使用的话需要我们手动赋值,否则编译报错;null也是一种赋值方式哟
- static和final修饰的变量,在该阶段就已经赋值,在javac编译时生成ConstantValue属性,类加载直接把ConstantValue的值赋给该字段,所以如果你用static final 你得给它赋上你需要的值;该值存于常量池;
解析
将常量池中的符号引用转化为直接引用的过程,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化
执行代码阶段。
这里举几个例子
-
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();
至此,类的加载机制就算结束,下节说说双亲委派模型