都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

时间:2022-09-11 01:01:50

文章目录

  • ​​二、类加载器​​
  • ​​2.1 类加载时机​​
  • ​​2.3 类加载器​​
  • ​​2.3.1 类加载器的种类​​
  • ​​2.3.2 双亲委派机制​​
  • ​​2.3.3 双亲委派的好处​​
  • ​​2.3.4 URLClassLoader类加载器​​
  • ​​1)加载本地磁盘上的类:​​
  • ​​2)加载网络上的类:​​
  • ​​2.3.5 自定义类加载器​​
  • ​​2.3.6 打破双亲委派​​
  • ​​2.2 类的加载过程​​
  • ​​2.2.1 类的生命周期​​
  • ​​1)加载​​
  • ​​2)连接​​
  • ​​3)初始化​​

二、类加载器

2.1 类加载时机

我们知道,所有的代码都是运行在内存中的,我们必须把类加载到内存中才能运行;在Java中,所有的Java类都是通过类加载器加载到内存进行执行的;

  • 一个类何时被加载?
  • 1)main方法所在的类总是被首先初始化
  • 2)创建该类对象时,首先会将内加载到内存(如果该类存在父类,那么首先加载父类到内存,创建父类的对象(super))
  • 3)访问该类的静态成员时,会将类加载到内存(该静态成员不能被fianl修饰)
  • 4)class.forName(“类的全包名”)
package com.dfbz.demo01;
import org.junit.Test;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01_类何时被加载 {
@Test
public void test1() throws ClassNotFoundException {
// new B(); // 先加载A再加载B
// Integer num = B.num; // 先加载A再加载B
Class<?> clazz = Class.forName("com.dfbz.demo01.B"); // 先加载A再加载B
}
}
class A {
public static Integer num = 10;
static {
System.out.println("A loader...");
}
}
class B extends A {
public static Integer num = 20;
static {
System.out.println("B loader...");
}
}

Tips:不管是用什么方法加载,类从始至终只会加载一次;

2.3 类加载器

2.3.1 类加载器的种类

  • 启动类加载器Bootstrap ClassLoader: 是嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负则加载JAVA_HOME/lib下的类库,启动类加载器无法被应用程序直接使用。
  • **扩展类加载器Extension ClassLoader:**该加载器器是用JAVA编写,且它的父加载器是Bootstrap,是由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext目录中的类库。开发者可以这几使用扩展类加载器。
  • **系统类加载器App ClassLoader:**系统类加载器,也称为应用程序类加载器,负责加载应用程序classpath目录下的所有jar和class文件(第三方jar)。它的父加载器为Ext ClassLoader。

Tips:这里的父加载器并非是Java中的继承关系,而是我们后面学习双亲委派过程中向上委派的加载器,我们将其称为父加载器;


测试类:

package com.dfbz.demo01;
import com.dfbz.demo02.Demo02;
import com.sun.java.accessibility.AccessBridge;
import org.junit.Test;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02_类加载器的种类 {
@Test
public void test1(){
// Bootstrap类加载器是获取不到的,为null
System.out.println("Bootstrap ClassLoader: "+ String.class.getClassLoader());
// jre\lib\ext\access-bridge-64.jar
System.out.println("ExtClassLoader ClassLoader: "+ AccessBridge.class.getClassLoader());
System.out.println("AppClassLoader ClassLoader: "+ Demo02.class.getClassLoader());
}
}

2.3.2 双亲委派机制

从JDK1.2开始,类的加载过程采用双亲委派机制,它是一种任务委派模式。即把加载类的请求交由父加载器处理,一直到顶层的父加载器(BootstrapClassLoader);如果父加载器能加载则用父加载器加载,否则才用子加载器加载该类;

  • 示例代码:
package com.dfbz.demo01;
import org.junit.Test;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo03_类加载的过程 {
@Test
public void test1() {
Class<T> tClass = T.class;
System.out.println(tClass);
}
class T {
}
}

JVM在加载类时,会调用类加载器(ClassLoader)的loadClass方法进行加载;
ClassLoader类加载源码:

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) {
// 使用父加载器加载
c = parent.loadClass(name, false);
} else {

// 如果没有父加载器则使用BootstrapClassLoader加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果依旧没有加载,则调用自身的findClass方法进行加载
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;
}
}

findClass方法源码:

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

