0%

Java类加载

序言

梦里玉人方下马,恨他天外一声鸿。

今天来总结Java类加载机制。

编译器与解释器

我们通常会把编程语言的处理器分为编译器解释器

编译器则是将某种语言代码转换为另外一种语言的程序,通常会转换为机器语言。

解释器是一种用来执行程序的软件,它会根据程序代码中的算法执行运算,如果这个软件是根据虚拟的或者类似机器语言的程序设计语言写成,那也称为虚拟机。

Java会混用解释器和编译器,Java会先通过编译器将源代码转换为Java二进制代码(字节码),并将这种虚拟的机器语言保存在文件中(通常是.class文件),之后通过Java虚拟机(JVM)的解释器来执行这段代码。

类和类加载器

这里直接上《深入理解Java虚拟机》原文,写得很好:

对于任意的一个类。其实都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。两个类是否相等,其实只有这两个类都是由同一个类加载器加载的前提下才有意义。只要加载它们的类加载器不同,那么这两个类就必定不相等。

Java是面向对象的语言,字节码中包含了很多Class信息。在 JVM 解释执行的过程中,ClassLoader就是用来加载Java类的,它会将Java字节码加载到内存中。每个 Class 类对象的内部都有一个 classLoader 属性来标识自己是由哪个 ClassLoader 加载的。

image-20210306155604696

Java的类加载体系:parent属性 理论关系

1
Bootstrap Classloader -> Extension ClassLoader -> Application ClassLoader

JDK类实现体系:实现/继承的关系,通常继承URLClassLoader

1
ClassLoader -> Secure ClassLoader -> URL ClassLoader -> ExtClassLoader/AppClassLoader

image-20210225193851385

类加载层次

站在JVM角度来看,只存在两种不同的类加载器:

  • 启动类加载器 BootstrapClassLoader 使用C++实现
  • 其他类加载器/都是由Java实现/全部继承抽象类java.lang.ClassLoader

Bootstrap ClassLoader 启动类加载器

负责加载存放在JAVA_HOME/lib目录下,或者被-Xbooclasspath参数所指定的路径中存放的,并且可以被JVM识别的类库,加载到虚拟机的内存中。

底层原生代码是C++语言编写,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。

出于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类。

它负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.. 都在里面。这个 ClassLoader 比较特殊,它其实不是一个ClassLoader实例对象,而是由C代码实现。用户在实现自定义类加载器时,如果需要把加载请求委派给启动类加载器,那可以直接传入null作为 BootstrapClassLoader。

Extension ClassLoader 扩展类加载器

负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar,库名通常以 javax 开头。

Application ClassLoader 系统类加载器

直接提供给用户使用的ClassLoader,它会加载 CLASSPATH 环境变量或者 java.class.path 属性里定义的路径中的 jar 包和目录,负责加载包括开发者编写的代码、以及依赖的第三方库中的类。

如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

可以通过 ClassLoader.getSystemClassLoader() 来获取它。

特殊:URLClassloader

ClassLoader抽象类的一种实现,它可以根据URL搜索类或资源,并进行远程加载。在JDK实现中,BootstrapClassLoader、ExtClassLoader、AppClassLoader等都是 URLClassLoader 的子类。

ExtClassLoader 和 AppClassLoader 类的实现代码位于rt.jar 中的 sun.misc.Launcher 类中,Launcher是由BootstrapClassLoader加载的。

双亲委派机制

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

image-20201105161406752

ClassLoader类 - 核心方法

每个 ClassLoader 对象都是一个 java.lang.ClassLoader 的实例。每个Class对象都被这些 ClassLoader 对象所加载,通过继承java.lang.ClassLoader 可以扩展出自定义 ClassLoader,并使用这些自定义的 ClassLoader 对类进行加载。

1
2
3
4
5
6
7
8
9
10
package java.lang;
public abstract class ClassLoader {
public Class loadClass(String name);
protected Class defineClass(byte[] b);
public URL getResource(String name);
public Enumeration getResources(String name);
public ClassLoader getParent();
Class<?> findClass(String name)
//...
}

