Java虚拟机:类加载器与双亲委派模型

时间:2022-12-27 12:56:53

学了挺久的java,也接触过java虚拟机,但是就是没有深入到源码中细细品读,也导致每次看到关于虚拟机的问题的时候都似曾相识却无从下手.这个系列就一点一点的品读java虚拟机.

类加载器

从java虚拟机规范描述来看,JVM支持两种类加载器:引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader).自定义类加载器派生于抽象类ClassLoader.

最常见的类加载器始终只有3个:
* Bootstrap ClassLoader 启动类加载器(引导加载器)
* ExtClassLoader
* AppClassLoader

对应来看,每种类加载器对应的目录为:

类加载器 目录 备注
Bootstrap ClassLoader JAVA_HOME/lib,或者由选项-Xbootclasspath指定 用C++编写,嵌在JVM内部
ExtClassLoader JAVA_HOME/ext Java编写 派生于ClassLoader
APPClassLoader ClassPath

测试代码:

public class Test {
public static void main(String[] args) {
//BootstrapClassLoader
ClassLoader classLoader = System.class.getClassLoader();
System.out.println(null != classLoader ? classLoader.getClass().getName(): "name:null");
//ExtClassLoader
System.out.println(CollationData_ar.class.getClassLoader().getClass().getName());
//AppClassLoader
System.out.println(Test.class.getClassLoader().getClass().getName());
}

}

输出结果:

name:null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader

以上测试的含义是:
1. 因为java.lang.System类包含在JAVA_HOME/lib目录中,测试由BootstrapClassLoader加载,由于启动类加载器本身由C++编写并嵌套在JVM内部,所以输出为null.
2. sun.text.resoutces.CollationData_ar类包含在JAVA_HOME/lib/ext扩展目录中,由ExtClassLoader加载.
3. 而我们的一般测试类在ClassPath目录中,因此由AppClassLoader加载.

双亲委派模型

java程序中重要的一点是要保证一个类的全局唯一性.当程序中出现多个全限定名相同的类时,类加载器始终只加载其中的某一个类而不会都加载.

查看ClassLoader类中loadClass的源码:

    protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//1.检查目标类之前是否已经被加载过了
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;
}
}

双亲委派模型的优点是使java类加载器具备优先级层级关系,越是基础的类越是被上层的类加载器加载,保证稳定运行.

**注意:**java虚拟机规范没有明确要求类加载器机制一定是双亲委派模型,只是建议.比如在Tomcat中,默认类加载器首先自己加载,失败时才给超类加载.

自定义类加载器

自定义类加载器的需求并不多.但是,当有一些特殊需求,比如,当一个字节码文件在编译的时候进行了加密处理,那么类加载器在加载的时候首先就要解密,否则会认为它不是标准的字节码文件.

自定义类加载器只需继承抽象类ClassLoader并重写ClassLoader.

注意:怎样查看当前的ClassPath目录?

System.getProperty("java.class.path");

准备工作

在F盘目录下创建Test1.java文件:

public Test1{
public static void main(String[] args){
System.out.println("test1");
}
}

并在此打开命令行,用javac Test1.java 命令将其编译为Test1.class 文件.

程序代码

public class MyClassLoader extends ClassLoader {
private String byteCode_Path;

public MyClassLoader(String byteCode_Path) {
this.byteCode_Path = byteCode_Path;
}

@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte value[] = null;
BufferedInputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(byteCode_Path + className + ".class"));
value = new byte[in.available()];//将字节码全部读取到数组里
in.read(value);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return defineClass(value, 0, value.length);//将byte数组转化为一个class对象实例
}

public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classLoader = new MyClassLoader("F:/");
System.out.println("加载目标类的类加载器->"+classLoader.loadClass("Test1").getClassLoader().getClass().getName());
System.out.println("当前类加载器的超类加载器:->"+classLoader.getParent().getClass().getName());

}
}

输出为:

加载目标类的类加载器->MyClassLoader
当前类加载器的超类加载器:->sun.misc.Launcher$AppClassLoader

参考书目:《Java虚拟机精讲》 高翔龙