jvm(4)类加载机制以及类加载器

1、类加载器的分类

  1. Bootstrap ClassLoader 负责加载$JAVA_HOME中的 jre/lib/rt.jar里面所有的class 或者 Xbootclassoath选项指定的jar包,由C++实现,不是ClassLoader的子类
  2. Extenson ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中的jre/lib/*.jar或者-Djava.ext.dirs指定目录下的jar包。
  3. App ClassLoader 负责加载classpath中指定的jar包以及 -Djava.class.path 所指定目录下的类和jar包
  4. Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载器。属于应用程序根据自身需要自定义的classLader。如Tomcat,Jboss都会根据J2EE规范自行实现ClassLoader

2、类加载原则:

检查某个类是否已经加载,顺序是自底而上,从custom classloader 到 Bootstrap classloader 逐层检查,只要某个classloader已加载就是为已加载此类,保证此类之加载一次。

加载的顺序是自顶向下,也就是先由上层尝试加载此类,这就是双亲委派机制

双亲委派定义:如果一个类加载器在接到加载棋类的请求时,它首先不会自己藏市区加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归。如果父类加载器可以完成加载任务,就成功返回,只有父类加载器不能完成此加载任务是,自己才去加载。

双亲委派优势:java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如java中的Object类,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给处于模型顶端的驱动类加载器进行加载。因此Object类在各种类加载环境中都是同一个类。如果不采用这种模式,那么由各个类自己加载的话,系统中会存在多种不同的Object类。

3、类加载器重要方法分析:

img

可以看到顶层的类加载器是classLoader类,它是一个抽象类,几乎所有的类加载器都继承自classloader(不包括启动类加载器),下面介绍一下classloader中比较重要的方法

  1. loadClass(String)

该方法用来加载指定名称的字节码文件,不建议用户重写但可以直接调用该方法。此方法是ClassLoader类自己实现的,方法逻辑就是双亲委派的实现。

loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

  1. findClass(String)

自定义加载器时需要重写的方法。该方法是在loadClass方法中被调用的,当loadClass方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载。将向上委托和自己加载两块逻辑分开,可以保证自定义的加载器也符合双亲委派模型

需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下:

  1. defineClass(byte[] b, int off, int len)

此方法用于将字节流解析成Jvm能够使别的Class对象,此方法由ClassLoader类实现,通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,比如通过网络接收字节流等

此方法通常与findClass方法一起使用,一般情况下在自定义类加载器时,会直接覆盖findClass方法去获取字节码文件,转换成字节流之后交给defineClass方法去实例化Class对象

1
2
3
4
5
6
7
8
9
10
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
4、自定义类加载器

1、 类与类加载器

在jvm中表示两个class对象是否为同一个对象需要满足两个必要条件

  • 全类名必须一致
  • 加载这个类的classLoader必须相同

也就是说,在jvm中即使两个类来自同一个字节码文件,被同一个虚拟机加载,但是只要加载它们的加载器不是同一个,那么这两个类对象也是不相等的,这是因为不同的加载器实例对象拥有不同的独立的类名称空间。

2、显示加载和隐式加载

显示加载:显示的使用代码加载一个类,如Class.forName()以及 getClassLoader().loadClass(string)

隐式加载:虚拟机自动加载,加载某个类时,需要加载其依赖的其他类

3、编写类加载器

  • 当需要加载的类不在classpath上时
  • 当字节码文件是通过网络传输并且可能被加密的时候
  • 当需要实现热部署功能,即一个字节码文件通过不同的类加载器产生不同的class对象
  • 当项目依赖多版本jar包时

自定义File类加载器:

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 class FileClassLoader extends ClassLoader{
private String rootDir ;

public FileClassLoader(String rootDir) {
this.rootDir = rootDir;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
String clazzLocation = rootDir + name + ".class";
try {
InputStream in = new BufferedInputStream(new FileInputStream(clazzLocation));
byte[] buffer = new byte[1024];
int len = in.read(buffer);
clazz = defineClass(name,buffer,0,len);
} catch (Exception e) {
e.printStackTrace();
}
return clazz;
}

public static void main(String[] args) throws Exception{
String rootDir = "/Users/zhaozhengkang/Documents/";
FileClassLoader classLoader = new FileClassLoader(rootDir);
Class<?> clazz = classLoader.loadClass("DemoClass");
System.out.println(clazz.getName());
Method method = clazz.getMethod("pringHello");
method.invoke(clazz.newInstance());
}
}

关键是重写findClass方法,方法中读取指定路径上的指定名称的字节码文件,获取到字节数组,在调用defineClass实例化Class对象。main方法中使用反射的方式调用了DemoClass中的方法。

抽象类ClassLoader构造方法中已经制定了 parent 为AppClassLoader

因此,只要我们使用继承ClassLoader的方式编写自定义的CL,那么这个加载器就是遵循双亲委派机制的加载器。

这里是为了演示自定义加载器的方法,实际自定义文件类加载器最好的方式是继承FileUrlClassLoader

5、破坏双亲委派

java的SPI机制,指的是java定义一套接口,这套接口允许第三方提供实现。常见的如JDBC。

  • java的SPI接口属于java的核心库,一般存在与rt.jar包中,由Bootstrap类加载器加载。
  • SPI的实现代码则是作为java应用所依赖的jar包被存放在classpath下,启动类加载器加载不到
  • 同时由于双亲委派机制的存在,启动类加载器也无法委托具体的AppClassLoader去加载

鉴于以上原因,我们需要一种特殊的类加载器来加载这种情形,那就是线程上下文类加载器。

我们可以通过java.lang.Thread类中的getContextClassLoader()setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。

img

线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。

线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题。

https://blog.csdn.net/t894690230/article/details/73252331

参考文章:

深入理解Java类加载器(ClassLoader)

真正理解线程上下文类加载器(多案例分析)