ClassLoader类有如下核心方法:

  1. loadClass(加载指定的Java类) 接收全类名字符串,在内存中加载为Class 对象,返回。
  2. findClass(查找指定的Java类) 查找当前JVM中,名称为name的Class类对象是否存在,找到的话返回Class类对象。
  3. findLoadedClass(查找JVM已经加载过的类)
  4. defineClass(定义一个Java类) 接受字节数组,在内存中加载为Class对象,返回。
  5. resolveClass(链接指定的Java类)
  6. getParent返回其parent ClassLoader

重点逻辑:

  • loadClass(String classname),参数为需要加载的全限定类名,该方法会先查看目标类是否已经被加载,查看父级加载器并递归调用loadClass(),如果都没找到则调用findClass()。

  • findClass(),搜索类的位置,一般会根据名称或位置加载.class字节码文件,获取字节码数组,然后调用defineClass()。

  • defineClass(),将字节码转换为 JVM 的 java.lang.Class 对象。

嵌套调用关系:loadClass->findClass->defineClass

整个demo玩玩看;

1
2
3
4
5
6
7
8
9
10
11
12
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader cl = Hello.class.getClassLoader();
System.out.println(cl);

ClassLoader clParent = cl.getParent();
System.out.println(clParent);

ClassLoader cl2 = clParent.getParent();
System.out.println(cl2);
}
}

输出:

image-20201105164854799

重点:loadClass()

源码:

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded 检查这个类是否被加载过
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果没有加载过,给父加载器加载
c = parent.loadClass(name, false);
} else {
// 交给Bootstrap ClassLoader加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 如果没找到的话,由本类来findClass
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;
}

它先使用了findLoadedClass(String)方法来检查这个类是否被加载过

接着使用父加载器调用loadClass(String)方法

之后就调用自身findClass(String) 方法装载类。

最后通过上述步骤找到了对应的Class对象,并且接收到的resolve参数的值为true,那么就调用resolveClass(Class)方法来链接(验证,准备,解析)。

forName方法和loadClass方法的区别

image-20210901204710336

image-20210901205017892

两种类加载:显式、隐式

显式加载[动态加载]:

  • Java反射CLass.forName("com.sec.Test") 加载、链接、初始化
  • 类加载器ClassLoader.loadClass("com.sec.Test"):仅加载

隐式加载[静态加载]:

  • 创建类对象 new
  • 使用类的静态域
  • 创建子类对象
  • 使用子类的静态域
  • 调用类的静态方法 Class.method()

Demo:

1
2
3
4
5
// 反射加载Test示例 显式加载
Class.forName("com.sec.Test");

// ClassLoader加载Test示例 隐式加载
this.getClass.getClassLoader().loadClass("com.sec.Test");

Class.forName("类名")默认会初始化被加载类的静态属性和方法

如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器)

ClassLoader.loadClass()只是将类加载进JVM虚拟机。

Class.forName()可以加载数组,而ClassLoader.loadClass() 不能。

子类引用父类的静态字段,不会触发子类的初始化。

自定义ClassLoader

Java 中提供的默认 ClassLoader 只加载指定目录下面的 jar 和 class ,我们从上面了解到 ClassLoader是一个抽象类,实现自定义的 ClassLoader 需要继承该类并实现里面的方法。

java.lang.ClassLoader是所有的类加载器的父类。

一般情况下,我们重写父类的 findClass 方法即可。

双亲委派机制是loadClass()函数负责的。 可以打破但是没必要。

*一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。 *

ClassLoader 方法那么多为什么只重写 findClass 方法? 因为 JDK 已经在 loadClass 方法中帮我们实现了 ClassLoader 搜索类的算法,当在 loadClass 方法中搜索不到类时,loadClass 方法就会调用findClass 方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写 loadClass 搜索类的算法。

步骤:

  1. 编写一个类继承自ClassLoader抽象类。
  2. 覆盖它的findClass()方法。
  3. findClass()方法中调用defineClass()

自定义 ClassLoader DEMO

假如我们自定义一个 classloader,我们可以编写一个测试类来说明。在当前目录下面新建一个 Hello 类。里面有个方法 sayHello,然后放入到指定目录下面,如:我当前的目录为:

1
2
3
4
5
public class Hello {
public void sayHello(){
System.out.println("Hello! ----------> DIYClassLoader");
}
}

