Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能。

关于类的加载机制的详细解释可以参考下面几篇文章

其中链接又分三个步骤,如下图所示

image

装载

装载是类加载的第一个阶段,主要是查找并加载类的二进制数据(查找和导入Class文件),通过类的全限定名获取二进制字节流,这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,然后在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口

链接

分为三个步骤:

  • 1、验证:确保被加载类的正确性。
  • 2、准备:为类的静态变量准备内存,并将其初始化为默认值。这时候进行内存分配的仅仅是static的类变量,而不是实例变量,而所设置的初始值是数据类型默认的零值(0、0L、null、false等)。加入public static int a = 3,这里在准备阶段赋的初始值是0而不是3,,在初始化阶段才会把3赋值给a。
  • 3、解析:把类的符合引用转换成直接引用。符号引用就是一组符号来描述目标,可以是任何字面量,直接引用就是直接指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。

初始化

初始化是对类的静态变量,静态代码块执行初始化操作。为类的静态变量赋予正确的初始值,如上面的a=3。在java中对static类变量进行初始值设定有两种方式。1、声明类变量是指定初始值。2、使用静态代码块为类变量指定初始值。

只有当一个类被主动使用时,JVM才会对其初始化,如下面6中情况为主动使用

  1. 创建类的实例,也就是new一个对象
  2. 访问某个类或接口的静态变量或对该静态变量赋值。
  3. 调用类的静态方法
  4. 反射
  5. 初始化一个类的子类(会首先初始化子类的父类)
  6. JVM启动时明确标明的启动类(即包含main方法的那个类)

类加载器

类加载器主要完成类的加载过程,JVM的类加载是通过ClassLoader及其子类来完成的,类加载器可以从本地系统、网络、zip或jar等归档文件、数据库、源文件动态编译、Class.forName()、ClassLoader.loadClass()等加载class文件。类的层次关系和加载顺序可以由下图来描述:

image

  • Bootstrap ClassLoader(引导类加载器):负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类
  • Extension ClassLoader(扩展类加载器):负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包
  • App ClassLoader或system class loader(系统类加载器):负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它
  • Custom ClassLoader:通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader

加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载,就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。

自定义类加载器

java.lang.ClassLoader类介绍

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求

类加载器树状组织结构示意图

image

ClassLoader 中与加载类相关的方法

方法 说明
getParent() 返回该类加载器的父类加载器
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的
resolveClass(Class<?> c) 链接指定的 Java 类

对于上面中给出的方法,表示类名称的 name参数的值是类的二进制名称。需要注意的是内部类的表示,如 com.example.Sample$1和 com.example.Sample$Inner等表示方式。

类加载器的树状组织结构示例如下

1
2
3
4
5
6
7
8
9
10
public class ClassLoaderTree {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}

输出结果

1
2
sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11

第一个输出的是 ClassLoaderTree类的类加载器,即系统类加载器。它是sun.misc.Launcher$AppClassLoader类的实例;第二个输出的是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader类的实例。需要注意的是这里并没有输出引导类加载器,这是由于有些 JDK 的实现对于父类加载器是引导类加载器的情况,getParent()方法返回 null。

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下Java虚拟机是如何判定两个Java类是相同的。Java虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个Java类com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。

1
2
3
4
5
6
7
package com.example;
public class Sample {
private Sample instance;
public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}