可以看到,默认情况下ClassLoader的findClass方法只是抛出了一个异常而已(这个方法是留给我们写的)


  • 双亲委派机制流程图:

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

  • 1)从上图我们可以分析,当一个Demo.class这样的文件要被加载时,首先会在AppClassLoader中检查是否加载过,如果有那就无需再加载了。

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

  • 2)如果没有加载,那么会拿到父加载器(向上委派),然后调用父加载器的loadClass方法进行加载。AppClassLoader的父加载器为ExtClassLoader,而ExtClassLoader并没有重写loadClass方法,因此还是调用ClassLoader类的loadClass方法,相当于是一个递归的操作;

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

  • 3)父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意是个递归的过程,直到到达Bootstrap classLoader之前,都是在检查是否加载过,并不会选择自己去加载。直到BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出ClassNotFoundException。

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)


BootstrapClassLoader不能加载该类,因此还是为null,然后调用本类的findClass方法;这里需要注意两点:

  • 1)本类还是ExtClassLoader
  • 2)ExtClassLoader是自身没有findClass方法,但ExtClassLoader继承与URLClassLoader,并且URLClassLoader提供有findClass方法;

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)


接下来调用到URLClassLoader类中的findClass方法来加载该类:

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)


URLClassLoader类中的findClass方法无法加载我们传递的类,然后向上抛出了一个异常;这里需要注意:

  • 1)URLClassLoader类中的findClass方法是通过ExtClassLoader调用findClass方法进去的,因此向上抛出异常后,findClass方法后面的代码将不会执行了,并且触发的异常继续往上抛给调用者(调用loadClass的对象)
  • 2)ExtClassLoader的loadClass方法是在AppClassLoader中,通过parent.loadClass()调用进去的,因此异常被抛到了这里;

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)


异常被抛到了AppClassLoader中的loadClass方法中,接着尝试使用AppClassLoader的findClass()方法来加载类;

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)


最终交给AppClassLoader完成类的加载:

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

2.3.3 双亲委派的好处

我们已经了解了Java中类加载的双亲委派机制,**即加载类时交给父加载器加载,如果父加载器不能加载,再交给子加载器加载;**这样做有何好处呢?

  • 1)避免类的重复加载:当父类加载器已经加载了该类时,就没有必要子 ClassLoader 再加载一次。
  • 2)安全问题:有了双亲委派机制,当有人想要替换系统级别的类或者篡改他的实现时,在双亲委派机制下,在任何的Java代码运行之前,会将所有要用到的系统类提前使用BootstrapClassLoader加载进内存(而当一个类需要被加载时必定会轮到BootstrapClassLoader来加载,只是是否能加载的问题,不能加载的必定不是系统级别的类),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

在指定的系统包下建立指定的类(由BootstrapClassLoader、ExtClassLoader加载的系统类):

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

  • Object:
package java.lang;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Object {
static {
System.out.println("自定义的Object类被加载了....");
}
}
  • AccessBridge:
package com.sun.java.accessibility;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class AccessBridge {
static {
System.out.println("自定义的AccessBridge类被加载了.....");
}
}
  • 测试类:
package com.dfbz.demo01;
import com.sun.java.accessibility.AccessBridge;
import org.junit.Test;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo04_双亲委派的好处 {
@Test
public void test1() {
// java.lang.Object 类在JVM启动时就已经被加载过了,因此不会再被加载了
Class<Object> clazz = Object.class;
// com.sun.java.accessibility.AccessBridge 类在JVM启动时就已经被加载过了,因此不会再被加载了
Class<AccessBridge> accessBridgeClass = AccessBridge.class;
}
}

Tips:根据双亲委派机制,我们自定义的Object、AccessBridge类不可能被加载;

另外,JVM的类加载器对包名的定义也有限制;不允许我们自定义系统包名

在系统包名下创建任意一个类:

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

@Test
public void test2() {
// 不允许用户将类定义在受限包名下 ,Prohibited package name: java.lang
Class<AA> clazz = AA.class;
}

运行结果:

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

2.3.4 URLClassLoader类加载器

在 java.net 包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了 ClassLoader,能够从本地或者网络上指定的位置加载类,我们可以使用该类作为自定义的类加载器使用。
URLClassLoader的构造方法:

  • ​public URLClassLoader(URL[] urls)​​:指定要加载的类所在的URL地址,父类加载器默认为系统类加载器
  • ​public URLClassLoader(URL[] urls, ClassLoader parent)​​:指定要加载的类所在的URL地址,并指定父类加载器。
1)加载本地磁盘上的类:

在指定目录下准备一个Java文件并把它编译成class文件:

  • Show.java:
package com.dfbz.demo01;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Show {
public Show(){
System.out.println("new Show....");
}
}
  • 编译文件:
D:\000\com\dfbz\demo01>javac Show.java
D:\000\com\dfbz\demo01>

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

  • 测试代码:
package com.dfbz.demo01_类加载器的功能;
import org.junit.Test;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo05_URLClassLoader {
@Test
public void test() throws Exception{
File file = new File("D:\\000");
// file ---> URI
URI uri = file.toURI();
// URI ---> URL
URL url = uri.toURL();
// 根据URL构建一个类加载器
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
System.out.println("父类加载器:" + classLoader.getParent()); // 默认父类加载器是系统类加载器
Class clazz = classLoader.loadClass("com.dfbz.demo01.Show");
// 实例化这个类
clazz.newInstance();
}
}

运行结果:

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

2)加载网络上的类:
@Test
public void test2() throws Exception{
// 构建一个网络地址
URL url = new URL("http://www.baidu.com/class/");

URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
System.out.println("父类加载器:" + classLoader.getParent()); // 默认父类加载器是系统类加载器
Class clazz = classLoader.loadClass("com.baidu.demo.Show");

// 实例化这个类
clazz.newInstance();
}

Tips:关于加载网络上的类,等我们以后学习了服务器编程再来体验!

2.3.5 自定义类加载器

我们如果需要自定义类加载器,只需要继承ClassLoader,并覆盖掉findClass方法即可。

Tips:我们自定义的类加载器的父加载器为AppClassLoader;

  • 自定义类加载器:
package com.dfbz.demo02;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
// 1. 继承 ClassLoader
// 2. 覆盖 findClass方法
public class MyClassLoader extends ClassLoader {
// 被加载类所在的目录
private String dir;
public MyClassLoader(String dir) { // 默认父类加载器就是系统类加载器 AppClassLoader
this.dir = dir;
}
public MyClassLoader(ClassLoader parent, String dir) {
super(parent);
this.dir = dir;
}
/**
*
* @param name
* @return 重写findClass方法
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 把类名转换为目录 ---> D:/000/com/dfbz/demo01/Show.class
String file = dir + "/" + name.replace(".", "/") + ".class";
// 从文件中读取这个Class文件
InputStream in = new FileInputStream(file);
// 构建一个内存输出流(将读取到的Class文件写在内存中)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len ;
while ((len = in.read(buf)) != -1) {
baos.write(buf, 0, len);
}
// 读取到的流的二进制数据
byte[] data = baos.toByteArray();
in.close();
baos.close();
/*
defineClass: 根据类的全包名和内存中的数据流来加载一个类
- 参数1: 需要加载类的全包名
- 参数2: 已经加载到内存中的数据流
- 参数3: 从指定的数据下表开始读取
- 参数4: 读取到什么位置
*/
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
  • 测试类:
package com.dfbz.demo02;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01_自定义类加载器的使用 {
public static void main(String[] args) throws Exception{

// 创建我们自己的类加载器
MyClassLoader classLoader = new MyClassLoader("d:/000");

// 使用loadClass加载类
Class<?> clazz = classLoader.loadClass("com.dfbz.demo01.Show");

clazz.newInstance();
}
}

2.3.6 打破双亲委派

我们前面自定义了类加载器,观察下面代码:

package com.dfbz.demo02_自定义类加载器;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo02_打破双亲委派_01 {
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader("d:/000");
MyClassLoader classLoader2 = new MyClassLoader("d:/000");
Class<?> clazz = classLoader.loadClass("com.dfbz.demo01.Show");
Class<?> clazz2 = classLoader2.loadClass("com.dfbz.demo01.Show");
System.out.println(clazz == clazz2); // true
System.out.println(clazz.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(clazz2.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
}
}

运行结果:

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)


根据我们之前学习双亲委派机制,上面两个类加载器在加载Show类时,都会判断有没有加载这个类,没有加载则使用父加载器加载,MyClassLoader的父加载器是AppClassLoader,而AppClassLoader正好可以加载这个类;所以其实这两次的加载都是由AppClassLoader来加载的,而AppClassLoader在加载时会判断是否已经加载过,加载过了则不加载;因此Show类只会加载一次;


但是需要注意的是,双亲委派机制的逻辑是写在ClassLoader类的loadClass方法中的,通过一系列逻辑判断最终执行findClass方法来加载类;如果我们加载类直接使用findClass方法呢?那就相当于避开了双亲委派;(当然也可以重写loadClass方法,重新自定义loadClass规则)

