JVM-双亲委派机制

1.双亲委派描述

双亲:

  • JVM自带的加载器(在JVM的内部所包含,c++语言编写)。
  • 用户自定义加载器(独立与JVM之外的加载器,java编写)。

加载器描述:

  • JVM自带加载器
    • 根加载器(Bootstrap):加载jdk\jre\lib\rt.jar(包含了平时编写代码大部分的API);可以指定加载某个jar(-Xbootclasspath = xx.jar)。
    • 扩展类加载器(Extention):加载jdk\jre\lib\ext*.jar(包含了jdk中的扩展jar包);可以指定加载(-Djava.ext.dirs = xx.jar)。
    • 系统加载器/应用加载器(System/App):加载classpath(自己写的类);可以指定加载(-Djava.class.path = 类/jar)。
  • 自定义加载器
    • 都是该抽象类(java.lang.ClassLoader)的子类

委派:

  • 当一个加载器要加载类的时候,若自己加载不了。
  • 就逐层向上交由双亲去加载,当双亲的某个加载器加载成功后,再向下返回成功。
  • 如果所有的双亲和自己都无法加载,则会抛出异常。
    在这里插入图片描述

(1)bootstrap根加载器案例:

public class Main {
    public static void main(String[] args) throws Exception {
        // Object是rt.jar包中的类
        Class obj = Class.forName("java.lang.Object");
        ClassLoader bootstrap = obj.getClassLoader();
        // 打印出来是null,所以使用了根加载器
        System.out.println(bootstrap);
    }
}

打印出来是null,点进去(getClassLoader)看源码注解如下:

