JVM 学习——类加载机制
HotSpot是按需加载类,当需要使用类的时候才会加载类。
类的加载过程(类的生命周期)
-
加载: 查找并加载类的二进制数据到内存中,把类中的常量,方法等信息保存在方法区,将生成的Class对象保存在方法区(jdk8之后方法区存在于元空间内)
-
链接:
-
验证:确保类的字节码符合jvm的规范。
-
准备:为类的静态变量分配内存,并将其初始化默认值。int:0,long:0L,boolean:false,引用类型为null。
public class Temo { // 常量a在编译阶段就直接赋值18了 public static final int a = 18; // 静态变量b在准备阶段时赋值0,在后续的初始化阶段才会赋值18 public static int b = 18; // 实例变量c,当对象被实例化时(被 new的时候)被分配内存和初始化 public int c; }
-
解析:把类中的符号引用转成直接引用(无需多做了解)。
-
-
初始化: 类加载的最后一步,jvm会根据编写的代码在类中执行静态方法和静态变量的初始化。
-
使用: 类加载完成后,类的实例可以被创建和使用,方法可以被调用,静态变量和静态方法可以被执行和访问。
-
卸载: 当类不再被使用且没有任何引用后,JVM的垃圾回收器会回收该类占用的内存资源,类的卸载是不可逆的。
卸载条件:
- 类的所有实例都被回收了,且类的Class对象也没有被引用。
- 加载该类的类加载器被回收了。
类加载器(ClassLoader)
类加载器分类
类加载器是实现类加载机制的核心组件,JVM中主要有三种类加载器:
- 启动类加载器:用来加载Java核心内库,由JVM自身实现。
- 扩展类加载器:用来加载Java扩展库,通常是
jre/lib/ext
目录下的类库。 - 应用程序(系统)类加载器:用来加载用户类路径下的类,是默认的类加载器。
双亲委派
过程
- 委派请求: 当一个类加载收到一个类的加载请求(应用程序(系统)类加载器),通常不会直接去加载该类,而是向上委派到该类加载器的父类加载器(通常是扩展类加载器)。
- 逐级向上: 父类加载器接收到请求后,同样会将请求委派给它的父类加载器。这个过程会一直向上委派,直到委派到最顶层的启动类加载器。
- 加载类: 启动类加载器尝试去加载这个类。如果这个能成功加载,则加载过程结束。如果启动类加载器无法加载该类,则会向下返还给它的子类加载器。
- 逐级向下: 每一层的类加载器依次尝试加载该类,直到找到成功加载该类的类加载器。如果所有父类都无法成功加载该类,则由最初接收到请求的类加载器去尝试加载。
- 失败处理: 如果最初的接收到类加载请求的类加载器也无法加载该类,则抛出
ClassNotFoundException
异常。
优势
- 安全性: 可以保证核心内库不会被用户自定义的类所替代,确保了Java运行环境的安全性。
- 避免重复加载: 通过这样一层的向上委派,可以避免父类加载器已经加载过的类再次被加载,造成资源的浪费。
loadClass & findClass
loadClass & findClass 是两个在类加载过程中是非重要的方法。
loadClass
loadClass方法就是类加载器的入口方法,用于加载类。
代码的执行逻辑如下:
public Class<?> loadClass(String name) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 判断类加载器的父类加载器是否为空
if (parent != null) {
// 使用父类加载器加载
c = parent.loadClass(name, false);
} else {
// 如果父类加载器为空,则默认使用启动类加载器作为父加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器加载失败,则抛出ClassNotFoundException 异常
}
// 如果抛出ClassNotFoundException 异常,并且还没有被加载到,则调用自己的findClass()方法加载
if (c == null) {
c = findClass(name);
}
}
return c;
}
}
findClass
findClass
方法是一个抽象方法,必须由子类实现,用于查找类的字节码并将其定义为类对象。其典型实现是:
- 根据类名查找对应的字节码文件。
- 读取字节码并将其转换为类对象。
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) throws ClassNotFoundException {
// 根据类名查找字节码文件并读取字节码
// 这是一个示例实现,具体实现方式可能因需求而异
InputStream inputStream = getClass().getResourceAsStream(name.replace('.', '/') + ".class");
if (inputStream == null) {
throw new ClassNotFoundException(name);
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int data = inputStream.read();
while (data != -1) {
byteArrayOutputStream.write(data);
data = inputStream.read();
}
return byteArrayOutputStream.toByteArray();
}
自定义类加载器
在代码中自定义一个类加载器,继承ClassLoader
,重写findClass
方法实现加载自己的类加载逻辑,示例代码如下:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 自定义类加载逻辑
}
}
执行逻辑解释:
- 首先会调用
loadClass
方法,然后通过双亲委派
机制委派给父类加载器去尝试加载类。 - 如果所有被委派的父类加载器都加载失败,则会抛出
ClassNotFoundException
异常,并且调用自定义类加载器的findClass
方法。 - 调用
findClass
方法执行自定义的类加载逻辑,加载类的字节码并通过defineClass
将其转换为Class
对象。
Tomcat类加载器
Tomcat在Java原来的类加载器的基础上新增了几种类加载器:
- Common类加载器: 加载存放在
$CATALINA_HOME/lib
目录下的类库, - Cataina类加载器: 加载Tomcat服务器本身的类,不会与应用程序类发生冲突。
- Shared类加载器: 加载存放在
$CATALINA_HOME/shared
目录下的库,这些库可以被服务器中的所有web应用程序共享,但不是由Common类加载器所加载的。 - Webapp类加载器: 打破了双亲委派机制,收到类加载器请求首先尝试自己去加载。 每个Web应用程序都有其自己的Webapp类加载器,负责加载Web应用的
WEB-INF/classes
和WEB-INF/lib
下的JAR文件中的类。这确保了应用之间的隔离性。