JVM基础2 - java类加载机制
1. java类的加载过程
JVM 中类的装载是由类加载器(ClassLoader) 实现的,类加载器负责在运行时查找和装入类文件中的类。
当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。
- 加载:指把.class文件中读入到内存中,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。
- 连接:包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。
- 类初始化:如果类存在父类且这个类没有被初始化,那么就先初始化父类;如果类中存在初始化语句,就依次执行这些初始化语句。
符号引用和直接引用: 当java类编译成class文件时,java类并不知道其引用类的实际内存地址,因此只能使用符号引用来代替。类装载器装载类时,可以通过虚拟机获取实际的内存地址,然后将符号引用替换为直接引用。
当程序执行结束,或者程序遇到异常、操作系统导致JVM进程终止、执行了 System.exit() 都会结束类的生命。
2. 类加载器
2.1. 类加载器
类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。
类加载器:
- Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
- Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
- System:应用类加载器,其父类是Extension。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。
2.2. 双亲委派机制
如果一个类收到了类加载的请求,它首先会将请求发送给父类去加载,以此向上,直到所有的类加载请求传递到顶层的启动类加载器中。
只有当父类加载器在自己的搜索范围找不到所需的类时,子加载器才会尝试自己去加载。
双亲委派过程
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
双亲委派源码
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
双亲委派优势
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行
3. 自定义类加载器
场景:
当通过网络来传输 Java 类的字节码,需为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。
自定义类加载器一般都是继承自 ClassLoader 类,从上面对 loadClass 方法来分析来看,我们只需要重写 findClass
方法即可。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.21</version>
</dependency>
package com.gao.jvm.classloader;
import org.apache.commons.compress.utils.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
public class MyClassLoader extends ClassLoader {
public static void main(String[] args) throws Exception {
MyClassLoader mcl = new MyClassLoader();
Class<?> clazz = Class.forName("com.gao.algorithm.integer.IntegerOne", true, mcl);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
}
public MyClassLoader() {
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
File file = new File("/Users/lianggao/Downloads/com/gao/algorithm/integer/IntegerOne.class");
try {
byte[] bytes = getClassBytes(file);
//将二进制流字节组成的文件转换为一个java.lang.Class
return this.defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
// 这里要读入.class的字节,因此要使用字节流
private byte[] getClassBytes(File file) throws Exception {
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();) {
//我们这里可以读到之后,可以对文件进行解密
。。。
IOUtils.copy(fis, baos);
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return new byte[0];
}
}
控制台输出
//com.gao.jvm.classloader.MyClassLoader@610455d6
需要注意 :
- 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。
- 这类Test 类本身可以被 AppClassLoader 类加载,因此我们不能把com/gao/algorithm/integer/IntegerOne.class 放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由 AppClassLoader 加载,而不会通过我们自定义类加载器来加载。
https://pdai.tech/md/java/jvm/java-jvm-classload.html