classloader类加载相关

java代码执行流程

image.pngimage.png

当 .java 源码被 javac.exe 编译器编译成 .class 字节码文件后,接下来的工作就交给JVM处理。JVM首先通过类加载器(ClassLoader),将class文件和相关Java API加载装入JVM,以供JVM后续处理。

类的加载过程

类的生命周期包括:加载、链接、初始化、使用、卸载

其中前三个属于类的加载过程

加载-loading

.class文件加载到JVM里的过程

  • 通过类的全限定名来获取定义此类的二进制字节流
  • 将此二进制字节流所代表的静态存储结构转化成方法区的运行时数据结构
  • 在内存中生成代表此类的java.lang.Class对象,作为该类访问入口.

链接-linking

链接分为三步

  • 验证:验证的目的是确保class文件的字节流中信息符合虚拟机的要求,不会危害虚拟机安全,使得虚拟机免受恶意代码的攻击,这一步至关重要。

    文件格式验证
    源数据验证
    字节码验证
    符号引用验证

  • 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。(仅包含类变量,不包含实例变量).

  • 解析:虚拟机将常量池中的符号引用替换为直接引用,解析动作主要针对类或接口,字段,类方法,方法类型等等。

初始化-initialization

在该阶段,才真正意义上的开始执行类中定义的java程序代码,该阶段会执行类构造器。

暂讨论关于类生命周期的三种,之后在学习内存的时候细嗦

类加载器ClassLoader

ClassLoader核心方法

ClassLoader的核心方法有:

  1. loadClass (加载指定的Java类)
  2. findClass (查找指定的Java类)
  3. findLoadedClass (查找JVM已经加载过的类)
  4. defineClass (定义一个Java类)
  5. resolveClass (链接指定的Java类)

ClassLoader的作用

Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang.ClassLoader加载类字节码,ClassLoader会调用JVM的native方法(defineClass0/1/2)来定义一个java.lang.Class实例。

另外一个作用是确认每个类应该由哪个类加载器加载。
第二个作用也用于判断JVM运行时的两个类是否相等,影响的判断方法有equals()、isAssignableFrom()、isInstance()以及instanceof关键字。

何时触发类加载

类加载的触发可以分为隐式加载和显示加载。

隐式加载

隐式加载包括以下几种情况:

  • 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时
  • 对类进行反射调用时
  • 当初始化一个类时,如果其父类还没有初始化,优先加载其父类并初始化
  • 虚拟机启动时,需指定一个包含main函数的主类,优先加载并初始化这个主类

显式加载

1
2
3
4
// 反射加载TestHelloWorld示例
Class.forName("top.longlone.TestHelloWorld");
// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("top.longlone.TestHelloWorld");

显示加载包含以下几种情况:

  • 通过ClassLoader的loadClass方法
  • 通过Class.forName(反射获取Class)
  • 通过ClassLoader的findClass方法

ClassLoader分类

image.png

从上到下认识他们

启动类/引导类:Bootstrap ClassLoader

这个类加载器使用C/C++语言实现的,嵌套在JVM内部,java程序无法直接操作这个类。它用来加载Java核心类库,如:JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路径下的包,用于提供jvm运行所需的包

并不是继承自java.lang.ClassLoader,它没有父类加载器

它加载扩展类加载器应用程序类加载器,并成为他们的父类加载器

出于安全考虑,启动类只加载包名为:java、javax、sun开头的类

扩展类加载器:Extension ClassLoader

Java语言编写,由sun.misc.Launcher$ExtClassLoader实现,我们可以用Java程序操作这个加载器派生继承自java.lang.ClassLoader,父类加载器为启动类加载器

从系统属性:java.ext.dirs目录中加载类库,或者从JDK安装目录:jre/lib/ext目录下加载类库。我们就可以将我们自己的包放在以上目录下,就会自动加载进来了。

应用程序类加载器:Application Classloader

程序默认的类加载器,我们编写的类就是由他来加载。

Java语言编写,由sun.misc.Launcher$AppClassLoader实现。
派生继承自java.lang.ClassLoader,父类加载器为ExtClassloader

它负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库

我们可以通过ClassLoader#getSystemClassLoader()获取并操作这个加载器

自定义加载器

为了实现自己的功能,比如加强安全传输,我们可以自己编写加载器。

继承java.lang.ClassLoader类,重写findClass()方法如果没有太复杂的需求,可以直接继承URLClassLoader类,重写loadClass方法,具体可参考AppClassLoaderExtClassLoader

获取ClassLoader