/**
  * Returns the class loader for the class.  Some implementations may use
  * null to represent the bootstrap class loader. This method will return
  * null in such implementations if this class was loaded by the bootstrap
  * class loader.
  * /

翻译过来:
返回该类的类加载器。一些实现可能使用null表示bootstrap类加载器。如果此类由bootstrap加载,则在此类实现中此方法将返回 null 。

就是用返回null表示使用了bootstrap根加载器。

(2)app应用加载器案例:

public class Main {
    public static void main(String[] args) throws Exception {
        // 自定义类是在classpath中
        Class myClass = Class.forName("org.gxuwz.arithmatic.lanqiao.MyClass");
        ClassLoader app = myClass.getClassLoader();
        // 打印出来是"sun.misc.Launcher$AppClassLoader@18b4aac2",app加载器
        System.out.println(app);
    }
}
class MyClass {}

打印出来的是:
sun.misc.Launcher$AppClassLoader@18b4aac2

表示使用了AppClassLoader加载器。
注意:

  • 定义类加载:最终实际加载类加载器。
  • 初始化类加载:首次加载类的加载器。

为什么应用(App)加载器又叫系统(System)加载器:

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取系统加载器打印结果
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);

        // 获取应用加载器打印结果
        Class myClass = Class.forName("org.gxuwz.arithmatic.lanqiao.MyClass");
        ClassLoader app = myClass.getClassLoader();
        System.out.println(app);
    }
}
class MyClass {}

不管是系统加载器还是应用加载器打印,两次打印结果一致,都是AppClassLoader应用加载器:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2

从应用加载器开始逐层获取父加载器:

public class Main {
    public static void main(String[] args) throws Exception {
        // 获取应用加载器app
        Class myClass = Class.forName("org.gxuwz.arithmatic.lanqiao.MyClass");
        ClassLoader app = myClass.getClassLoader();
        System.out.println(app);
        
        // 获取应用加载器的父加载器 -> 扩展加载器Extention
        ClassLoader parent1 = app.getParent();
        System.out.println(parent1);

        // 获取扩展加载器的父加载器 -> 根加载器bootstrap
        ClassLoader parent2 = parent1.getParent();
        System.out.println(parent2);

        // 获取根加载器的父加载器 -> 无
        ClassLoader parent3 = parent2.getParent();
        System.out.println(parent3);
    }
}
class MyClass {}

打印结果如下:
应用加载器App -> Extention扩展加载器 -> 根加载器Bootstrap -> 无
在这里插入图片描述

(3) 查看类加载器源码

(1)类加载器根据二进制名称加载(binary names):

// 正常的包类名
“java.lang.String”
// $表示匿名内部类:JSpinner包下的匿名内部类DefaultEditor
“javax.swing.JSpinner$DefaultEditor”
// FileBuilder内部类中的第一个匿名内部类
“java.security.KeyStoreBuilderBuilderFileBuilder$1// URLClassLoader包中的第三个内部类的第一个内部类
“java.net.URLClassLoader$3$1

(2)数组的加载器类型

  • 数组的加载器类型和数组元素加载器类型相同。
  • 原生类型(不是类,如int、long…)的数组是没有加载器。

类加载器数组描述源码文档:

/**
 * <p> <tt>Class</tt> objects for array classes are not created by class
 * loaders, but are created automatically as required by the Java runtime.
 * The class loader for an array class, as returned by {@link
 * Class#getClassLoader()} is the same as the class loader for its element
 * type; if the element type is a primitive type, then the array class has no
 * class loader.
 * /

大概意思:
返回的数组类的类加载器与其元素类型的类加载器相同;如果元素类型是原始类型,则数组类没有类加载器。

用一个例子来验证一下:

public class Main {
    public static void main(String[] args) throws Exception {
        MyClass[] cls = new MyClass[1];
        MyClass cl = new MyClass();
        // 数组加载器
        System.out.println(cls.getClass().getClassLoader());
        // 数组元素加载器
        System.out.println(cl.getClass().getClassLoader());

        int[] arr = new int[1];
        // 原始类型数组加载器
        System.out.println(arr.getClass().getClassLoader());
        // 原始类型数组元素加载器
        System.out.println(int.class.getClassLoader());
    }
}
class MyClass {}
  • 因为原生类型时基本数据类型,不是一个类,所以类加载器不能加载。
  • 如果为null,有两种可能,一种是没有加载器,另一种是加载类型为根加载器。

输出结果:
在这里插入图片描述
(3)xx.class类文件可能存在本地,也可能来自网络network或者在运行时动态产生。

/**
 * <p> However, some classes may not originate from a file; they may originate
 * from other sources, such as the network, or they could be constructed by an
 * application.
 * /

(4)如果类文件来自与网络,则需要继承ClassLoader父类,并重写findClass()和loadClassData()两个方法。

/**
 * <p> The network class loader subclass must define the methods {@link
 * #findClass <tt>findClass</tt>} and <tt>loadClassData</tt> to load a class
 * from the network.  Once it has downloaded the bytes that make up the class,
 * it should use the method {@link #defineClass <tt>defineClass</tt>} to
 * create a class instance.  A sample implementation is:
 *
 * <blockquote><pre>
 *     class NetworkClassLoader extends ClassLoader {
 *         String host;
 *         int port;
 *
 *         public Class findClass(String name) {
 *             byte[] b = loadClassData(name);
 *             return defineClass(name, b, 0, b.length);
 *         }
 *
 *         private byte[] loadClassData(String name) {
 *             // load the class data from the connection
 *             &nbsp;.&nbsp;.&nbsp;.
 *         }
 *     }
 * </pre></blockquote>
 * /

(4)自定义类加载器的实现

需要继承ClassLoader类并重写findClass()和loadClassData()两个方法,findClass()调用loadClassData()方法。

import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;

import java.io.File;
import java.io.FileInputStream;

public class Main1 extends ClassLoader {

    // 默认构造方法,使用根加载器getSystemClassLoader()
    public Main1() {
        super();
    }

    // 自定义使用的加载器
    public Main1(ClassLoader parent) {
        super(parent);
    }

    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) {
        name = splitDot("out.production.MyProject_02_arithmetic." + name) + ".class";
        FileInputStream inputStream = null;
        ByteOutputStream outputStream = null;
        byte[] res = null;
        try {
            // 文件输入流获取文件数据
            inputStream = new FileInputStream(new File(name));
            // 获取输出流
            outputStream = new ByteOutputStream();
            // 创建缓冲区
            byte[] buf = new byte[2];
            int len = -1;
            // 循环的将缓冲区输出到输出流中
            while ((len = inputStream.read(buf)) != -1) {
                outputStream.write(buf, 0, len);
            }
            // 返回输出流字节数组
            res =  outputStream.getBytes();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if (inputStream != null) inputStream.close();
                if (outputStream != null) outputStream.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
        return res;
    }

    // 将"."替换成"/"
    private String splitDot(String s) {
        return s.replace(".", "/");
    }

    public static void main(String[] args) throws Exception {
        Main1 main1 = new Main1();
        Class myClass = main1.loadClass("org.gxuwz.arithmatic.lanqiao.MyClass");
        System.out.println(myClass.getClassLoader());
        MyClass myClass1 = (MyClass)(myClass.newInstance());
        myClass1.hello();
    }
}
class MyClass{
    public void hello() {
        System.out.println("hello...");
    }
}

注意:

  • loadClassData(String name) 是文形式字符串a/b/MyClass.class,并且开头out.production…
  • findClass(String name) 是全类名形式a.b.MyClass,并且开头是:包名.类名.class。

以上代码打印出来的仍然是APP加载器:sun.misc.Launcher$AppClassLoader@18b4aac2,因为MyClass.class类文件在classpath中,所以使用的仍是应用加载器,需要将其移出到外部目录,如:D:/MyClass.class,这样就会触发自定义加载器如下,主要修改了path,且需要将classpath中的类文件删除:

import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;

import java.io.File;
import java.io.FileInputStream;

public class Main1 extends ClassLoader {

    private String path;

    // 默认构造方法,使用根加载器getSystemClassLoader()
    public Main1() {
        super();
    }

    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) {
        // 若自定义路径不为空,设置自定义路径名称
        if (path != null) {
            // 获取全路径中末尾的类名称
            name = path + name.substring(name.lastIndexOf(".") + 1) + ".class";
        } else {
            name = splitDot("out.production.MyProject_02_arithmetic." + name) + ".class";
        }
        FileInputStream inputStream = null;
        ByteOutputStream outputStream = null;
        byte[] res = null;
        try {
            // 文件输入流获取文件数据
            inputStream = new FileInputStream(new File(name));
            // 获取输出流
            outputStream = new ByteOutputStream();
            // 创建缓冲区
            byte[] buf = new byte[2];
            int len = -1;
            // 循环的将缓冲区输出到输出流中
            while ((len = inputStream.read(buf)) != -1) {
                outputStream.write(buf, 0, len);
            }
            // 返回输出流字节数组
            res =  outputStream.toByteArray();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if (inputStream != null) inputStream.close();
                if (outputStream != null) outputStream.close();
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
        return res;
    }

    // 将"."替换成"/"
    private String splitDot(String s) {
        return s.replace(".", "/");
    }

    public static void main(String[] args) throws Exception {
        Main1 main1 = new Main1();
        // 自定义类路径,D:/MyClass.class
        main1.path = "D:/";
        Class<?> myClass = main1.loadClass("org.gxuwz.arithmatic.lanqiao.MyClass");
        System.out.println(myClass.getClassLoader());
    }
}

输出自定加载器:org.gxuwz.arithmatic.lanqiao.Main1@45ee12a7

自定义加载器流程:

loadClass() -> findClass() -> loadClassData()

  • 在启动类中通过自定义加载器调用loadClass()。
  • loadClass()再调用自定义方法findClass()。
  • findClass()再调用loadClassData()。

加载器结论:

  • 类加载器只会把同一个类加载一次(位置也相同)。
  • 先委托AppClassLoader加载,AppClassLoader会在classpath路径中寻找是否存在,若存在则直接加载。
  • 否则才有可能(可能还会交给扩展和根加载器)交给自定义加载器加载。
  • 双亲委派体系中下层的加载器是引用上层parent加载器,各个加载器之间不是继承关系。

loadClass源码解析:

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 {
            	// 1.若父加载器不为空,则委托父加载器进行加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                	// 2.若父加载器为空,说明双亲委派调用了顶层加载器(根加载器)
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
			// 3.若为null,表示父加载器加载失败,只能由自己加载
            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;
    }
}

双亲委派机制的优势:

可以防止用户自定义的类和rt.jar中的类重名,而造成混乱。

如下代码是自定义一个java.langMath类(和jdk中的rt.jar重名):

package java.lang;

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

运行结果:
在这里插入图片描述

原因:
因为双亲委派机制,越顶层的加载器优先级越高,根加载器bootstrap是最顶层,优先级最高,则会被优先加载,所以加载的是rt.jar中的java.lang.Math类,而该类不是我们自定义的Math类,因此没有main方法,就会抛出异常。

双亲委派特点:

  • 若存在继承关系:继承的双方(父类、子类)都必须是同一个加载器,否则报错。
  • 如果不存在继承关系:子类加载器可以访问父类的加载器(自定义加载器可以访问App加载器),反之则不行(App加载器不能访问子类加载器)。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值