类加载器ClassLoader
一直想搞清楚类加载器是什么东西,终于有机会好好研究一下。
类加载器是什么?
定义:将“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个代码模块的类就是类加载器——ClassLoader。其实说的通俗一点就是将Class加载到JVM中去。
ClassLoader结构
ClassLoader的三个作用:
将Class加载到JVM中
审查每个类该由谁加载(父优先的等级加载机制)
将Class字节码重新解析成JVM统一要求的对象格式
ClassLoader是我们经常用到或扩展的抽象类,下面是它的几个主要方法:
//将byte字节流解析成JVM能够识别的Class对象
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
//实现类的加载规则
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
//加载类
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
...
}
defineClass方法用来将byte字节流解析成JVM能够识别的Class对象。这个方法不仅可以使用class文件实例化对象,还可以通过其他方式:从网络接收到一个类的字节码流来创建对象
findClass方法主要用来实现类的加载规则
loadClass用来加载类
类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在JVM中的唯一性,每一个类加载器都有一个独立的类名称空间。通俗来说就是:比较两个类是否相等,只有在这两个类都是被同一个类加载器加载的前提下进行比较才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。
这其实很好理解,就如同你有两个证,一个是教师资格证,一个是工程师资格证。但是你去应聘教师岗位的时候需要的是教师的身份,不需要工程师的身份,由于证的不同,身份也就发生了变化,即使人是同一个。
下面看一下不同类加载器对instanceof关键字运算结果的影响:
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
//自定义ClassLoader
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.yuangh.classloader.demo.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.yuangh.classloader.demo.ClassLoaderTest);
}
}
//结果
class com.yuangh.classloader.demo.ClassLoaderTest
false
分析: 这里我们是自己定义了一个类加载器来加载“com.yuangh.classloader.demo.ClassLoaderTest”这个类,并实例化了对象。关于上述的结果,可能会有所迷惑,obj.getClass()的结果都打印出是“com.yuangh.classloader.demo.ClassLoaderTest”了吗?为什么用instanceof判断得到结果是false呢?其实,这是因为虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,另外一个是由我们自定义的类加载器加载的,虽然来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查时自然为false。
双亲委派模型
从JVM层面来说,存在两种不同的类加载器:
一种是启动类加载器(Bootstrap ClassLoader),该类加载器由C++语言实现,是虚拟机自身的一部分
另一种就是其他的所有类加载器,这些累加器器由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader
从Java开发人员层面来说,类加载器可以分为以下三个层面:
- 启动类加载器(Bootstrap Classloader)
该类加载器负责将存放在 \lib 目录中的,或者被 -Xbootstrappath 参数所指定路径的,并且是被虚拟机识别的类库加载到虚拟机内存中来。启动类加载器无法被Java程序直接引用。
- 扩展类加载器(Extension ClassLoader)
该类加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责将 \lib\ext 目录中的,或者被 java.ext.dirs 系统变量所指定路径中的所有类库加载到虚拟机中,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader)
该类加载器由sun.misc.Launcher$AppClassLoader 实现。该类是 ClassLoader.getSystemClassLoader() 的返回值,所以一般被称为系统累加器,它负责加载用户类路径(ClassPath)下的所有类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
下面看一下ExtClassLoader和AppClassLoader的加载路径:
public class Test {
public static void main(String[] args) throws Exception {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println("============Extensiion ClassLoader===========");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("============Application ClassLoader===========");
String[] str = System.getProperty("java.class.path").split(";");
for (String str1 : str) {
System.out.println(str1);
}
}
}
//结果如下:
sun.misc.Launcheraaa@qq.com
============Extensiion ClassLoader===========
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
============Application ClassLoader===========
C:\Program Files\Java\jdk1.8.0_111\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\rt.jar
C:\Program Files\Java\jdk1.8.0_111\jre\lib\ext\algs4.jar
//项目路径
D:\ideaIU-2018.1.6.win\project\Java\ClassLoader\target\classes
D:\ideaIU-2018.1.6.win\lib\idea_rt.jar
三个ClassLoader类型的参数展示:
ClassLoader类型 | 参数选项 | 说明 |
---|---|---|
Bootstrap ClassLoader | -Xbootclasspath: -xbootclasspath/a: -Xbootclasspath/p: |
设置Bootstrap ClassLoader的搜索路径 把路径添加到已存在的Bootstrap ClassLoader搜索路径的后面 把路径添加到已存在的Bootstrap ClassLoader搜索路径的前面 |
ExtClassLoader | -Djava.ext.dirs | 设置 ExtClassLoader的搜索路径 |
AppClassLoader | -Djava.class.path=-cp 或 -classpath | 设置 AppClassLoader 的搜索路径 |
双亲委派模型展示
在上面介绍了三种主要的类加载器,我们的应用程序就是由这三个累加器互相配合进行加载的,如有必要我们也可以加入自定义的类加载器,下面是这些类加载器之间的关系:
上面这种层次关系称为双亲委派模型,该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。类加载器之间的父子关系不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码。
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它不会首先尝试去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求时(在它的搜索范围内没有搜索到所需要的类),子加载器才会尝试自己去加载。
双亲委派模型的好处:Java类随着它的类加载器一起具备了一种带有优先级的层级关系。例如:java.lang.Object 这个类存放在 rt.jar 之中,无论哪一个类加载器加载这个类,最终都会委派给处于模型顶端的启动类加载器加载,因此Object类在程序的各种类加载器环境下都是同一个类。
双亲委派模型的实现在java.lang.ClassLoader的loadClass()方法中:
//加载类
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先检查请求的类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出 ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 父类加载器无法加载时
// 使用自己的类加载器加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
自定义ClassLoader
通过前面的分析介绍,我们已经了解了什么类加载器,以及双亲委派模型的原理,现在我们就自己实现ClassLoader。下面是三种场景:
自定义ClassLoader找到那些不在ClassPath路径下的class文件
对于我们要加载的类做特殊处理。例如:保证通过网络传输的类的安全性,可以将类经过加密之后再传输,在加载到JVM之前先对类的字节码再解密。
定义类的实现机制,检查已经加载的class文件是否被修改,如果被修改了,可以重新加载这个类,从而实现类的热部署。
加载自定义路径下的class文件
1. 继承ClassLoader
//测试类
public class ClassLoaderCase {
public void test() {
System.out.println("我是一个测试方法。。。");
}
}
public class PathCLassLoader extends ClassLoader {
private String classPath; //类路径
private String packageName = "com.yuangh.classloader.demo";
public PathCLassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//如果该类是指定包名下的类,就使用自定义的类加载器加载
if (packageName.startsWith(name)) {
//得到要加载的类的字节数组
byte[] classData = getData(name);
//如果为空,代表该类不存在
if (classData == null) {
throw new ClassNotFoundException();
} else {
//将byte字节流解析为JVM能够识别的Class对象
return defineClass(name, classData, 0, classData.length);
}
//如果不是,就交给父类加载
} else {
return super.loadClass(name);
}
}
/**
* 得到类的字节数组
*/
private byte[] getData(String className) {
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception {
PathCLassLoader pathCLassLoader = new PathCLassLoader("D:\\ideaIU-2018.1.6.win\\project\\Java\\ClassLoader\\src\\main\\java\\com\\yuangh\\classloader\\demo\\");
Class<?> classLoadercase1 = pathCLassLoader.findClass("com.yuangh.classloader.demo.ClassLoaderCase");
ClassLoaderCase classLoadercase2 = (ClassLoaderCase) classLoadercase1.newInstance();
classLoadercase2.test();
}
}
//结果:
我是一个测试方法。。。
2. 继承URLClassLoader
public class URLPathClassLoader extends URLClassLoader {
private String packageName = "com.yuangh.classloader.demo";
public URLPathClassLoader(URL[] classPath, ClassLoader parent) {
super(classPath, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> aClass = findLoadedClass(name);
if (aClass != null) {
return aClass;
}
if (!packageName.startsWith(name)) {
return super.loadClass(name);
} else {
return findClass(name);
}
}
}
加载自定义格式的class文件
public class NetCLassLoader extends ClassLoader {
private String classPath; //类路径
private String packageName = "com.yuangh.classloader.demo";
public NetCLassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> aClass = findLoadedClass(name);
if (aClass != null) {
return aClass;
}
if (packageName.startsWith(name)) {
//得到要加载的类的字节数组
byte[] classData = getData(name);
//如果为空,代表该类不存在
if (classData == null) {
throw new ClassNotFoundException();
} else {
//将byte字节流解析为JVM能够识别的Class对象
return defineClass(name, classData, 0, classData.length);
}
//如果不是,就交给父类加载
} else {
return super.loadClass(name);
}
}
/**
* 得到类的字节数组
*/
private byte[] getData(String className) {
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
URL url = new URL(path);
InputStream is = url.openStream();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws Exception {
NetCLassLoader netCLassLoader = new NetCLassLoader("D:\\ideaIU-2018.1.6.win\\project\\Java\\ClassLoader\\src\\main\\java\\com\\yuangh\\classloader\\demo\\");
Class<?> classLoadercase1 = netCLassLoader.findClass("com.yuangh.classloader.demo.ClassLoaderCase");
ClassLoaderCase classLoadercase2 = (ClassLoaderCase) classLoadercase1.newInstance();
classLoadercase2.test();
}
}
//结果
我是一个测试方法。。。
实现类的热部署
前面已经说过,JVM表示两个类是否是同一个类会有两个条件:一是看完整类名是否一样,包括包名。二是看加载这个类的ClassLoader是否是同一个类加载器类实例。即使是同一个累加器类的两个实例,加载同一个类也会不一样。所以要实现热部署可以创建不同的ClassLoader实例对象,然后通过这个不同的实例对象来加载同名的类。
public class ClassReloader extends ClassLoader {
private String classPath; //类路径
String classname = "com.yuangh.classloader.demo.ClassLoaderCase";
public ClassReloader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(classname, classData, 0, classData.length);
}
}
/**
* 得到类的字节数组
*/
private byte[] getData(String className) {
String path = classPath + className;
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while ((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
try {
String path = "D:\\ideaIU-2018.1.6.win\\project\\Java\\ClassLoader\\target\\classes\\com\\yuangh\\classloader\\demo\\";
ClassReloader reloader = new ClassReloader(path);
Class r = reloader.findClass("ClassLoaderCase.class");
System.out.println(r.newInstance());
ClassReloader reloader2 = new ClassReloader(path);
Class r2 = reloader2.findClass("ClassLoaderCase.class");
System.out.println(r2.newInstance());
System.out.println(r.newInstance() == r2.newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//结果:
aaa@qq.com45ee12a7
aaa@qq.com4b67cf4d
false
参考
《深入理解Java虚拟机》 《深入分析Java Web技术内幕》