JVM内部机制大揭秘:Java类加载全过程

当程序使用某个类时, 如果该类还没被初始化, 加载到内存中, 则系统会通过加载、连接、初始化三个过程来对该类进行初始化. 该过程就被称为类的初始化

类加载

指将类的 class 文件读入内存, 并为之创建一个 java.lang.Class 的对象

类文件来源

  • 从本地文件系统加载的 class 文件

  • 从 JAR 包加载 class 文件

  • 从网络加载 class 文件

  • 把一个 Java 源文件动态编译, 并执行加载

类加载器通常无须等到“首次使用”该类时才加载该类, JVM 允许系统预先加载某些类

类加载器

类加载器就是负责加载所有的类, 将其载入内存中, 生成一个 java.lang.Class 实例. 一旦一个类被加载到 JVM 中之后, 就不会再次载入了.

20241229154732_YsqIonrn.webp

  • 根类加载器(Bootstrap ClassLoader): 其负责加载 Java 的核心类, 比如 String、System 这些类

  • 拓展类加载器(Extension ClassLoader): 其负责加载 JRE 的拓展类库

  • 系统类加载器(System ClassLoader): 其负责加载 CLASSPATH 环境变量所指定的 JAR 包和类路径

  • 用户类加载器: 用户自定义的加载器, 以类加载器为父类

类加载器之间的父子关系并不是继承关系, 是类加载器实例之间的关系

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws IOException {
ClassLoader systemLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类加载");
Enumeration<URL> em1 = systemLoader.getResources("");
while (em1.hasMoreElements()) {
System.out.println(em1.nextElement());
}
ClassLoader extensionLader = systemLoader.getParent();
System.out.println("拓展类加载器" + extensionLader);
System.out.println("拓展类加载器的父" + extensionLader.getParent());
}

结果

1
2
3
4
系统类加载
file:/E:/gaode/em/bin/
拓展类加载器 sun.misc.Launcher$ExtClassLoader@6d06d69c
拓展类加载器的父 null

为什么根类加载器为 NULL?

根类加载器并不是 Java 实现的, 而且由于程序通常须访问根加载器, 因此访问扩展类加载器的父类加载器时返回 NULL

JVM 类加载机制

  • 全盘负责, 当一个类加载器负责加载某个 Class 时, 该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入, 除非显示使用另外一个类加载器来载入
  • 父类委托, 先让父类加载器试图加载该类, 只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制, 缓存机制将会保证所有加载过的 Class 都会被缓存, 当程序中需要使用某个 Class 时, 类加载器先从缓存区寻找该 Class, 只有缓存区不存在,
    系统才会读取该类对应的二进制数据, 并将其转换成 Class 对象, 存入缓存区. 这就是为什么修改了 Class 后, 必须重启 JVM, 程序的修改才会生效

URLClassLoader 类

URLClassLoader 为 ClassLoader 的一个实现类, 该类也是系统类加载器和拓展类加载器的父类(继承关系). 它既可以从本地文件系统获取二进制文件来加载类,
也可以远程主机获取二进制文件来加载类.

两个构造器

  • URLClassLoader(URL[] urls): 使用默认的父类加载器创建一个 ClassLoader 对象, 该对象将从 urls 所指定的路径来查询并加载类
  • URLClassLoader(URL[] urls,ClassLoader parent): 使用指定的父类加载器创建一个 ClassLoader 对象, 其他功能与前一个构造器相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import com.mysql.jdbc.Driver;

public class GetMysql {
private static Connection conn;
public static Connection getConn(String url,String user,String pass) throws MalformedURLException, InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException{
if(conn==null){
URL[]urls={new URL("file:mysql-connector-java-5.1.18.jar")};
URLClassLoader myClassLoader=new URLClassLoader(urls);
Driver driver=(Driver) myClassLoader.loadClass("com.mysql.jdbc.Driver").newInstance();
Properties pros=new Properties();
pros.setProperty("user", user);
pros.setProperty("password", pass);
conn=driver.connect(url, pros);
}
return conn;
}
public static method1 getConn() throws MalformedURLException, InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException{

URL[]urls={new URL("file:com.arraydsj@163.com")};
URLClassLoader myClassLoader=new URLClassLoader(urls);
method1 driver=(method1) myClassLoader.loadClass("com.arraydsj@163.com.method1").newInstance();

return driver;
}

public static void main(String[] args) throws MalformedURLException, InstantiationException, IllegalAccessException, ClassNotFoundException, SQLException {
System.out.println(getConn("jdbc:mysql://10.10.16.11:3306/auto?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true", "jiji", "jiji"));
System.out.println(getConn());
}
}

获得 URLClassLoader 对象后, 调用 loanClass() 方法来加载指定的类

自定义类加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;

