共计 3586 个字符,预计需要花费 9 分钟才能阅读完成。
Java 虚拟机中的类加载有三大步骤:,链接,初始化.其中加载是指查找字节流(也就是由 Java 编译器生成的 class 文件)并据此创建类的过程,这中间我们需要借助类加载器来查找字节流.
Java 虚拟机默认类加载器
Java 虚拟机提供了 3 种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、应用(Application)类加载器. 除了启动类加载器外,其他的类加载器都是 java.lang.ClassLoader 的子类.启动类加载器由 C ++ 语言实现,没有对应的 Java 对象,它负责将 <JAVA_HOME>/lib 路径下的核心类库或 -Xbootclasspath 参数指定的路径下的 jar 包加载到内存中.扩展类加载器是指 sun.misc.Launcher$ExtClassLoader 类,由 Java 语言实现,是 Launcher 的静态内部类,它负责加载 <JAVA_HOME>/lib/ext 目录下或者由系统变量 -Djava.ext.dir 指定位路径中的类库,他的父类加载器是 null.应用类加载器是指 sun.misc.Launcher$AppClassLoader 类,他负责加载应用程序路径下的类,这里路径指 java -classpath 或 -D java.class.path 指定的路径,他的父类加载器是扩展类加载器.
注意这里面的父子类加载器并不是继承的关系,只是 ClassLoader 类中的 parent 属性.我们来看 Launcher 类中创建扩展类加载器的代码:
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
这里设置了其父加载器为 null.
双亲委派机制
Java 虚拟机在加载类时默认采用的是双亲委派机制,即当一个类加载器接收到加载请求时,会将请求转发到父类加载器,如果父类加载器在路径下没有找到该类,才会交给子类加载器去加载.我们来看 ClassLoader 中 laodClass 方法:
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) {
long t1 = System.nanoTime();
// 到自己指定类加载路径下查找是否有 class 字节码
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;
}
}
通过这种层级我们可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子类加载器再加载一次。其次也考虑到安全因素,比如我们自己写一个 java.lang.String 的类,通过双亲委派机制传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字的类,发现该类已被加载,并不会重新加载我们新写的 java.lang.String,而直接返回已加载过的 String.class,这样保证生成的对象是同一种类型.
自定义类加载器
除了 jvm 自身提供的类加载器,我们还可以自定义类加载器,我们先写一个 Person 类
public class Person {
private int age;
private String name;
// 省略 getter/setter 方法
}
我们先看他是由哪个类加载器加载的.
public class TestJava {
public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(“person 是由 ” + person.getClass().getClassLoader() + “ 加载的 ”);
}
}
运行结果如下:
我们把 Person.class 放置在其他目录下
再运行会发生什么,在上面的 loadClass 方法中其实已经有了答案,会抛出 ClassNotFoundException,因为在指定路径下查找不到字节码.
我们现在写一个自定义的类加载器,让他能够去加载 person 类,很简单,我们只需要继承 ClassLoader 并重写 findClass 方法,这里面写查找字节码的逻辑.
public class PersonCustomClassLoader extends ClassLoader {
private String classPath;
public PersonCustomClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll(“\\.”, “/”);
FileInputStream fis = new FileInputStream(classPath + “/” + name
+ “.class”);
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
我们来测试一下:
public class TestJava {
public static void main(String[] args) throws Exception {
PersonCustomClassLoader classLoader = new PersonCustomClassLoader(“/home/shenxinjian”);
Class<?> pClass = classLoader.loadClass(“me.shenxinjian.algorithm.Person”);
System.out.println(“person 是由 ” + pClass.getClassLoader() + “ 类加载器加载的 ”);
}
}
测试结果如下:
编写自定义类加载器的意义
当 class 文件不在 classPath 路径下,如上面那种情况,默认系统类加载器无法找到该 class 文件,在这种情况下我们需要实现一个自定义的 classLoader 来加载特定路径下的 class 文件来生成 class 对象。
当一个 class 文件是通过网络传输并且可能会进行相应的加密操作时,需要先对 class 文件进行相应的解密后再加载到 JVM 内存中,这种情况下也需要编写自定义的 ClassLoader 并实现相应的逻辑