  • 测试类:
package com.dfbz.demo02;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo03_打破双亲委派_02 {
public static void main(String[] args) throws Exception{
MyClassLoader classLoader = new MyClassLoader("d:/000");
MyClassLoader classLoader2 = new MyClassLoader("d:/000");
// 不使用loadClass来加载类,直接使用findClass方法去加载类,每一次调用findClass都相当于是加载一次新的类
Class<?> clazz = classLoader.findClass("com.dfbz.demo01.Show");
Class<?> clazz2 = classLoader2.findClass("com.dfbz.demo01.Show");
System.out.println(clazz == clazz2); // false
System.out.println(clazz.getClassLoader()); // com.dfbz.demo02.MyClassLoader@135fbaa4
System.out.println(clazz2.getClassLoader()); // com.dfbz.demo02.MyClassLoader@330bedb4
}
}

运行结果:

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

2.2 类的加载过程

2.2.1 类的生命周期

一个Java类从开始到结束整个生命周期会经历7个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。

其中验证、准备、解析三个部分又统称为连接(Linking)。

都工作3年了,怎么能不懂双亲委派呢?(带你手把手断点源码)

1)加载

加载过程就是把class字节码文件载入到虚拟机中,至于从哪儿加载,虚拟机设计者并没有限定,你可以从文件、压缩包、网络、数据库等等地方加载class字节码。
类加载的方式有:

  • 1)通过类的全限定名来获取定义此类的二进制字节流
  • 2)将此二进制字节流所代表的静态存储结构转化成方法区的运行时数据结构(加载到内存)
  • 3)在内存中生成代表此类的java.lang.Class对象(在堆中),作为该类访问入口;
2)连接

连接阶段的开始,并不一定等到加载阶段结束。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹杂在加载阶段之中的动作任然属于连接阶段,加载和连接这两个阶段的开始顺序是固定的。

  • 验证:验证节点主要确保Class文件的格式正确,运行时不会危害JVM的安全;包含文件格式验证、元数据验证、字节码验证、符号引用验证等;
  • 准备:准备阶段会为类变量(被static修饰的变量)分配内存并设置类变量的初始值,这些变量所使用的内存都将在方法区中分配。
    假如有一个变量​​​private static int value = 123;​​​那么value在准备阶段过后值是0,而不是123;因为这个时候尚未执行任何java方法,而把value赋值为123的动作在初始化阶段才会执行。
    但是如果上面的变量被final修饰,变为:​​​private static final int value = 123;​​编译时javac会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
  • 解析:解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

Tips:

  • 符号引用:在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
  • 直接引用:直接引用可以直接指向目标的指针,如果有了直接引用,那引用的目标必定已经在内存中存在。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型方法句柄和调用点限定符7类符号引用进行。

3)初始化

类初始化是类加载过程的最后一步,这一步会真正开始执行类中定义的Java程序代码(或者说字节码)。在准备阶段,变量已经被赋过一次系统要求的初始值,在初始化阶段,变量会再次赋值为程序员设置的值。比如变量:​​private static int value = 123;​​​那么value在准备阶段过后值是0,初始化阶段后值是123。
会导致 类加载 的情况

  • 1)main方法所在的类总是被首先初始化
  • 2)创建该类对象时,首先会将内加载到内存(如果该类存在父类,那么首先加载父类到内存,创建父类的对象(super))
  • 3)访问该类的静态成员时,会将类加载到内容(该静态常量不能被final修饰的基本类型和字符型)
  • 4)class.forName(“类的全包名”)

不会导致 类加载 的情况

  • 1)访问 类的 static final 静态变量(基本类型和字符型)不会触发初始化
  • 2)类对象.class 不会触发初始化
  • 3)创建该类的数组不会触发初始化
  • 4)Class.forName 的参数2 为 false 时

测试类:

package com.dfbz.demo03_类的初始化流程;
/**
* @author lscl
* @version 1.0
* @intro:
*/
public class Demo01_测试类 {
public static void main(String[] args) {
System.out.println(A.B); // 访问类中的final成员类并不会初始化
System.out.println(A.OBJ); // 访问非基本数据类型和String时将会初始化A
Class<A> aClass = A.class; // 类已经初始化过一次了,并不会再次初始化
System.out.println(aClass);
}
}
class A {
static {
System.out.println("A加载了");
}
public static final String B = "1";
// public static final String B = new String(); // 如果访问的是堆内存中的String,那么A将会被加载
public static final Obj OBJ = new Obj();
}
class Obj{}