public class CompileClassLoader extends ClassLoader{

// 读取一个文件的内容
@SuppressWarnings("resource")
private byte[] getBytes(String filename) throws IOException{
File file = new File(filename);
long len = file.length();
byte[] raw = new byte[(int) len];
FileInputStream fin = new FileInputStream(file);
// 一次读取 class 文件的全部二进制数据
int r = fin.read(raw);
if (r != len)
throw new IOException("无法读取全部文件" + r + "!=" + len);
fin.close();
return raw;
}

// 定义编译指定 java 文件的方法
private boolean compile(String javaFile) throws IOException {
System.out.println("CompileClassLoader:正在编译" + javaFile + "……..");
// 调用系统的 javac 命令
Process p = Runtime.getRuntime().exec("javac" + javaFile);
try {
// 其它线程都等待这个线程完成
p.waitFor();
} catch (InterruptedException ie){
System.out.println(ie);
}
// 获取 javac 的线程的退出值
int ret = p.exitValue();
// 返回编译是否成功
return ret == 0;
}

// 重写 Classloader 的 findCLass 方法
protected Class<?> findClass(String name) throws ClassNotFoundException{
Class clazz = null;
// 将包路径中的. 替换成斜线 /
String fileStub = name.replace(".", "/");
String javaFilename = fileStub + ".java";
String classFilename = fileStub + ".class";
File javaFile = new File(javaFilename);
File classFile = new File(classFilename);
// 当指定 Java 源文件存在, 且 class 文件不存在, 或者 Java 源文件的修改时间比 class 文件 // 修改时间晚时, 重新编译
if (javaFile.exists() && (!classFile.exists())
|| javaFile.lastModified()> classFile.lastModified()) {
try {
// 如果编译失败, 或该 Class 文件不存在
if (!compile(javaFilename) || !classFile.exists()) {
throw new ClassNotFoundException("ClassNotFoundException:"
+ javaFilename);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
// 如果 class 文件存在, 系统负责将该文件转化成 class 对象
if (classFile.exists()) {
try {
// 将 class 文件的二进制数据读入数组
byte[] raw = getBytes(classFilename);
// 调用 Classloader 的 defineClass 方法将二进制数据转换成 class 对象
clazz = defineClass(name, raw, 0, raw.length);
} catch (IOException ie) {
ie.printStackTrace();
}
}

// 如果 claszz 为 null, 表明加载失败, 则抛出异常
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}

// 定义一个主方法
public static void main(String[] args) throws Exception {
// 如果运行该程序时没有参数, 即没有目标类
if (args.length < 1) {
System.out.println("缺少运行的目标类, 请按如下格式运行java源文件: ");
System.out.println("java CompileClassLoader ClassName");
}
// 第一个参数是需要运行的类
String progClass = args[0];
// 剩下的参数将作为运行目标类时的参数, 所以将这些参数复制到一个新数组中
String progargs[] = new String[args.length - 1];
System.arraycopy(args, 1, progargs, 0, progargs.length);
CompileClassLoader cl = new CompileClassLoader();
// 加载需要运行的类
Class<?> clazz = cl.loadClass(progClass);
// 获取需要运行的类的主方法
Method main = clazz.getMethod("main", (new String[0]).getClass());
Object argsArray[] = { progargs};
main.invoke(null, argsArray);
}
}

JVM 中除了根类加载器之外的所有类的加载器都是 ClassLoader 子类的实例, 通过重写 ClassLoader 中的方法, 实现自定义的类加载器

loadClass(String name,boolean resolve): 为 ClassLoader 的入口点, 根据指定名称来加载类, 系统就是调用 ClassLoader 的该方法来获取制定累对应的
Class 对象

findClass(String name): 根据指定名称来查找类

推荐使用 findClass 方法

类的链接

当类被加载后, 系统会为之生成一个 Class 对象, 接着将会进入连接阶段, 链接阶段负责把类的二进制数据合并到 JRE 中

三个阶段

验证: 检验被加载的类是否有正确的内部结构, 并和其他类协调一致

准备: 负责为类的类变量分配内存. 并设置默认初始值

解析: 将类的二进制数据中的符号引用替换成直接引用

类的初始化

JVM 负责对类进行初始化, 主要对类变量进行初始化

在 Java 中对类变量进行初始值设定有两种方式: ① 声明类变量是指定初始值 ② 使用静态代码块为类变量指定初始值

JVM 初始化步骤

  1. 假如这个类还没有被加载和连接, 则程序先加载并连接该类
  2. 假如该类的直接父类还没有被初始化, 则先初始化其直接父类
  3. 假如类中有初始化语句, 则系统依次执行这些初始化语句

类初始化时机

  1. 创建类实例. 也就是 new 的方式
  2. 调用某个类的类方法
  3. 访问某个类或接口的类变量, 或为该类变量赋值
  4. 使用反射方式强制创建某个类或接口对应的 java.lang.Class 对象
  5. 初始化某个类的子类, 则其父类也会被初始化
  6. 直接使用 java.exe 命令来运行某个主类

类加载机制(类加载过程和类加载器)