classloader:
JVM是什么:类似于翻译器,将同一个java代码翻译给不同的操作系统,以达到通用的效果,如windows适配的JVM可以将java翻译使windows可以运行。
classloader 是什么:将.class加载到JVM中,为节约资源,提高速度和效率,JVM不会一次性将所有类文件加载到内存中,而是根据需要动态加载。
编译过程:我们写的.java源码不能直接运行,而是会经过编译在同目录下生成.class字节码文件再以这种可被JVM识别的形式拿给JVM翻译给操作系统运行。
深入类加载过程:
windows下的环境变量配置:
JAVA_HOME:告知系统JDK的安装位置
PATH:这个变量的作用是让你在命令行中可以直接使用某些命令,而不需要输入它们的完整路径。比如,运行 javac 和 java 命令时,如果没有将 JDK 中的 bin 目录添加到 PATH 中,就需要输入全路径,非常麻烦。
CLASSPAYH:这个变量是用来指向 .jar 包或 .class 文件所在路径的。JVM 在运行时会根据 CLASSPATH 找到你需要加载的类文件或依赖包。
注:CLASSPATH 的值通常以 .; 开头,其中 . 表示当前目录。如果省略了这部分,JVM 可能无法识别当前目录下的类文件。
三个系统类加载器:
Bootstrap ClassLoader(引导类加载器):这是Java中最顶层的类加载器,负责加载核心类库。它会加载 %JRE_HOME%lib 目录下的重要文件,例如 rt.jar、resources.jar、charsets.jar 等基础类库。
注:引导类加载器的路径是可以改的,可以添加新的默认加载路径
java -Xbootclasspath/a:path
// 这样会将指定的路径文件追加到默认的引导加载路径中。
Extension ClassLoader(扩展类加载器):扩展类加载器用于加载 %JRE_HOME%libext 目录下的 .jar 包和 .class 文件。除此之外,还可以通过 JVM 参数 -Djava.ext.dirs=路径 指定其他扩展加载目录。
Application ClassLoader(应用类加载器):也被称为 System ClassLoader,它是用户最常接触的类加载器,主要负责加载当前应用程序的类路径(classpath)中的所有类。用户自己编写的类和依赖库大多由它加载。
类加载加载顺序:
Bootstrap CLassloder->Extention ClassLoader->AppClassLoader
先看sun.misc.Launcher,它是一个java虚拟机的入口应用。
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}
}
可以看出:
- Launcher初始化了ExtClassLoader和AppClassLoader。
- Launcher中并没有看见BootstrapClassLoader,但通过
System.getProperty("sun.boot.class.path")得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。 - extcl是一个ExtClassLoader实例
ExtClassLoader工作流程:
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
......
}
简要解释:创建一个从URLClassLoaderextends的类Extention ClassLoader,它的getExtDirs()方法将java.ext.dirs目录字符串转化为一个个文件对象,然后集合为一个文件数组,再交给getExtClassLoader()方法处理,这个方法将文件数组转化为URL数组,交给ExtClassLoader构造方法处理,这样会调用它的父类,即URLClassLoader的构造方法完成初始化。可以看到,java.ext.dirs就是它的加载路径。
AppClassLoader工作流程:
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
......
}
可以看出AppClassLoader加载的就是java.class.path下的路径。
每个类加载器都有一个父加载器:
每个类加载器都有一个父类加载器,AppClassLoader的父类加载器是ExtClassLoader,而ExtClassLoader的则是Bootstrap ClassLoader,可以通过getParent()方法得到父类加载器。不过要注意的是,父加载器不是父类。
先看getParent()方法的实现,这个方法在ClassLoader.java中:
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
// @GuardedBy("ClassLoader.class")
private static ClassLoader scl;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
...
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
if (parent == null)
return null;
return parent;
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
return scl;
}
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
//通过Launcher获取ClassLoader
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
}
可以看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个情况: 1. 由外部类创建ClassLoader时直接指定一个ClassLoader为parent。 2. 由getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
然后看代码:
// 重点是这一段代码
ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
loader = AppClassLoader.getAppClassLoader(extcl);
可以看出extcl是一个ExtClassLoader实例,然后会传给getAppClassLoader(),可知AppClassLoader的父加载器就是ExtClassLoader
Bootstrap ClassLoader:
不同于其他用java实现的加载器,它是一个用C++实现的特殊加载器,属于java虚拟机的一部分。它没有父加载器但是可以成为其他的加载器的父加载器。比如ExtClassLoader。这也可以解释通过ExtClassLoader的getParent方法获取为Null的现象。
双亲委托:
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
这个机制分为两部分,第一部分是向上委托的过程:
- 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
- 递归,重复第1步骤的操作。
- 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存。如果有的话则返回,没有的话,则开始第二部分过程。
如果上诉还是没有找到对应的类,则开始第二部分的过程:
- 如果Bootstrap ClassLoader缓存没有找到的话,就去找自己的规定的路径下,也就是
sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。 - Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在
java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。 - ExtClassLoader查找不成功,AppClassLoader就自己查找,在
java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
几个重要方法:
上面已经详细介绍了加载过程,但具体为什么是这样加载,我们还需要了解几个个重要的方法loadClass()、findLoadedClass()、findClass()、defineClass()。
loadclass方法原型:
protected Class<?> loadClass(String name,
boolean resolve)
throws ClassNotFoundException
方法的执行步骤:
- 执行
findLoadedClass(String)去检测这个class是不是已经加载过了。 - 执行父加载器的
loadClass方法。如果父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。 - 如果向上委托父加载器没有加载成功,则通过
findClass(String)查找。 - findClass根据类名,去特定的位置(文件系统、网络、数据库、加密空间)找到类的二进制字节数组 (
byte[])。 - 拿到字节数组后,调用
defineClass()将其转换为Class对象。
流程图:
sequenceDiagram
participant User as 用户代码
participant Load as loadClass()
participant Cache as findLoadedClass()
participant Parent as 父加载器
participant Find as findClass() (重写)
participant Define as defineClass() (JVM)
User->>Load: loadClass("com.example.Secret")
Load->>Cache: 检查缓存 ("Secret" 加载过吗?)
alt 已加载
Cache-->>Load: 返回 Class 对象
Load-->>User: 返回 Class 对象
else 未加载
Cache-->>Load: 返回 null
Load->>Parent: 委托父级加载
alt 父级找到
Parent-->>Load: 返回 Class 对象
Load-->>User: 返回 Class 对象
else 父级找不到
Parent-->>Load: 抛出异常/返回 null
Note over Load, Find: 父级不行,自己上!
Load->>Find: 调用 findClass("Secret")
Note right of Find: 1. 读取加密文件<br/>2. 异或解密<br/>3. 得到 byte[]
Find->>Define: defineClass(name, byte[], ...)
Note right of Define: JVM 验证字节码<br/>生成 Class 对象
Define-->>Find: 返回 Class 对象
Find-->>Load: 返回 Class 对象
Load-->>User: 返回 Class 对象
end
end
如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()又会调用resolveClass(Class)这个方法来生成最终的Class对象。 我们可以从源代码看出这个步骤。
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.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
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;
}
}
代码解释了双亲委托。
自定义ClassLoader:
步骤:
- 编写一个类继承自ClassLoader抽象类。
- 复写它的
findClass()方法。 - 在
findClass()方法中调用defineClass()。
注意:一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
例:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class DiskClassLoader extends ClassLoader {
private String mLibPath;
public DiskClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = getFileName(name);
File file = new File(mLibPath,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 (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".class";
}else{
return name.substring(index+1)+".class";
}
}
}
注意:在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。
基于自定义的一些花活:
编写加密工具:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileUtils {
public static void test(String path){
File file = new File(path);
try {
FileInputStream fis = new FileInputStream(file);
FileOutputStream fos = new FileOutputStream(path+"en");
int b = 0;
int b1 = 0;
try {
while((b = fis.read()) != -1){
//每一个byte异或一个数字2
fos.write(b ^ 2);
}
fos.close();
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
然后自定义的解密类加载器:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
public class DeClassLoader extends ClassLoader {
private String mLibPath;
public DeClassLoader(String path) {
// TODO Auto-generated constructor stub
mLibPath = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
String fileName = getFileName(name);
File file = new File(mLibPath,fileName);
try {
FileInputStream is = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
byte b = 0;
try {
while ((len = is.read()) != -1) {
//将数据异或一个数字2进行解密
b = (byte) (len ^ 2);
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
byte[] data = bos.toByteArray();
is.close();
bos.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return super.findClass(name);
}
//获取要加载 的class文件名
private String getFileName(String name) {
// TODO Auto-generated method stub
int index = name.lastIndexOf('.');
if(index == -1){
return name+".classen";
}else{
return name.substring(index+1)+".classen";
}
}
}
Context ClassLoader 线程上下文类加载器:
ContextClassLoader其实只是一个概念。
源码:
public class Thread implements Runnable {
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
}
可以看到它只是代表一个classloader的变量而已。
每个Thread(线程)都有一个相关联的ClassLoader,默认是AppClassLoader。并且子线程默认使用父线程的ClassLoader除非子线程特别设置。
用途:让父加载器能够用线程设定的子加载器权限访问父加载器找不到的子加载器资源,解决双亲委派模式下只能向上递归委托的缺陷
例子:JDK 核心代码(父加载器)需要加载你项目里的 MySQL 驱动(子加载器资源)
// 1. JDK 源码 (java.sql.DriverManager)
// 它自己找不到 mysql 驱动,于是“借用”当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 成功加载了位于 classpath 下的 com.mysql.cj.jdbc.Driver
Class<?> driver = cl.loadClass("com.mysql.cj.jdbc.Driver");
// 2. 你的代码 (main 方法)
// 主线程默认就把 AppClassLoader 设为了上下文类加载器
// 所以上面那行代码能成功运行,无需你额外配置
DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "pass");