一.类的生命周期
类的生命周期描述了一个类加载、使用、卸载的整个过程。整体可以分为:加载 --> 验证 --> 准备 --> 解析 --> 初始化 --> 使用 --> 卸载

1.1 加载阶段
1、加载(Loading)阶段第一步是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息,程序员可以使用Java代码拓展的不同的渠道。
- 从本地磁盘上获取文件
- 运行时通过动态代理生成,比如Spring框架
- Applet技术通过网络获取字节码文件
2、类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中,方法区中生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。

3、Java虚拟机同时会在堆上生成与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)。

1.2 验证
1、文件格式验证,比如文件是否以0xCAFEBABE开头 ,主次版本号是否满足当前Java虚拟机版本要求。

2、元信息验证,例如类必须有父类(super不能为空)。
3、验证程序执行指令的语义,比如方法内的指令执行中跳转到不正确的位置。
4、符号引用验证,例如是否访问了其他类中private的方法等。
1.3 准备
准备阶段为静态变量(static)分配内存并设置初值,每一种基本数据类型和引用数据类型都有其初值。
| 数据类型 | 初始化 |
|---|---|
| int | 0 |
| long | 0L |
| short | 0 |
| char | '\u0000' |
| byte | 0 |
| boolean | false |
| double | 0.0 |
| 引用数据类型 | null |
注意!!!
final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。
1.4 解析
解析阶段主要是将常量池中的符号引用替换为直接引用 ,符号引用就是在字节码文件中使用编号来访问常量池中的内容。

直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。

1.3 初始化阶段
- 初始化阶段会执行字节码文件中clinit(class init 类的初始化)方法的字节码指令,包含了静态代码块中的代码,并为静态变量赋值。
- 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 虚拟机会保证一个类的() 方法在多线程环境中被正确加锁和同步
二.类加载器
类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术,类加载器只参与加载过程中的字节码获取并加载到内存这一部分。
类加载器会通过二进制流的方式获取到字节码文件的内容,接下来将获取到的数据交给Java虚拟机,虚拟机会在方法区和堆上生成对应的对象保存字节码信息。
2.1 启动类加载器
- 启动类加载器(Bootstrap ClassLoader)是由Hotspot虚拟机提供的、使用C++编写的类加载器。
- 默认加载Java安装目录**/jre/lib下的类文件**,比如rt.jar,tools.jar,resources.jar等。
用户扩展基础jar包 如果用户想扩展一些比较基础的jar包,让启动类加载器加载,有两种途径:
- 放入jre/lib下进行扩展。不推荐,尽可能不要去更改JDK安装目录中的内容,会出现即时放进去由于文件名不匹配的问题也不会正常地被加载。
- 使用参数进行扩展。推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩展,参数中的/a代表新增。
2.2 扩展类加载器
扩展类加载器(Extension Class Loader)是JDK中提供的、使用Java编写的类加载器。默认加载Java安装目录/jre/lib/ext下的类文件。
通过扩展类加载器去加载用户jar包:
- 放入/jre/lib/ext下进行扩展。不推荐,尽可能不要去更改JDK安装目录中的内容。
- 使用参数进行扩展使用参数进行扩展。推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,这种方式会覆盖掉原始目录,可以用;(windows):(macos/linux)追加上原始目录
2.3 应用程序类加载器
应用程序类加载器会加载classpath下的类文件,默认加载的是项目中的类以及通过maven引入的第三方jar包中的类。
2.4 三者关系
- 启动类加载器,由C++实现,没有父类。
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 应用程序类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

三.双亲委派机制
双亲委派机制指的是:当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载。

双亲委派机制的作用:
1.保证类加载的安全性。通过双亲委派机制避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。
2.避免重复加载。双亲委派机制可以避免同一个类被多次加载。
3.1 打破双亲委派机制
双亲委派机制默认的加载类逻辑是:当一个类加载器需要加载某个类时,先请求它的父类加载器进行加载,只有在父加载器找不到该类时,它才会尝试自己加载。
为了打破这一机制,通常需要自定义一个类加载器。并通过重写 ClassLoader 的 loadClass 方法,完全接管类加载的流程,可以实现自己的类加载逻辑。
java
/**
* 打破双亲委派机制 - 自定义类加载器
*/
public class BreakClassLoader1 extends ClassLoader {
private String basePath;
private final static String FILE_EXT = ".class";
//设置加载目录
public void setBasePath(String basePath) {
this.basePath = basePath;
}
//使用commons io 从指定目录下加载文件
private byte[] loadClassData(String name) {
try {
String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));
FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);
try {
return IOUtils.toByteArray(fis);
} finally {
IOUtils.closeQuietly(fis);
}
} catch (Exception e) {
System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());
return null;
}
}
//重写loadClass方法
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
//如果是java包下,还是走双亲委派机制
if(name.startsWith("java.")){
return super.loadClass(name);
}
//从磁盘中指定目录下加载
byte[] data = loadClassData(name);
//调用虚拟机底层方法,方法区和堆区创建对象
return defineClass(name, data, 0, data.length);
}
- 默认情况下自定义类加载器的父类加载器是应用程序类加载器:
- 在同一个Java虚拟机中,只有相同类加载器+相同的类限定名才会被认为是同一个类,所以两个自定义类加载器加载相同限定名的类不会冲突

