classloader 是干什么的
类加载器(classloader) 用于加载 Java 类到 Java 虚拟机中。
java 类使用方法。
.java 文件被编译为 .class 文件 字节码文件。
Classloader 读取字节码 转化为对应 实例。
java.lang.Class
将 Class 的字节码形式转化成内存形式的 Class 对象。
字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。
classloader 类型
Bootstarp ClassLoader(引导类加载器) 最顶层 利用getClassloader() 会返回 null(因为是利用 c++ 写的) 加载JDK中的核心类库
测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14package com.jiang;
import java.net.URL;
public class Bootstarp {
public static void main(String[] args) {
URL urls[] = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(int i=0 ; i < urls.length; i++){
System.out.println(urls[i].toExternalForm());
}
}
}Extension ClassLoader(扩展类加载器) 负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar
App ClassLoader (系统类加载器) 默认的类加载器 负责加载应用程序classpath目录下的所有jar和class文件
classloader 类方法
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)它接受一个全类名,然后返回一个Class类型的实例。
findClass
(查找指定的Java类)查找名称为
name
的类,返回的结果是java.lang.Class
类的实例。findLoadedClass
(查找JVM已经加载过的类)查找名称为
name
的已经被加载过的类,返回的结果是java.lang.Class
类的实例。defineClass
(定义一个Java类)接受一组字节,然后将其具体化为一个Class类型实例,它一般从磁盘上加载一个文件,然后将文件的字节传递给JVM,通过JVM(native 方法)对于Class的定义,将其具体化,实例化为一个Class类型实例。
resolveClass
(链接指定的Java类)
加载类的原理
classloader 的加载 使用的 是双亲委派机制
双亲委派机制
当一个ClassLoader
加载某个类的时候,会把这个类交给其父类加载器来加载,而整个过程是由上而下的检查的,也就是说首先由最顶层的Bootstarp ClassLoader
加载,如果没加载到,则再由Extension ClassLoader
来加载,如果还是没加载到,再交给App ClassLoader
来加载,如果还是没找到,则会返回到一开始发起这个委托请求的ClassLoader
,由这个加载器到指定的文件系统、网络等URL来获取要加载的类。如果还是没有获取到,则会抛出ClassNotFoundException
异常。
利用双亲委派。避免重复加载 保证安全
就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。利用 findLoadedClass
判断。如果已经加载直接返回
分析对应源码
sun.misc.Launcher
中的内部类,
这里首先将 ExtensionClassLoader
作为第一个参数参数 getAppClassloader
这里调用了 AppClassLoader
的构造方法 传输是 我们 getAppClassloader
传入的ExtensionClassLoader
因为我们的 AppClassloader
继承了 URLClassloader
这里的构造方法会调用 URLClassloader
的方法。
且 URLClassloader
继承 SecureClassLoader
SecureClassLoader
又是继承了 ClassLoader
且继承的 URLClassLoader
SecureClassLoader
都会调用 父类构造方法,
URLClassLoader
SecureClassLoader
ClassLoader
但是对于我们的 Extension ClassLoader
却没有对应的 parent
加载器实现 自定义加载器
对于一个加载器的实现
- 需要继承
ClassLoader
。 - 需要重写
findClass
方法,这个方法的规则告诉java 如何寻找要加载的类。 findClass
方法中调用defineClass
方法向 JVM 虚拟机中注册类。
写一个我们要加载的类
1 | package testjava; |
然后利用 javac 编译为 .class 文件
我们的加载器
MyclassLoader
1 | package testjava; |
主要调用函数
1 | package testjava; |
然后我们就可以调用实现
tips: 关于调用类下的 Hello.class 文件。我们给的路径应该为。
Path 路径/src/ 文件名为 包名.类名
入 testjava 包下的 Hello.class 文件
filepath = 文件src目录路径 className 为 testjava.Hello
加载类型
- 动态加载
- 利用 Classloader 加载字节码
- 利用URLClassLoader 远程加载字节码
- 利用Unsafe 加载字节码
- 利用Proxy 加载字节码
对于 Java 类的加载存在 显示加载 和 隐式加载
隐式加载 利用new 创建一个类实例。
显示加载 动态加载 反射 或者 Classloader 动态加载对象
动态加载
创建我们要加载的类
Student
1 | package test; |
测试加载代码
1 | package test; |
利用 Classloader 加载字节码
在我们自定义 classloader 的时候,我们发现。
会一次调用
loadClass
:根据类名使用双亲委派机制从父加载器中寻找类,如果没找到会调用findClass
findClass
:会根据URL所指定的方式来寻找字节码,然后调用defineClass
处理字节码defineClass
:把Java字节码处理成真正的Java类,并且在JVM中注册
获取到一个Java字节码文件之后,可以通过ClassLoader#defineClass
来把它变成真正的Java类。但是在defineClass
执行的时候并不会触发static代码块或者类的构造方法的,只有当显式调用其构造函数的时候才会被执行。因为ClassLoader#defineClass
的属性是protected
,所以无法直接在类外部访问,在实际情况中很少直接利用这种方式。
利用URLClassLoader远程加载字节码
在外面学习 双亲委派的时候 里面继承了一个 URLClassLoader
继承于ClassLoader
并且是App ClassLoader
的父类,它通过重写findClass
方法来实现了加载目录系统中的.class文件或者是远程服务器上的.class文件的功能。
AppClassLoader
继承 URLClassLoader
虽然 URLClassLoader
是 AppClassLoader
的父类
但是 URLClassLoader
的父类是 AppClassLoader
利用 URLClassLoader
可以加载 Jar包
文件系统
远程服务器
中的class
正常情况下,Java会根据配置项
sun.boot.class.path
和java.class.path
中列举的基础路径(这些路径是经过处理之后的java.net.URL
类)来寻找.class文件来加载,这种基础路径分为三种情况:
- URL未以
/
结尾,则认为是一个JAR文件,用JarLoader
来寻找类,即在Jar包中寻找.class文件- URL以
/
结尾,且协议名是file
,则用FileLoader
来寻找类,即在本地文件系统中寻找.class文件- URL以
/
结尾,且协议名不是file
,则使用最基础的Loader
来寻找类
加载远程 服务器上的 Class文件
这里可以使用 http
协议去访问
加载服务器 class 文件
利用 python 启动一个 web 环境
py3 -m http.server 8888
文件路径下有一个 Hello.class 文件
然后我们写一个我们的类来加载
1 | package URLClassLoader; |
使用Unsafe 加载字节码
Unsafe 就是不安全的函数
sun.misc.Unsafe
该类的 Unsafe#defineClass
可以直接向 JVM 中注册类。实现直接加载字节码的功能。
我们要使用 Unsafe#defineClass
这里我们需要使用 jdk 版本 java11 以下的 java11 中 Unsafe#defineClass 已经被移除
对应代码
1 | package unsafe; |
在Java8中这个方法的定义变成了public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
这个方法仅适用于Java8之前的版本 public native Class defineClass(String var1, byte[] var2, int var3, int var4);
使用Proxy来直接加载字节码
这里用的是 java.lang.reflect.Proxy
中的 defineClass0
的 native
实现代码
1 | package proxy; |
运行
还有一种是用 BCEL 来加载字节码