类加载
类加载步骤
-
加载:简言之,将 Class 文件(可能是类、可能是接口)加载到内存中,并在内存中创建一个对应的 Class 实例
- 类的信息、数据、结构等放在放在方法区(jdk1.8之前永久代,1.8之后元空间)
- Class 文件对应的 Class 对象是在堆中
-
链接
-
验证:简言之,语法的校验,比如 fina 有没有被继承,抽象方法是否有实现,jdk版本(向下兼容)等
和第一步 加载 同时进行的,如果验证不通过是不能创建 Class 实例的(个人觉得验证这个步骤应该放在加载更适合)
-
准备:简言之,为类的静态变量分配内存,并赋值(要区分是静态变量和静态常量)
-
如果是静态成员变量,赋默认值,如果是静态常量,赋初始值
-
默认值:基本类型就默认值,引用类型就是 null;初始值:第一次赋正确的值
static int a = 1; // 准备阶段赋 0,等初始化阶段才赋值 1 static final int b = 1; //准备阶段就赋值 1 static final int c = a; // 因为这里要先有 a 的值,所以这里虽然有 final 修饰,初始值还是初始化阶段赋值
-
-
解析:简言之,将字节码文件中的类、接口、字段和方法的符号引用转为直接引用
- 比如代码
System.out.println()
对应字节码invokevirtal #24 <java/io/PrintStream.println>
- 要知道 System 类在哪,方法在哪等(比如 #24 对应 java/io/PrintStream.println)
- 比如代码
-
-
初始化:简言之,为类的静态变量设置正确的初始值
- 链接的准备环节设置的是默认值,这里设置的是正确的初始值
- 到这里才真正开始执行 java 类中的代码,类的静态代码块、变量初始值操作开始运行
类加载器
一个类只会被加载一次,这句话对了一半,准确的说法是:一个类只会被一个类加载器加载一次,但是可以被多个类加载器加载。不同的类加载器加载同一个类得到的 Class 对象是不等的,这样就能做到不同的 java 文件有不同的 Class 实例,从而实现类的隔离
-
启动类加载器 BootstrapClassLoader
Cpp 编写,加载 java 核心类库,加载
JAVA_HOME/lib
目录下的类库或-Xbootclasspath
参数指定路径的类库 -
扩展类加载器 ExtClassLoader
- java 编写,继承 ClassLoader 加载
JAVA_HOME/lib/ext
目录下的类库 - 自己的类想被扩展类加载器加载,可以把这个 jar 放在这里面,这个 jar 里面的类就由扩展类加载器加载
- 父类加载器是启动类加载器
- java 编写,继承 ClassLoader 加载
-
应用程序类加载器/系统类加载器 AppClassLoader
- java 编写,加载的是程序员自己写的各种类
- 父类加载器是扩展类加载器
-
用户自定义类加载器
- 自己实现,需要继承 ClassLoader
- 自己写的类默认由系统类加载器加载,为啥还要自定义类加载器呢?比如想打破双亲委派,比如要多次加载同一个类做版本隔离等操作
双亲委派机制
简言之就是加载器在加载一个类的时候,先委托父类加载器加载,父类加载器不能加载的时候才由子类加载器自己加载
比如自定义一个 User 类,没有自定义加载器,就 jvm 默认的三个类加载器,加载过程如下
- 请求委派
- AppClassLoader 将加载 User 的请求委派给 ExtClassLoader
- ExtClassLoader 再将请求委派给 BootstrapClassLoader
- BootstrapClassLoader 加载
- BootstrapClassLoader 检查核心类库中是否存在 User
- 如果 BootstrapClassLoader 发现核心类库中没有 User,他会返回 null,然后再请求传递回 ExtClassLoader
- ExtClassLoader 加载
- ExtClassLoader 检查 jre/lib/ext 下是否存在 User
- 如果 ExtClassLoader 找不到 User,返回 null,然后请求传递回 AppClassLoader
- AppClassLoader 加载
- 如果成功加载(加载、链接、初始化),内存中创建 User 的 Class 示例
- 加载失败,抛出 ClassNotFoundException
![](https://img2024.cnblogs.com/blog/3095527/202408/3095527-20240813144916428-1780828331.png)
为什么要双亲委派
- 保证一个类只加载一次(在不搞花里胡哨操作的情况下)一个类不会被不同的类加载器加载,避免类的重复加载
- 要站在 jvm 已经有三个加载器的前提下来说明
- 假设 jvm 默认就只有一个类加载器,所有的类都由这个唯一的加载器来加载,也能保证只加载一次,但是这个与事实不符明明都已经有 3 个加载器为什么要假设只有一个?或者应该换个问题问:为什么要有多个类加载器
- 保证核心类库不会给修改,比如 String,Object 用户是不能修改这个类的
- 比如自己自定义一个包
java.lang
,在这个包下创建一个类String
- 系统就会有两个
java.lang.String
类,显然是不允许的,双亲委派的机制下不允许这种情况发生
- 比如自己自定义一个包
双亲委派弊端
子类加载器加载的类,对父类加载器来说不可见,比如 AppClassLoader 加载的一个普通类 User
,BootstrapClassLoader 和 ExtClassLoader 是不能使用的。原因也比较简单,如下:
- BootstrapClassLoader 最先开始工作,加载核心类库(ExtClassLoader、AppClassLoader 也是由 BootstrapClassLoader 加载的)
- 当 BootstrapClassLoader 工作的时候,AppClassLoader 都还没被加载,所以 AppClassLoader 加载的类,BootstrapClassLoader 不可见
这样会有问题:JDBC 的 Driver 是核心类库中的类,所以由 BootstrapClassLoader 来加载。 MySql 的驱动是在 classpath 下,由 AppClassLoader 来加载。
连接数据库和执行 sql 都是某个具体的数据库驱动来完成的,java 定义好规范,不同的厂商来实现。sql 的操作都封装在 jdbc 中,BootstrapClassLoader 加载好 jdbc 这些核心类库后,AppClassLoader 才加载 Mysql 的驱动,结果是 BootstrapClassLoader 不能看到 Mysql 的驱动,连接不到数据库。这种情况下就需要打破双亲委派的机制,如果 mysql 的驱动类也由 BootstrapClassLoader 加载就不会有什么问题
定义一个接口,指定一个实现类,new 这个实现类,可以通过多态的形式调用接口的方法实际会调用实现类的方法,因为都是同一个类加载器。数据驱动和这个明显不一样,不要混淆了,或者可以试试接口和实现类不使用同一个类加载器加载,看看有没有什么异常发生
打破双亲委派的机制:1,自定义 ClassLoader,复写 loadClass,不遵循双亲委派机制;2,SPI 机制
ClassLoader
类中有个私有变量 parent 指向当前类加载器的父类加载器,如果父类加载器是 BootstrapClassLoader,parent 会是 null
ExtClassLoader、AppClassLoader 和 用户自定义类加载都要继承 ClassLoader
加载类过程,假设是第一次加载,第一次先 AppClassLoader 来加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// native 方法,看这个类是否被加载过
Class<?> c = findLoadedClass(name);
// 如果没被加载过,父加载器去加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 如果还是没有加加载成功,调用 findClass() 来加载
if (c == null) {
long t1 = System.nanoTime();
// 这是个模板方法,这个方法里面会调用 preDefineClass 方法,保证 java 核心类库不被篡改
// 比如 自定义一个 java.lang 包,创建一个 String 类,即使重写了 loadClass 方法破坏了双亲委派,java 核心类库不受影响
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c); // 刚才加载了,现在看是否需要解析(链接的过程),是个 native 方法
}
return c;
}
}
自定义类加载器
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// class 文件路径
String filePath = classPath + "/" + name.replace('.', '/') + ".class";
try (InputStream inputStream = new FileInputStream(filePath);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
byte[] classBytes = byteArrayOutputStream.toByteArray();
return defineClass(name, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException("Could not load class " + name, e);
}
}
public static void main(String[] args) {
try {
// 传入 jar 的路径
CustomClassLoader loader = new CustomClassLoader("/home/user/classes");
// 这个 class 在服务器上的路径为 /home/user/classes/com/example/MyClass.class
Class<?> cls = loader.loadClass("com.example.MyClass");
// 创建实例
cls.newInstance();
System.out.println("Class loaded: " + cls.getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}