💗推荐阅读文章💗
- 🌸JavaSE系列🌸👉1️⃣《JavaSE系列教程》
- 🌺MySQL系列🌺👉2️⃣《MySQL系列教程》
- 🍀JavaWeb系列🍀👉3️⃣《JavaWeb系列教程》
- 🌻SSM框架系列🌻👉4️⃣《SSM框架系列教程》
🎉本博客知识点收录于🎉👉🚀《JavaSE系列教程》🚀—>✈️18【枚举、类加载器、动态代理】✈️
我们知道,所有的代码都是运行在内存中的,我们必须把类加载到内存中才能运行;在Java中,所有的Java类都是通过类加载器加载到内存进行执行的;
package com.dfbz.demo01;
import org.junit.Test;
/*** @author lscl* @version 1.0* @intro:*/
public class Demo01_类何时被加载 {@Testpublic void test1() throws ClassNotFoundException {
// new B(); // 先加载A再加载B
// Integer num = B.num; // 先加载A再加载BClass> 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:不管是用什么方法加载,类从始至终只会加载一次;
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_类加载器的种类 {@Testpublic void test1(){// Bootstrap类加载器是获取不到的,为nullSystem.out.println("Bootstrap ClassLoader: "+ String.class.getClassLoader());// jre\lib\ext\access-bridge-64.jarSystem.out.println("ExtClassLoader ClassLoader: "+ AccessBridge.class.getClassLoader());System.out.println("AppClassLoader ClassLoader: "+ Demo02.class.getClassLoader());}
}
从JDK1.2开始,类的加载过程采用双亲委派机制,它是一种任务委派模式。即把加载类的请求交由父加载器处理,一直到顶层的父加载器(BootstrapClassLoader);如果父加载器能加载则用父加载器加载,否则才用子加载器加载该类;
package com.dfbz.demo01;
import org.junit.Test;
/*** @author lscl* @version 1.0* @intro:*/
public class Demo03_类加载的过程 {@Testpublic void test1() {Class 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 statssun.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方法只是抛出了一个异常而已(这个方法是留给我们写的)





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

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

异常被抛到了AppClassLoader中的loadClass方法中,接着尝试使用AppClassLoader的findClass()方法来加载类;
最终交给AppClassLoader完成类的加载:
我们已经了解了Java中类加载的双亲委派机制,**即加载类时交给父加载器加载,如果父加载器不能加载,再交给子加载器加载;**这样做有何好处呢?
在指定的系统包下建立指定的类(由BootstrapClassLoader、ExtClassLoader加载的系统类):
package java.lang;
/*** @author lscl* @version 1.0* @intro:*/
public class Object {static {System.out.println("自定义的Object类被加载了....");}
}
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_双亲委派的好处 {@Testpublic void test1() {// java.lang.Object 类在JVM启动时就已经被加载过了,因此不会再被加载了Class
Tips:根据双亲委派机制,我们自定义的Object、AccessBridge类不可能被加载;
另外,JVM的类加载器对包名的定义也有限制;不允许我们自定义系统包名
在系统包名下创建任意一个类:
@Test
public void test2() {// 不允许用户将类定义在受限包名下 ,Prohibited package name: java.langClass clazz = AA.class;
}
运行结果:
在 java.net 包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了 ClassLoader,能够从本地或者网络上指定的位置加载类,我们可以使用该类作为自定义的类加载器使用。
URLClassLoader的构造方法:
public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器public URLClassLoader(URL[] urls, ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器。在指定目录下准备一个Java文件并把它编译成class文件:
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>

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 {@Testpublic void test() throws Exception{File file = new File("D:\\000");// file ---> URIURI uri = file.toURI();// URI ---> URLURL 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();}
}
运行结果:
@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:关于加载网络上的类,等我们以后学习了服务器编程再来体验!
我们如果需要自定义类加载器,只需要继承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) { // 默认父类加载器就是系统类加载器 AppClassLoaderthis.dir = dir;}public MyClassLoader(ClassLoader parent, String dir) {super(parent);this.dir = dir;}/**** @param name* @return 重写findClass方法* @throws ClassNotFoundException*/@Overrideprotected Class> findClass(String name) throws ClassNotFoundException {try {// 把类名转换为目录 ---> D:/000/com/dfbz/demo01/Show.classString 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();}
}
我们前面自定义了类加载器,观察下面代码:
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); // trueSystem.out.println(clazz.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2System.out.println(clazz2.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2}
}
运行结果:
根据我们之前学习双亲委派机制,上面两个类加载器在加载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); // falseSystem.out.println(clazz.getClassLoader()); // com.dfbz.demo02.MyClassLoader@135fbaa4System.out.println(clazz2.getClassLoader()); // com.dfbz.demo02.MyClassLoader@330bedb4}
}
运行结果:
一个Java类从开始到结束整个生命周期会经历7个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。
其中验证、准备、解析三个部分又统称为连接(Linking)。
加载过程就是把class字节码文件载入到虚拟机中,至于从哪儿加载,虚拟机设计者并没有限定,你可以从文件、压缩包、网络、数据库等等地方加载class字节码。
类加载的方式有:
连接阶段的开始,并不一定等到加载阶段结束。加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹杂在加载阶段之中的动作任然属于连接阶段,加载和连接这两个阶段的开始顺序是固定的。
private static int value = 123;那么value在准备阶段过后值是0,而不是123;因为这个时候尚未执行任何java方法,而把value赋值为123的动作在初始化阶段才会执行。private static final int value = 123;编译时javac会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。Tips:
- 符号引用:在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。
- 直接引用:直接引用可以直接指向目标的指针,如果有了直接引用,那引用的目标必定已经在内存中存在。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型方法句柄和调用点限定符7类符号引用进行。
类初始化是类加载过程的最后一步,这一步会真正开始执行类中定义的Java程序代码(或者说字节码)。在准备阶段,变量已经被赋过一次系统要求的初始值,在初始化阶段,变量会再次赋值为程序员设置的值。比如变量:private static int value = 123;那么value在准备阶段过后值是0,初始化阶段后值是123。
会导致 类加载 的情况
不会导致 类加载 的情况
测试类:
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时将会初始化AClass 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{}
上一篇:UML全解