JVM双亲委派机制
双亲委派是什么?
双亲:
- 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
* . . .
* }
* }
* </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加载器不能访问子类加载器)。