接着我们需要自定义一个 ClassLoader 来继承系统的 ClassLoader,命名为 DIYClassLoader 类。

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
public class DIYClassLoader extends ClassLoader{
private String mylibPath;
public DIYClassLoader(String path) {
mylibPath = path;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getFileName(name);
File file = new File(mylibPath,fileName);
try{
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while((len = is.read())!= -1){
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
}catch (Exception e){
e.printStackTrace();
}
return super.findClass(name);
}

//获取要加载 的class文件名
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index)+".class";
}
}
}

现在需要的是写一个调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
DIYClassLoader diyClassLoader = new DIYClassLoader("/path");
try {
Class<?> c = diyClassLoader.loadClass("class.name");
if (c != null) {
try {
Object obj = c.newInstance();
Method method = c.getDeclaredMethod("sayHello", null);
//通过反射调用Hello类的sayHello方法
method.invoke(obj, null);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

最终实现方法调用:

image-20201106092414009

重要方法 loadClass

直接看源码:

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
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) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 如果该类未被加载,并且父类加载器也没有找到,则调用findclass
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()
resolveClass(c);
}
return c;
}
}

上面是方法原型,一般实现这个方法的步骤是

  1. 执行findLoadedClass(String)去检测这个class是不是已经加载过了,已经加载过的都应该在缓存中。
  2. 执行父加载器的loadClass方法。如果父加载器为空,则Bootstrap ClassLoader加载器去加载。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。
  3. 如果向上委托父加载器没有加载成功,则通过findClass(String)查找。

如果class在上面的步骤中找到了,参数resolve又是true的话(上文提到的==resolveClass==,resolve参数就是表示需不需要连接阶段),那么loadClass()又会调用resolveClass(Class)这个方法去链接,来生成最终的Class对象。

类加载主要的三个阶段

  1. 加载

  2. 链接

    • 验证:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。
    • 准备:为类变量分配内存,“预分配内存”,并且赋予初值0。
    • 解析:将常量池中的符号引用转换为直接引用(内存块),替换为具体的内存地址或偏移量。
  3. 初始化:只给static修饰的变量或者语句赋值,执行静态代码块。

    如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

    如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

URLClassLoader

在java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它继承了ClassLoader,能够从本地或者网络上指定的位置加载类,我们可以使用该类作为自定义的类加载器使用。

URLClassLoader是ClassLoader的子类,它用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源。也就是说,通过URLClassLoader就可以加载指定jar中的class到内存中。
下面来看一个例子,在该例子中,我们要完成的工作是利用URLClassLoader加载jar并运行其中的类的某个方法。

加载过程中不会执行静态代码块。

构造方法:

public URLClassLoader(URL[] urls):指定要加载的类的所在地URL地址,父类加载器默认为App系统类加载器

public URLClassLoader(URL[] urls,ClassLoader parent):指定要加载的类的所在的URL地址,并指定父类加载器。

1
2
3
4
File file = new File(jar文件全路径); 
URL url = file.toURL();
URLClassLoader loader = new URLClassLoader(new URL[] { url });
Class tidyClazz = loader.loadClass(所需class的含包名的全名);

image-20201106103945821

Java 运行时类加载

ClassLoader.loadClass()与Class.forName()是反射用来构造类,给一个类名即可,返回值是Class。

注意一点,C lass.forName()方法全套服务,会执行目标class的static代码块方法。

loadClass 应该在URLClassLoader里面用的多,这个涉及到动态加载jar包。

获取当前ClassLoader的四种方式:

1
2
3
4
5
6
7
8
// 方式一:获取当前类的 ClassLoader
clazz.getClassLoader()
// 方式二:获取当前线程上下文的 ClassLoader
Thread.currentThread().getContextClassLoader()
// 方式三:获取系统的 ClassLoader
ClassLoader.getSystemClassLoader()
// 方式四:获取调用者的 ClassLoader
DriverManager.getCallerClassLoader()

通过反射获取defineClass方法

1
ClassLoader.defineClass(buye[] b);
1
2
3
1. java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
2. defineClassMethod.setAccessible(true);
3. Class cc = (Class) defineClassMethod.invoke(new ClassLoader(){}, classBytes, 0, classBytes.length);

通过反射结合Thread

1
2
3
1. ...
2. ...
3. Class cc = (Class) defineClassMethod.invoke(Thread.currentThread().getContextClassLoader(), classBytes, 0, classBytes.length);

构造函数

类加载完成后,对象实例化是Class.newInstance()方法。该方法会执行无参构造函数,

凡是能触发构造函数,都能触发静态代码块。