测试Java 类是否相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void testClassIdentity() {
String classDataRootPath = "C:\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}

上例中使用了类 FileSystemClassLoader的两个不同实例来分别加载类 com.example.Sample,得到了两个不同的 java.lang.Class的实例,接着通过 newInstance()方法分别生成了两个类的对象 obj1和 obj2,最后通过 Java 的反射 API 在对象 obj1上调用方法 setSample,试图把对象 obj2赋值给 obj1内部的 instance对象,结果如下:

1
2
3
4
5
6
7
8
9
10
11
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more

从上面运行结果可以看到,运行时抛出了java.lang.ClassCastException异常。虽然两个对象obj1和obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。

代理模式

在类的加载过程中,,类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的(称为一个类的定义加载器defining loader);而启动类的加载过程是通过调用loadClass来实现的(称为初始加载器initiating loader)。

在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类com.example.Inner,则由类com.example.Outer的定义加载器负责启动类com.example.Inner的加载过程。

Class.forName

Class.forName是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize的值为 true,loader的值为当前类的类加载器。Class.forName的一个很常见的用法是在加载数据库驱动的时候

开发自定义类加载器

文件系统类加载器

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
public class FileSystemClassLoader extends ClassLoader{
private String rootDir;
public FileSystemClassLoader(String rootDir){
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException{
byte[] classData = getClassData(className);
if(classData == null){
throw new ClassNotFoundException();
}else{
return defineClass(className,classData,0,classData.length);
}
}
private byte[] getClassData(String className){
String path = this.rootDir+ File.separator+className.replace('.',File.separatorChar)+".class";
try {
InputStream in = new FileInputStream(path);
return IOUtils.toByteArray(in);
}catch (IOException e){
e.printStackTrace();
}
return null;
}
}

类 FileSystemClassLoader继承自类java.lang.ClassLoader。在前面表中列出的java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写findClass(Stringname)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写loadClass()方法,而是覆写 findClass()方法。

网络加载器

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
public class NetworkClassLoader extends ClassLoader{
private String networkUrl;
private List<String> loadHistory = new ArrayList<>();//对加载过的class或jar做一个记录
private URLClassLoader urlClassLoader;//内置一个URLClassLoader用于加载解析jar包
public NetworkClassLoader(String networkUrl) throws Exception{
this(networkUrl,null);
}
public NetworkClassLoader(String networkUrl,String className) throws Exception {
super();
loadHistory.add(networkUrl);
this.networkUrl = networkUrl;
//如果构建地址是jar,初始化URLClassLoader并解析jar
if(StringUtils.equals(getExtension(networkUrl),"jar")){
URL[] urls = new URL[]{ new URL(networkUrl)};
urlClassLoader = new URLClassLoader(urls,Thread.currentThread().getContextClassLoader());
if(StringUtils.isNotBlank(className)){
urlClassLoader.loadClass(className);
}
}else if(StringUtils.equals(getExtension(networkUrl),"class")&&StringUtils.isNotBlank(className)){
createClass(networkUrl,className);
}
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException{
try {
Class<?> cls = this.findLoadedClass(className);//从当前loader查找是否已经加载过该类
if(cls!=null){
return cls;
}else{
if(urlClassLoader!=null){
cls = urlClassLoader.loadClass(className);//尝试从内置的URLClassLoader加载该类
if(cls!=null){
return cls;
}
}
if(StringUtils.equals(getExtension(networkUrl),"class")){
return createClass(networkUrl,className);//从网络地址加载类
}else{
return null;
}
}
} catch (Exception e) {
throw new ClassNotFoundException(e.getMessage());
}
}
/**
* 添加一个class文件地址,并指定className名称
* @param classUrl
* @param className
* @return
* @throws Exception
*/
public Class<?> addClassUrl(String classUrl,String className) throws Exception{
if(!StringUtils.equals(getExtension(classUrl),"class")){
throw new RuntimeException("只能是class文件");
}
Class<?> cls = this.findLoadedClass(className);
if(cls!=null){
return cls;
}else{
cls = urlClassLoader.loadClass(className);
if(cls!=null){
return cls;
}
}
networkUrl = classUrl;
loadHistory.add(classUrl);
return createClass(classUrl,className);
}
/**
* 添加一个jar文件地址
* @param url
* @throws Exception
*/
public void addJarUrl(URL url) throws Exception {
if(url==null){
throw new Exception("url 不能为空");
}
if(!StringUtils.equals(getExtension(url.getPath()),"jar")){
throw new Exception("addJarUrl方法只能加载jar包");
}
if(urlClassLoader==null){
URL[] urls = new URL[]{ new URL(networkUrl)};
urlClassLoader = new URLClassLoader(urls,Thread.currentThread().getContextClassLoader());
}
//调用URLClassLoader的addURL方法,自动下载并解析jar
Method addURLMethod = urlClassLoader.getClass().getDeclaredMethod("addURL",URL.class);
addURLMethod.setAccessible(true);
addURLMethod.invoke(urlClassLoader,url);
networkUrl = url.toString();
loadHistory.add(url.toString());
}
/**
* 从网络class地址下载字节码并加载类
* @param classUrl
* @param className
* @return
* @throws Exception
*/
private Class<?> createClass(String classUrl,String className) throws Exception {
URL url = new URL(classUrl);
InputStream is = url.openStream();
byte[] bytes = IOUtils.toByteArray(is);
if(bytes!=null){
return defineClass(className,bytes,0,bytes.length);
}else{
return null;
}
}
private String getExtension(String url){
int index = url.indexOf("?");
if(index>0){
url = url.substring(0,index);
}
int dotIndex = url.lastIndexOf(".");
return url.substring(dotIndex+1);
}
}

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
public static void main(String[] args){
try {
NetworkClassLoader loader = new NetworkClassLoader("http://localhost:8080/test/GameType.class");
Object gameTypeObj = loader.loadClass("test.GameType").newInstance();
Method getGameType = gameTypeObj.getClass().getMethod("getGameType");
System.out.println(getGameType.invoke(gameTypeObj));
System.out.println("加载器:"+gameTypeObj.getClass().getClassLoader().toString());
loader.addJarUrl(new URL("http://localhost:8080/test/game.jar"));
Object gameObj =loader.loadClass("test.Game").newInstance();
Method getName = gameObj.getClass().getMethod("getName");
System.out.println(getName.invoke(gameObj));
System.out.println("加载器:"+gameObj.getClass().getClassLoader().toString());
Class gameObj2Class = loader.loadClass("test.Game");
Object gameObj2 = gameObj2Class.newInstance();
Method getName2 = gameObj2.getClass().getMethod("getName");
System.out.println(getName2.invoke(gameObj2));
System.out.println("加载器:"+gameObj2.getClass().getClassLoader().toString());
Object gameTypeObj2 = loader.loadClass("test.GameType").newInstance();
Method getGameType2 = gameTypeObj2.getClass().getMethod("getGameType");
System.out.println(getGameType2.invoke(gameTypeObj));
System.out.println("加载器:"+gameTypeObj2.getClass().getClassLoader().toString());
}catch (Exception e){
e.printStackTrace();
}
}

测试时在一个${TOMCATE_HOME}/webapp/test/下放了一个game.jar,一个Game.class和一个GameType.class文件。
Game.java和GameType.java如下

1
2
3
4
5
6
7
8
9
10
11
12
class Game{
String name;
GameType type;
public void getName(){
System.out.println("test game Name,"+type.getGameType());
}
}
class GameType{
public void getGameType(){
System.out.println("test game Type");
}
}

输出结果

1
2
3
4
5
6
7
8
test game Type
加载器:com.basic.loader.NetworkClassLoader@59f99ea
test game Name,test game Type
加载器:java.net.URLClassLoader@6ebc05a6
test game Name,test game Type
加载器:java.net.URLClassLoader@6ebc05a6
test game Type
加载器:com.basic.loader.NetworkClassLoader@59f99ea