除了启动类加载器,其他加载器都是继承自java.lang.ClassLoader这个抽象类。

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

类加载流程

  1. 调用loadClass加载
  2. 调用findLoadedClass检查是否已加载,若已加载则直接返回已加载的类
  3. 如果创建ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))则使用父类加载器先加载,否则使用JVM的Bootstrap ClassLoader加载
  4. 若父类加载器无法加载则调用自身findClass加载
  5. 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false
  6. 加载失败或返回加载后的java.lang.Class类对象

类加载机制-双亲委派

jvm对class文件采用的是按需加载的方式,当需要使用该类时,jvm才会将它的class文件加载到内存中产生class对象。

在加载类的时候,是采用的双亲委派机制,即把请求交给父类处理的一种任务委派模式。

这也被叫做双亲委派模型,在jdk1.2之后引入

其中,两个用户自定义类加载器的父加载器是AppClassLoader,AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader是没有父类加载器的,在代码中,ExtClassLoader的父类加载器为null。BootstrapClassLoader也并没有子类,因为他完全由JVM实现。

image.png

工作逻辑

(1)如果一个类加载器接收到了类加载的请求,它自己不会先去加载,会把这个请求委托给父类加载器去执行。

(2)如果父类还存在父类加载器,则继续向上委托,一直委托到启动类加载器:Bootstrap ClassLoader

(3)如果父类加载器可以完成加载任务,就返回成功结果,如果父类加载失败,就由子类自己去尝试加载,如果子类加载失败就会抛出ClassNotFoundException异常,这就是双亲委派模式

使用目的

双亲委派模型能够保证类在内存中的唯一性,能够保证系统级别类的安全,因为当启动类ClassLoader加载过了之后,子ClassLoader便不会再加载:

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

运行原理

loadClass方法如下,代码中做了注释解析

双亲委派模型实现的核心就是这个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
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 {
// 如果父类加载器为null,说明ExtClassLoader也没有找到目标类,则调用BootstrapClassLoader来查找
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果都没有找到,调用findClass方法,尝试自己加载这个类
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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

我们可以实现自己的类加载器,加载指定路径下的class文件

  • 通过loadClass在指定的路径下查找文件。
  • 通过findClass方法解析class字节流,并实例化class对象。

编写一个自己的ClassLoader

defineClass:将字节码变成内存中的class

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
package com.xianbei.test3;

import java.io.*;

public class TestClassLoader extends ClassLoader{
private String classPath;

public TestClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classBytes = getData(name);
//defineClass来生成class实例
return defineClass(name,classBytes,0,classBytes.length);
} catch (IOException e) {
e.printStackTrace();
}

return super.findClass(name);
}

//该方法读取目标class文件的字节码并返回
private byte[] getData(String className) throws IOException {
String path = classPath+ File.separator + className.replace('.', File.separatorChar) + ".class";

try{
//读取字节
FileInputStream 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 (FileNotFoundException e) {
e.printStackTrace();
}

return null;
}
}

然后我们尝试加载自己电脑上指定路径的class文件:

这是我要加载的class文件,之前用JAVAssist控制字节码编写的恶意类

这里有个静态块,就是最后的static

在实例化的时候会被自动加载

关于JAVAssist不在此多做赘述

image.png

1
2
3
4
5
6
7
8
public class TestCLDemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
TestClassLoader myClassLoader = new TestClassLoader("C:\\Users\\Liyc\\IdeaProjects\\JavassistTest\\out");
Class<?> newStudent = myClassLoader.loadClass("NewStudent");
Constructor<?> c = newStudent.getDeclaredConstructor(String.class, int.class);
Object o = c.newInstance("Xianbei",19);
}
}

image.png

成功弹出计算机,也就是成功加载了指定路径下的class文件并且用反射将其实例化了。

URLClassLoader類代碼示例

URLClassLoader是ClassLoader的子类,它用于从指向 JAR 文件和目录的 URL 的搜索路径加载类和资源。也就是说,通过URLClassLoader就可以加载指定jar中的class到内存中。

https://vimsky.com/zh-tw/examples/detail/java-class-java.net.URLClassLoader.html

参考

https://xz.aliyun.com/t/9002

https://www.cnblogs.com/goloving/p/14438785.html讲双亲委派的

https://www.jianshu.com/p/fe8a01b0c3b7讲java代码运行机制的

https://zhuanlan.zhihu.com/p/67991761讲类加载的

https://www.cnblogs.com/xrq730/p/4847337.html很多种类加载器的写法

image.png