Java类加载过程
概念
Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。
Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。
类加载器分类
引导类加载器(BootstrapClassLoader)
引导类加载器(BootstrapClassLoader)属于JVM的一部分,不继承java.lang.ClassLoader
类,也没有父加载器,主要负责加载核心Java库,即在/jre/lib/rt.jar
目录当中的类。
该类加载扩展类加载器和应用类加载器,并作为它们的父类加载器
处于安全考虑,引导类加载器只加载包名为:java、javax、sun开头的类
扩展类加载器(ExtensionsClassLoader)
扩展类加载器(ExtensionsClassLoader),用来加载/jre/lib/ext
目录下的类,或者是在java.ext.dirs
中指明的目录
由sun.misc.Launcher$ExtClassLoader
方法实现,父类加载器为引导类加载器
应用类加载器(AppClassLoader)
应用类加载器(AppClassLoader),用来加载java.class.path
指定目录或者classpath
路径下的类
由sun.misc.Launcher$AppClassLoader
方法实现,父类加载器为扩展类加载器
自定义类加载器(UserDefineClassLoader)
自定义类加载器(UserDefineClassLoader),通过继承java.lang.ClassLoader
类重写findClass()
方法来实现自定义类加载器
类加载机制-双亲委派
JVM对class文件采用是按需加载方式,当需要使用该类的时候,JVM才会将class文件加载到内存中产生class对象
在加载类的时候,采用双亲委派机制
工作原理
- 当一个类加载器接收到了类加载的请求,会把这个请求委托给父类加载器去执行
- 如果父类还存在父类加载器,则继续向上委托,一直到引导加载器:
BootstrapClassLoader
- 如果父类加载器可以完成加载任务,则返回成功结果,否则就由子类去加载,如果子类加载失败就会抛出
ClassNotFoundException
异常
优点
自底向上检查类是否加载、自顶向下加载类
避免了同一个类被多次加载,避免有重复的字节码出现,保证了Java程序安全稳定运行
ClassLoader类核心方法
findClass:查找指定的Java类
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
findLoadedClass:查找JVM是否已加载该类
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
resolveClass:链接指定Java类
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
private native void resolveClass0(Class<?> c);
loadClass:加载指定的Java类
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 {
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();
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;
}
}
在loadClass方法中,大概的流程如下:
- 使用
findLoadedClass
方法来检查该类是否已被加载 - 若未被加载,接着使用
parent.loadClass
父类加载器进行加载,直到使用引导类加载器BootstrapClassLoader进行加载 - 之后调用
findClass
方法进行查找该类并加载 - 成功装载后,就会调用
resolveClass
方法去链接该类
defineClass:定义一个Java类
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
将字节码解析成JVM识别的Class对象
自定义类加载器定义过程
自定义类加载器可以让我们可以调用本地磁盘文件或者通过网络远程加载类
为了符合双亲委派规范,所以我们一般不去重写loadClass方法,而是去修改findClass方法,所以自定义类加载器需要有以下三个步骤:
- 继承ClassLoader类
- 重写findClass方法
- 在findClass方法中调用defineClass方法来定义一个类
示例
先来定义一个自定义类Test.java
package com.MyClassLoader;
public class Test {
public static void main(String[] args) {
System.out.println("This experiment test is successful");
}
}
先将该自定义类编译为Class字节码文件
然后我们编写一个异或加密类Encryption.java
,执行该类对Test.class
字节码进行异或加密
package com.MyClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Encryption {
public static void main(String[] args) {
encode(new File("./src/main/java/com/MyClassLoader/Test.class"), new File("./src/main/java/com/MyClassLoader/Test.class"));
}
public static void encode(File src, File dest) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(src);
fos = new FileOutputStream(dest);
// 取反加密
int temp = -1;
while ((temp = fis.read()) != -1) {
fos.write(temp ^ 0xff);
}
} catch (IOException e) {
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("This experiment test is successful");
}
}
然后通过自定义类加载器Decryption.java
来加载这个被加密的类
package com.MyClassLoader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class Dectyption extends ClassLoader {
public byte[] getClassData(String className) {
String path = "./src/main/java/" + className.replace('.', '/') + ".class";
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
is = new FileInputStream(path);
int temp;
while ((temp = is.read()) != -1) {
baos.write(temp ^ 0xff);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (baos != null) {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Override
protected Class<?> findClass(String name) {
Class<?> c = findLoadedClass(name);
if (c == null) {
byte[] classData = getClassData(name);
System.out.println(name);
c = defineClass(name, classData, 0, classData.length);
}
return c;
}
}
然后我们去使用自定义类去加载Test.class
package com.MyClassLoader;
public class TestLoader {
public static void main(String[] args) throws ClassNotFoundException{
Dectyption dectyption = new Dectyption();
Class<?> clazz = dectyption.loadClass("com.MyClassLoader.Test");
System.out.println(clazz);
}
}
应用场景
代码保护
通过刚才的自定义类加载器示例就能发现,我们可以对自定义的类进行加密,然后再通过自定义解密类加载器去加载该类,实现了对代码的保护,并且还可以构造恶意类绕过服务器对文件内容的检测。
资源隔离
通过类加载器,我们可以去加载指定的类,故可以实现不同项目或同一项目上不同版本的jar包隔离,避免集群错误或者产生冲突。
热部署
热部署是在不重启Java虚拟机的前提下,能自动侦测到class文件的变化,更新正在运行的Class对象行为。
Java类在被ClassLoader加载后,会产生相应的Class对象,默认的JVM只会在启动时加载类,如果后期有一个类更新,只会替换class文件,不会更新正在运行的Class对象
所以改变了ClassLoader的加载行为,使得JVM能够监听class文件的更新并重写加载class文件
热部署步骤:
- 销毁自定义ClassLoader(被该加载器加载的class也会自动卸载)
- 更新class文件
- 使用新的ClassLoader去加载class文件
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法