关于java类的加载过程,一直弄的不是很懂,最近看了相关资料,来总结一下。
1. 类加载过程
类从被加载到虚拟机内存中开始,到使用之前,要经历加载、连接、初始化,三个阶段。
其中连接包括:验证、准备、解析。
上述顺序只是一个大致的参考,具体实现顺序可能有穿插。
虚拟机规范规定5种场景会触发类的初始化,前面的步骤自然要在初始化之前完成。
加载过程除了可以系统自动完成之外,也可能可以通过ClassLoader类来由程序员控制,加载主要完成:
1. 通过类的全限定名来获取定义此类的二进制字节流
2. 将二进制字节流转化为方法区的运行时数据结构,及Class实例对象。
完成上述过程的是类加载器 .
2. 类加载器
系统默认提供了三种类型的类加载器:
用原生C++实现的启动类加载器bootstrap class loader,其余的都是用java实现的。
引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
java.lang.ClassLoader是一个抽象类,为我们实现自己的类加载器提供了可能,我们继承它即可。
这里再次摘抄:
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 类。 |
- 类加载器的树状组织结构
根据这个结构,类加载器加载类的时候有一个“双亲委派模型”。这个名称我有点不懂,为什么叫“双亲”,明明只是一个父类。也有叫代理模式的,个人觉得这个名字更好一些。 代理模式
当某一个类加载器(也叫初始加载器)在尝试加载某个类的字节码时,会先代理给它的父类加载器,如果父类加载器加载不了,再交由该类加载器加载。
这样做的好处是使java类随着它的类加载器一起具备带有优先级的层次关系,越基础的类越由顶层加载器加载,这样就能保证类的一致性(因为相同类名称的类如果分别由不同的类加载器加载,也认为是两个不同的类,这是通过在Class对象中附带的定义加载器来实现的)。反代理模式的现象
在Java核心类库中定义了一些SPI,翻译过来叫做服务提供接口,具体的实现由第三方来提供,因此我们常常需要在应用中包含第三方提供的jar包,但是不可能都添加到核心库中。因此SPI需要由引导类加载器来加载,但是其中涉及到的调用具体部分,却需要由系统类加载器来加载,但是引导类加载器是不可能让系统类加载器来为其代理的,因此代理模式解决不了。
有线程上下文类加载器可以解决上述问题,还有Class.forName方法。
3. 自定义类加载器
一般通过继承ClassLoader来实现,覆盖其中的某些方法即可。
loadClass(String name)
findClass(String name)
defineClass(String name, byte[] b, int off, int len)
主要涉及到上述三个方法,loadClass中一般包含加载逻辑,比如实现代理逻辑:先尝试代理给父类加载器,如果不成功就调用自己的findClass来加载类,findClass最终将调用defineClass将b中包含的byte数据转换成Class对象。一般为了顺应代理逻辑,推荐覆盖findClass即可。defineClass是final类型的,使用的是原生代码。
小实验:利用自己定义的FileSystemClassLoader加载磁盘上某个地方的class文件。
eclipse工程目录:
package jvmtest;
import java.lang.reflect.Method;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
ClassLoader myLoader = new FileSystemClassLoader("d:\\up");
Class<?> class1 = myLoader.loadClass("utils.SayHello");
Object obj1 = class1.newInstance();
Method sayhello = class1.getMethod("hi", null);
sayhello.invoke(obj1, null);
}
}
FileSystemClassLoader是IBM开发社区中的示例,类似的还可以开发从网络中加载的等等。
在调试过程中多次出现java.lang.NoClassDefFoundError: utils/SayHello (wrong name: SayHello)
跟踪了一下,是从defineClass中抛出的,发现字节都已经成功读取到了,但是在defineClass中报错。
后来终于发现是在源文件中没有包含package, 导致defindClass中发现二进制字节流中的信息与输入的类名不一致,导致error。
最后结果:
hi, there!