- 字节码文件是如何被加载的?以及加载时机
- java中的已有的类加载器
- 双亲委派机制
- 通过自定义类加载器加载磁盘的字节码文件实现热修复功能
前言
之前介绍了Java字节码文件(.class)的格式。
- 一个完整的Java程序由多个.class文件组成,在程序运行过程中,需要将这些.class文件加载到JVM中才可以使用。
- 而负责加载这些.class文件的就是类加载器-ClassLoader
Java中的类何时被加载器加载
- 在Java程序启动的时候,并不会一次性加载程序中所有的.class文件,而是在程序运行过程中,动态地加载相应的类到内存中(方法区)
- 通常情况下Java程序中的.class文件会在以下2种情况被ClassLoader主动加载到内存中
- 调用类构造器
- 调用类中的静态(static)变量或者静态方法
Java中的ClassLoader
- JVM中自带3个类加载器
- 启动类加载器 BootstrapClassLoader
- 扩展类加载器 ExtClassLoader
- 系统加载器 APPClassLoader
系统加载器 APPClassLoader
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
- AppClassLoader主要加载系统属性“java.class.path”配置下类文件,也就是环境变量CLASS_PATH配置的路径
- 因此AppClassLoader是面向用户的类加载器,我们编写的代码以及使用的第三方jar包通常都是由他来加载的
扩展类加载器 ExtClassLoader
private static Launcher.ExtClassLoader createExtClassLoader() throws IOException {
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Launcher.ExtClassLoader run() throws IOException {
File[] var1 = Launcher.ExtClassLoader.getExtDirs();
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
MetaIndex.registerDirectory(var1[var3]);
}
return new Launcher.ExtClassLoader(var1);
}
});
} catch (PrivilegedActionException var1) {
throw (IOException)var1.getException();
}
}
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
...
return var1;
}
- ExtClassLoader主要加载系统属性"java.ext.dirs"配置下类文件。
启动类加载器 BootstrapClassLoader
- BootstrapClassLoader不是用Java代码实现的,而是由C/C++语言编写的,本身属于虚拟机的一部分。
- 其加载系统属性 “sun.boot.class.path”配置下类文件。
代码实现:
public static void main(String[] args) {
System.out.println(System.getProperty("java.class.path"));
System.out.println("------------------------");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("------------------------");
System.out.println(System.getProperty("sun.boot.class.path"));
}
双亲委派模式
问题:既然JVM中已经有了3中ClassLoader,那么我们编写的.class文件到底该使用哪一个类加载器去加载呢?
答案是:双亲委派模式
-
双亲委派模式就是,当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载。
- 也就是说,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程
class类的加载方法在ClassLoader的loadClass方法中实现
public abstract class ClassLoader {
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void var1, ClassLoader parent) {
this.classes = new Vector();
this.defaultDomain = new ProtectionDomain(new CodeSource((URL)null, (Certificate[])null), (PermissionCollection)null, this, (Principal[])null);
this.packages = new HashMap();
this.nativeLibraries = new Vector();
this.defaultAssertionStatus = false;
this.packageAssertionStatus = null;
this.classAssertionStatus = null;
this.parent = parent;
if (ClassLoader.ParallelLoaders.isRegistered(this.getClass())) {
this.parallelLockMap = new ConcurrentHashMap();
this.package2certs = new ConcurrentHashMap();
this.domains = Collections.synchronizedSet(new HashSet());
this.assertionLock = new Object();
} else {
this.parallelLockMap = null;
this.package2certs = new Hashtable();
this.domains = new HashSet();
this.assertionLock = this;
}
}
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class> loadClass(String name, boolean resolve) {
synchronized(getClassLoadingLock(name)) {
Class c = findLoadedClass(name);
if (c == null) {
long var5 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long var7 = System.nanoTime();
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
-
代码解析
- findLoadedClass()方法判断该Class是否已加载,如果已加载,则直接将该Class返回
- 如果该Class没有被加载过,则判断parent是否为空,如果不为空则将类加载的任务委托给parent,调用parent的loadClass()方法
- parent也是ClassLoader类型,作为构造函数入参传入
- 如果parent == null,则直接调用BootstrapClassLoader加载该类
- 如果parent或者BootstrapClassLoader都没有加载成功,则调用当前ClassLoader的findClass方法继续尝试加载
-
parent属性
- 可以看出,在每一个ClassLoader中都有一个ClassLoader类型的parent引用,并且在构造器中传入值。
- 从源码中可知,AppClassLoader传入的parent是ExtClassLoader,而ExtClassLoader并没有传入任何parent,也就是null
举例
执行下面的代码,_2ClassLoader类是如何加载的呢?
_2ClassLoader demo = new _2ClassLoader();
-
默认情况下,JVM首先使用AppClassLoader去加载_2ClassLoader类
- AppClassLoader将加载任务委派给它的父类加载器ExtClassLoader
- ExtClassLoader的parent为null,所以直接将加载任务委派给BootstrapClassLoader
- BootstrapClassLoader在jdk/lib目录下无法找到Test类,因此返回的Class为null
- 因为parent和BootstrapClassLoader都没有成功加载_2ClassLoader类,所有AppClassLoader会调用自身的findClass()方法来加载 _2ClassLoader类
最终_2ClassLoader类是被AppClassLoader加载到内存中的,可通过如下代码验证:
private static void test3() {
ClassLoader classLoader = _2ClassLoader.class.getClassLoader();
System.out.println("classLoader is "+classLoader);
ClassLoader parent = classLoader.getParent();
System.out.println("parent is "+parent);
ClassLoader bootstrp = parent.getParent();
System.out.println("bootstrp is "+bootstrp);
}
打印结果:
classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
parent is sun.misc.Launcher$ExtClassLoader@42a57993
bootstrp is null
注意:双亲委派 机制只是java推荐的机制,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。
- 如果想保持双亲委派模式,就应该重写findClass(name)方法
- 如果想破坏双亲委派模型,可以重写loadClass(name)方法
自定义ClassLoader
- 加入我们要加载在磁盘上的一个.class文件(该文件可以是网络下载下来的)
public class ClassTest {
public void printTest(){
System.out.println("ClassTest -- print 111");
}
}
- 自定义ClassLoader从磁盘加载class文件,获取文件字节流,并加获取Class对象
public class DiskClassLoader extends ClassLoader {
private String filePath;
public DiskClassLoader(String path) {
this.filePath = path;
}
@Override
protected Class> findClass(String className) throws ClassNotFoundException {
String classPath = filePath + className + ".class";
byte[] classBytes = null;
try {
Path path = Paths.get(new URI(classPath));
classBytes = Files.readAllBytes(path);
} catch (URISyntaxException | IOException e) {
e.printStackTrace();
}
return defineClass(className, classBytes, 0, classBytes.length);
}
}
- 使用自定义的ClassLoader加载字节码文件,并反射生成类对象
public class ClassLoaderUse {
public static void main(String[] args) {
System.out.println("111");
String filePath = "E:/Study/";
DiskClassLoader diskClassLoader = new DiskClassLoader(filePath);
try {
Class> aClass = diskClassLoader.loadClass("ClassTest");
if (aClass != null) {
Object obj = aClass.newInstance();
Method method = aClass.getMethod("printTest", null);
method.invoke(obj, null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 动态加载.class文件的思路,经常被用作热修复和插件化开发的框架中,流程如下:
- 客户端从服务端下载一个加密的.class文件,然后再本地通过事先定义好的加密方式进行解密
- 再使用自定义ClassLoader动态加载解密后的.class文件,并动态调用相应的方法