JAVA类加载机制全解析

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

类加载

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

类文件来源
  • 从本地文件系统加载的class文件
  • 从JAR包加载class文件
  • 从网络加载class文件
  • 把一个Java源文件动态编译,并执行加载

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

类加载器

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

这里写图片描述

  • 根类加载器(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.em")};
URLClassLoader myClassLoader=new URLClassLoader(urls);
method1 driver=(method1) myClassLoader.loadClass("com.em.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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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命令来运行某个主类

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

迹_Jason wechat
分享快乐,感受科技的温度
坚持原创技术分享,您的支持将鼓励我继续创作!