类的加载及ClassLoader源码分析

小例题

  1. package com.jvm.classloader;
  2. class YeYe{
  3. static {
  4. System.out.println("YeYe静态代码块");
  5. }
  6. }
  7. class Father extends YeYe{
  8. public static String strFather="HelloJVM_Father";
  9. static{
  10. System.out.println("Father静态代码块");
  11. }
  12. }
  13. class Son extends Father{
  14. public static String strSon="HelloJVM_Son";
  15. static{
  16. System.out.println("Son静态代码块");
  17. }
  18. }
  19. public class InitiativeUse {
  20. public static void main(String[] args) {
  21. System.out.println(Son.strFather);
  22. }
  23. }

运行结果:

运行结果:
YeYe静态代码块
Father静态代码块
HelloJVM_Father

小例题2

  1. package com.jvm.classloader;
  2. class YeYe{
  3. static {
  4. System.out.println("YeYe静态代码块");
  5. }
  6. }
  7. class Father extends YeYe{
  8. public final static String strFather="HelloJVM_Father";
  9. static{
  10. System.out.println("Father静态代码块");
  11. }
  12. }
  13. class Son extends Father{
  14. public static String strSon="HelloJVM_Son";
  15. static{
  16. System.out.println("Son静态代码块");
  17. }
  18. }
  19. public class InitiativeUse {
  20. public static void main(String[] args) {
  21. System.out.println(Son.strFather);
  22. }
  23. }

运行结果:HelloJVM_Father

类的加载

需要使用一个类的时候,如果这个类没有被加载内存里面,则JVM会通过加载、连接、初始化三个步骤来对该类进行初始化。

  • 类型的加载、连接、初始化过程都是在程序运行期间完成的。
    • 类型:定义的类、接口、枚举成为类型,不涉及具体的对象
      • 是创建对象之前的一些信息
    • 程序运行期间:典型例子是动态代理
  • 这样可以提供更多的灵活性,增加了更多的可能性
  • 对比:Class对象与new出来的对象的关系
    • Class对象描述了new出来的对象是什么样的,
    • Class对象是唯一的,但是new出来的对象可以是多个,
    • 每个new的对象都是以Class对象为模板进行创建的

加载阶段的过程就是:
.class文件(二进制数据)——>读取到内存——>数据放进方法区——>堆中创建对应Class对象——>并提供访问方法区的接口
**
其中.class对象可以是通过以下方式来获取:本地磁盘、网络、zip以及jar等归档文件、数据库中提取、java源文件动态编译等方式。

加载阶段是最可控的阶段,在这个阶段,可以自定义自己的类加载器来完成加载.

首次主动使用

类的主动使用 (超级重要)

  1. 使用new关键字实例化对象的时候
  2. 读取或设置一个类型的静态字段(如果静态自动同时final需另外讨论)
    1. 但是对于final static 的则不需要类的初始化,例如static final int a = 11;则使用a的时候不需要进行初始化,因为在编译期就已经把a的运算结果放入了常量池,直接去常量池取就可以,不需要进行类初始化
    2. 但是对于static final int a = random(),使用a的时候必须进行初始化,因为a的值得编译期间无法得到,必须在运行期间才可以确定,需要使用类型的初始化
  3. 调用一个类型的静态方法
  4. 使用反射的时候,Class.forName()
  5. 启动类(包含Main方法的那个类)
  6. 子类初始化时候,如果父类没有进行初始化,初始化父类

一般来说只有当类的首次主动使用的时候才会导致类的初始化(注意是类的初始化不是类的对象的初始化)

  1. class Father6 {
  2. public static int a = 1;
  3. static {
  4. System.out.println("父类");
  5. }
  6. }
  7. class Son6 {
  8. public static int b = 2;
  9. static {
  10. System.out.println("子类");
  11. }
  12. }
  13. public class OverallTest {
  14. static {
  15. System.out.println("Main");
  16. }
  17. public static void main(String[] args) {
  18. Father6 father6; // 此时没有初始化,因为只是声明了一个引用
  19. System.out.println("======");
  20. father6 = new Father6(); //进行了初始化,因为使用了new()命令
  21. System.out.println("======");
  22. System.out.println(Father6.a);
  23. System.out.println("======");
  24. System.out.println(Son6.b); // 使用了Son中定义的静态变量,需要进行Son的初始化
  25. }
  26. }
  1. Main方法静态代码块
  2. ======
  3. 父类粑粑静态代码块
  4. ======
  5. 1
  6. ======
  7. 子类熊孩子静态代码块
  8. 2
  • OverallTest会首先被初始化,因为执行了其中的Main静态方法
  • Father6 father6; 不进行初始化
  • new Father() 使用了new关键字,说明主动使用了,所以Father要进行类的初始化
  • System.out.println(Father6.a) 因为已经初始化了Father,所以不需要进行在此的初始化
  • 使用Son.b的时候,使用了静态字段,必须进行初始化


验证

确保class文件的字节流中包含的信息符合虚拟机规范的全部要求,确保这些信息以后不会危害虚拟机的安全。

分为四个阶段:

  1. 文件格式验证:
    1. 目的:此阶段保证输入的字节流可以正确的存储在方法区之内,格式上符合Java类型信息的要求,后面三个阶段都是在方法区中进行验证的
    2. 具体流程
      1. 一个具体的魔数开头
      2. 主次版本号是不是可以被当前虚拟机接受
      3. 常量池是否有不被支持的类型
  2. 元数据验证:
    1. 目的:对字节码描述的信息进行语义分析,主要是对类的元数据信息进行校验
    2. 流程
      1. 是否有父类
      2. 是否继承了不能被继承的类
      3. 字段、方法是否和父类矛盾
  3. 字节码验证
    1. 目的:通过数据流分析和控制流分析,确保程序语义合法且符合逻辑
    2. 流程:主要是对类的方法体进行校验,需要保证:
      1. 操作数栈的数据类型与指令类型一致
      2. 不会跳转到方法体以外
      3. 方法内的类型转换是合理的
      4. ….
  4. 符号引用验证:
    1. 将虚拟机符号引用转换为直接引用,确定该类是否访问了禁止本类访问的类、方法、资源等

准备

本阶段正式为类中定义的变量(static变量)**分配内存并设置变量默认初始值**。
注意:此时进行内存分配的变量仅仅是类变量(例如static int a = 1,而不是实例变量(例如int a = 1),实例变量将会随着对象的初始化一同分配到java堆中。
类变量在此阶段初始值一般设置为该变量对应类型的**零值**。

  • 假设:一个类变量被定义为public static int value = 123;那么在这个阶段过后,value的值应该是0,而不是123,value被赋值为123会在类的初始化阶段进行。
  • 注意:如果static final int a = 11;则会直接在此阶段赋值为a=11,因为final类型是不可变的,且在编译期间就确定了a的值。
    • 如果此时static final int a = random();那么a的赋值必须等到运行期间random()方法执行的时候进行

解析

虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

其实这个阶段对于我们来说也是几乎透明的,了解一下就好。

初始化

到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。
在准备阶段,类变量已经被赋值成初始零值,而在本阶段,则会根据程序员的要求去赋值

初始化类构造器:JVM 会按顺序收集类变量**的赋值语句静态代码块,最终组成类构造器由 JVM 执行。就是clinit()方法
初始化对象构造器:JVM 会按照收集成员变量**的赋值语句普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。

更一般来说,初始化阶段就是执行<**clinit**>()方法的过程。

  • 此方法不是程序员直接编写的方法,而是自动生成的。
  • 此方法是编译器自动收集类中的所有类变量的赋值动作和静态语句块中的代码static{...}合并成的(如果类没有静态语句块以及类变量的赋值操作,则不产生此方法),顺序是源文件中出现的顺序
    • 注意:静态语句块只能访问定义在静态语句块之前的变量, 可以为定义在本静态语句块之后的变量进行赋值,但是不能访问自身之后定义的变量
      • image.png
  • 保证子类的()方法执行前,父类的此方法已经执行完毕(父接口的不一样),所以虚拟机第一个被执行的此方法一定是Object的此方法。(父类立即执行clinit)
    • 因为父类的()先执行,所以父类的静态语句块优先于子类的静态语句块执行
    • image.png
  • 对于子类的父接口,只有当父接口中定义的变量被使用时候,父接口才被初始化,接口的实现类在初始化时候也一样不会执行接口的clinit()方法(父接口中定义的变量被使用的时候,才进行初始化)
  • 多线程时候,同一个类的clinit()是被加锁执行的

关于初始化的面试题

对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块)!

一:没有final的情况

  1. class A {
  2. static int a = 1;
  3. static {
  4. System.out.println("A.static initializer");
  5. }
  6. }
  7. class B extends A {
  8. static {
  9. System.out.println("B.static initializer");
  10. }
  11. static int b = 2;
  12. }
  13. class C extends B {
  14. static {
  15. System.out.println("C.static initializer");
  16. }
  17. static int c = 3;
  18. }
  19. public class D {
  20. static {
  21. System.out.println("D.static initializer");
  22. }
  23. public static void main(String[] args) {
  24. System.out.println(C.b);
  25. }
  26. }
  27. D.static initializer // Main方法所在类需要初始化
  28. A.static initializer // C.b 访问时候 b在B中,A为B的父类
  29. B.static initializer // 初始化B
  30. 2

根据首次主动使用的规则:

  1. 先初始化Main所在的D类
  2. 直接定义b的类是B,所以C.b的时候,不需要初始化C,因为b直接定义在了B类中。由于需要访问B类的类变量,所以需要初始化类B,同时B有父类,所以需要先初始化B的父类A。

二:final
有final的情况

  1. final值确定 ```java class A { static int a = 1;

    static {

    1. System.out.println("A.static initializer");

    } }

class B extends A { static { System.out.println(“B.static initializer”); }

  1. final static int b = 2;

}

class C extends B {

  1. static {
  2. System.out.println("C.static initializer");
  3. }
  4. static int c = 3;

}

public class D { static { System.out.println(“D.static initializer”); }

  1. public static void main(String[] args) {
  2. System.out.println(C.b);
  3. }

}

D.static initializer // Main方法初始化 2 // 因为在编译期间已经确定了b的值,所以直接去常量池去取b的值就可以,不需要先初始化B类

  1. 祥看首次主动初始化第二条。
  2. 2. final值不确定
  3. ```java
  4. class A {
  5. static int a = 1;
  6. static {
  7. System.out.println("A.static initializer");
  8. }
  9. }
  10. class B extends A {
  11. static {
  12. System.out.println("B.static initializer");
  13. }
  14. final static int b = Math.min(1, 2);
  15. }
  16. class C extends B {
  17. static {
  18. System.out.println("C.static initializer");
  19. }
  20. static int c = 3;
  21. }
  22. public class D {
  23. static {
  24. System.out.println("D.static initializer");
  25. }
  26. public static void main(String[] args) {
  27. System.out.println(C.b);
  28. }
  29. }
  30. D.static initializer
  31. A.static initializer
  32. B.static initializer
  33. 1

此时由于b的值不能再编译期间确定,所以就需要先初始化类B

上面两个程序的主要区别在于:是否编译期间可以确定类变量的值
**
三:构造代码块:注意并不收集构造代码块的内容。
普通代码块只有在调用构造函数的时候才会执行。

  1. public class D {
  2. static {
  3. System.out.println("D.static initializer");
  4. }
  5. {
  6. System.out.println("D.instance initializer");
  7. }
  8. public static void main(String[] args) {
  9. }
  10. }
  11. D.static initializer
  12. =========
  13. public class D {
  14. static {
  15. System.out.println("D.static initializer");
  16. }
  17. {
  18. System.out.println("D.instance initializer");
  19. }
  20. public static void main(String[] args) {
  21. D d = new D();
  22. }
  23. }
  24. D.static initializer
  25. D.instance initializer

更多参见:https://www.yuque.com/u8070070/kosy91/vzx3o3image.png

类加载器

一个类的唯一性确定:由类加载器本身和类本身共同确立
比较两个类是否相同,只有在两个类是同一个类加载器的前提下才有意义。

JVM中,一个类用其**全限定类名和其类加载器**作为其唯一标识。

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载入JVM中,同一个类就不会被再次载入了。

三种加载器:
5类加载(不常问) - 图4

  1. 启动(Bootstrap)类加载器
    1. BootstrapClassLoader,启动类加载器主要加载的是JVM自身需要的类
    2. 这个类加载使用C++语言实现的,是虚拟机自身的一部分,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。
    3. 启动类加载器是无法被Java程序直接引用的。
    4. 总结一句话:启动类加载器加载java运行过程中的核心类库JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List…
  2. 扩展(Extension)类加载器
    1. ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
  3. 系统(System)类加载器(也称应用类加载器)
    1. ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
    2. 总结一句话:应用程序类加载器加载CLASSPATH变量指定路径下的类 即指你自已在项目工程中编写的类
  4. 还有一个在框架中用的比较多的的线程上下文加载器,它可以破坏双亲委派模型

  5. 也可以自定义类加载器

双亲委派

  • Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,
  • 而且加载某个类的class文件时,Java虚拟机默认采用的是双亲委派模式即把请求交由父类处理


  1. package com.jvm.classloaderQi;
  2. public class ClassloaderTest {
  3. public static void main(String[] args) {
  4. //获取ClassloaderTest类的加载器
  5. ClassLoader classLoader= ClassloaderTest.class.getClassLoader();
  6. System.out.println(classLoader);
  7. System.out.println(classLoader.getParent()); //获取ClassloaderTest类的父类加载器
  8. System.out.println(classLoader.getParent().getParent());
  9. }
  10. }
  11. ======
  12. sun.misc.Launcher$AppClassLoader@18b4aac2
  13. sun.misc.Launcher$ExtClassLoader@1b6d3586
  14. null

全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

父类委托先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类,通俗讲就是儿子们都他么是懒猪,自己不管能不能做,就算能加载也先不干,先给自己的父亲做,一个一个往上抛,直到抛到启动类加载器也就是最顶级父类,只有父亲做不了的时候再没办法由下一个子类做,直到能某一个子类能做才做,之后的子类就直接返回,实力坑爹!
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

流程

  1. 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上
  2. 因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,
  3. 只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。


  1. **AppClassLoader**加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器**ExtClassLoader**去完成。
  2. **ExtClassLoader**加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给**BootStrapClassLoader**去完成。
  3. 如果 **BootStrapClassLoader**加载失败(例如在 $JAVA_HOME/jre/lib里未查找到该class),会使用 **ExtClassLoader**来尝试加载;
  4. **ExtClassLoader**也加载失败,则会使用 **AppClassLoader**来加载,如果 **AppClassLoader**也加载失败,则会报出异常 **ClassNotFoundException**

5类加载(不常问) - 图5

5类加载(不常问) - 图6

源码

类加载器路口sun.misc.Launcher

ApplicationClassLoader和扩展类加载器 ExtensionClassLoader的时候就已经提到过这两个类加载器是由sun.misc.Launcher实现的!

  1. public class Launcher {
  2. private static Launcher launcher = new Launcher();
  3. private static String bootClassPath =
  4. System.getProperty("sun.boot.class.path");
  5. public static Launcher getLauncher() {
  6. return launcher;
  7. }
  8. private ClassLoader loader;
  9. public Launcher() {
  10. // Create the extension class loader
  11. ClassLoader extcl;
  12. try {
  13. // 主要是为了作为参数传递给AppClassLoader
  14. extcl = ExtClassLoader.getExtClassLoader();
  15. } catch (IOException e) {
  16. throw new InternalError(
  17. "Could not create extension class loader", e);
  18. }
  19. // Now create the class loader to use to launch the application
  20. try {
  21. loader = AppClassLoader.getAppClassLoader(extcl);
  22. } catch (IOException e) {
  23. throw new InternalError(
  24. "Could not create application class loader", e);
  25. }
  26. //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
  27. Thread.currentThread().setContextClassLoader(loader);
  28. }
  29. /*
  30. * Returns the class loader used to launch the main application.
  31. */
  32. public ClassLoader getClassLoader() {
  33. return loader;
  34. }
  35. /*
  36. * The class loader used for loading installed extensions.
  37. */
  38. static class ExtClassLoader extends URLClassLoader {}
  39. /**
  40. * The class loader used for loading from java.class.path.
  41. * runs in a restricted security context.
  42. */
  43. static class AppClassLoader extends URLClassLoader {}

注意:ExtClassLoader 并不是 AppClassLoader 的父类,而是一种逻辑上的父类。

  1. static class ExtClassLoader extends URLClassLoader {}
  2. static class AppClassLoader extends URLClassLoader {}
  1. Launcher初始化了ExtClassLoaderAppClassLoader
  2. Launcher()首先是创建了Extcl扩展类加载器,之后的App应用类【系统类】加载器作为Launcher中的一个成员变量
    1. 至于为啥不把Extcl扩展类加载器也做为成员变量的原因,大家可以仔细想一想,是为啥呢?其实很简单,因为没必要,因为直接把App系统加载器.parent()方法即可得到Extcl扩展类加载器!
    2. extcl作为参数loader = AppClassLoader.getAppClassLoader(extcl);进行初始化loader
  3. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty(“sun.boot.class.path”)得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。

ExtClassLoader、AppClassLoader源码

ExtClassLoader

  1. /*
  2. * The class loader used for loading installed extensions.
  3. */
  4. static class ExtClassLoader extends URLClassLoader {
  5. static {
  6. ClassLoader.registerAsParallelCapable();
  7. }
  8. /**
  9. * create an ExtClassLoader. The ExtClassLoader is created
  10. * within a context that limits which files it can read
  11. */
  12. public static ExtClassLoader getExtClassLoader() throws IOException
  13. {
  14. final File[] dirs = getExtDirs();
  15. try {
  16. // Prior implementations of this doPrivileged() block supplied
  17. // aa synthesized ACC via a call to the private method
  18. // ExtClassLoader.getContext().
  19. return AccessController.doPrivileged(
  20. new PrivilegedExceptionAction<ExtClassLoader>() {
  21. public ExtClassLoader run() throws IOException {
  22. int len = dirs.length;
  23. for (int i = 0; i < len; i++) {
  24. MetaIndex.registerDirectory(dirs[i]);
  25. }
  26. return new ExtClassLoader(dirs);
  27. }
  28. });
  29. } catch (java.security.PrivilegedActionException e) {
  30. throw (IOException) e.getException();
  31. }
  32. }
  33. private static File[] getExtDirs() {
  34. String s = System.getProperty("java.ext.dirs");
  35. File[] dirs;
  36. if (s != null) {
  37. StringTokenizer st =
  38. new StringTokenizer(s, File.pathSeparator);
  39. int count = st.countTokens();
  40. dirs = new File[count];
  41. for (int i = 0; i < count; i++) {
  42. dirs[i] = new File(st.nextToken());
  43. }
  44. } else {
  45. dirs = new File[0];
  46. }
  47. return dirs;
  48. }
  49. }

AppClassLoader源码

  1. /**
  2. * The class loader used for loading from java.class.path.
  3. * runs in a restricted security context.
  4. */
  5. static class AppClassLoader extends URLClassLoader {
  6. public static ClassLoader getAppClassLoader(final ClassLoader extcl)
  7. throws IOException
  8. {
  9. final String s = System.getProperty("java.class.path");
  10. final File[] path = (s == null) ? new File[0] : getClassPath(s);
  11. return AccessController.doPrivileged(
  12. new PrivilegedAction<AppClassLoader>() {
  13. public AppClassLoader run() {
  14. URL[] urls =
  15. (s == null) ? new URL[0] : pathToURLs(path);
  16. return new AppClassLoader(urls, extcl);
  17. }
  18. });
  19. }
  20. }

获取parent

父加载器并不是指其父类,ExtClassLoaderAppClassLoader同样继承自URLClassLoader,:那为啥调用AppClassLoader.getParent()方法会得到ExtClassLoader的实例呢?

实际上URLClassLoader的源码中也并没有getParent()方法。这个方法在ClassLoader中,源码如下:

  1. public abstract class ClassLoader {
  2. // The parent class loader for delegation
  3. // Note: VM hardcoded the offset of this field, thus all new fields
  4. // must be added *after* it.
  5. private final ClassLoader parent; // 这个地方
  6. // The class loader for the system
  7. // @GuardedBy("ClassLoader.class")
  8. private static ClassLoader scl;
  9. private ClassLoader(Void unused, ClassLoader parent) {
  10. this.parent = parent;
  11. ...
  12. }
  13. protected ClassLoader(ClassLoader parent) { // 传入父类
  14. this(checkCreateClassLoader(), parent);
  15. }
  16. protected ClassLoader() {
  17. // 如果不指定父类,
  18. // 由getSystemClassLoader()方法生成
  19. // 也就是AppClassLoader
  20. this(checkCreateClassLoader(), getSystemClassLoader());
  21. }
  22. public final ClassLoader getParent() {
  23. if (parent == null)
  24. return null;
  25. return parent;
  26. }
  27. public static ClassLoader getSystemClassLoader() {
  28. initSystemClassLoader();
  29. if (scl == null) {
  30. return null;
  31. }
  32. return scl;
  33. }
  34. private static synchronized void initSystemClassLoader() {
  35. if (!sclSet) {
  36. if (scl != null)
  37. throw new IllegalStateException("recursive invocation");
  38. sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
  39. if (l != null) {
  40. Throwable oops = null;
  41. //通过Launcher获取ClassLoader
  42. scl = l.getClassLoader();
  43. try {
  44. scl = AccessController.doPrivileged(
  45. new SystemClassLoaderAction(scl));
  46. } catch (PrivilegedActionException pae) {
  47. oops = pae.getCause();
  48. if (oops instanceof InvocationTargetException) {
  49. oops = oops.getCause();
  50. }
  51. }
  52. if (oops != null) {
  53. if (oops instanceof Error) {
  54. throw (Error) oops;
  55. } else {
  56. // wrap the exception
  57. throw new Error(oops);
  58. }
  59. }
  60. }
  61. sclSet = true;
  62. }
  63. }
  64. }

从上面的源码可以看到getParent()实际上返回的就是一个ClassLoader对象parentparent的赋值是在ClassLoader对象的构造方法中,它有两个情况:
1、由外部类创建ClassLoader时直接指定一个ClassLoaderparent
2、不指定就由getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。简单的说,就是一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader

ClassLoader源码分析

ClassLoader.loadClass()

在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作:
5类加载(不常问) - 图7
顶层的类加载器是抽象类ClassLoader类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),为了更好理解双亲委派模型,ClassLoader源码中的loadClass(String)方法该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,**loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现**,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。

  1. public Class<?> loadClass(String name) throws ClassNotFoundException {
  2. return loadClass(name, false);
  3. }
  4. protected Class<?> loadClass(String name, boolean resolve)
  5. throws ClassNotFoundException
  6. {
  7. synchronized (getClassLoadingLock(name)) {
  8. // 先从缓存查找该class对象,找到就不用重新加载
  9. Class<?> c = findLoadedClass(name);
  10. if (c == null) {
  11. long t0 = System.nanoTime();
  12. try {
  13. if (parent != null) {
  14. //如果找不到,则委托给父类加载器去加载
  15. c = parent.loadClass(name, false);
  16. } else {
  17. //如果没有父类,则委托给启动加载器去加载
  18. c = findBootstrapClassOrNull(name);
  19. }
  20. } catch (ClassNotFoundException e) {
  21. // ClassNotFoundException thrown if class not found
  22. // from the non-null parent class loader
  23. }
  24. if (c == null) {
  25. // If still not found, then invoke findClass in order
  26. // 如果都没有找到,则通过自定义实现的findClass去查找并加载
  27. c = findClass(name);
  28. // this is the defining class loader; record the stats
  29. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  30. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  31. sun.misc.PerfCounter.getFindClasses().increment();
  32. }
  33. }
  34. if (resolve) {//是否需要在加载时进行解析
  35. resolveClass(c);
  36. }
  37. return c;
  38. }
  39. }

安全性:
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

findClass

  • JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中
  • findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
  • 需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下: ```java //直接抛出异常 protected Class<?> findClass(String name) throws ClassNotFoundException {
    1. throw new ClassNotFoundException(name);
    }
  1. <a name="3WJHi"></a>
  2. #### defineClass
  3. 将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:
  4. ```java
  5. protected Class<?> findClass(String name) throws ClassNotFoundException {
  6. // 获取类的字节数组
  7. byte[] classData = getClassData(name);
  8. if (classData == null) {
  9. throw new ClassNotFoundException();
  10. } else {
  11. //使用defineClass生成class对象
  12. return defineClass(name, classData, 0, classData.length);
  13. }
  14. }

如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

Class.forName

Class.forName()是一种获取Class对象的方法,而且是静态方法。

  1. publicstatic Class<?> forName(String className)
  2. Returns the Class object associated withthe class or interface with the given string name. Invokingthis method is equivalent to:
  3. Class.forName(className,true, currentLoader)
  4. where currentLoader denotes the definingclass loader of the current class.
  5. For example, thefollowing code fragment returns the runtime Class descriptor for theclass named java.lang.Thread:
  6. Class t =Class.forName("java.lang.Thread")
  7. A call to forName("X") causes theclass named X to beinitialized.
  8. Parameters:
  9. className - the fully qualifiedname of the desired class.
  10. Returns:
  11. the Class object for the classwith the specified name.

可以看出,Class.forName(className)实际上是调用Class.forName(className,true, this.getClass().getClassLoader())。第二个参数,是指Classloading后是不是必须被初始化。可以看出,使用Class.forName(className)加载时,类则已初始化。所以Class.forName()方法可以简单的理解为:获得字符串参数中指定的类,并初始化该类。

Class.forName与ClassLoader.loadClass区别

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块
  • Class.forName(name, initialize, loader):带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
    ```java package com.jvm.classloader;

class Demo{ static { System.out.println(“static 静态代码块”); } }

public class ClassLoaderDemo { public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader=ClassLoaderDemo.class.getClassLoader(); //1、使用ClassLoader.loadClass()来加载类,不会执行初始化块 classLoader.loadClass(“com.jvm.classloader.Demo”);

  1. //2、使用Class.forName()来加载类,默认会执行初始化块
  2. Class.forName("com.jvm.classloader.Demo");
  3. //3、使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
  4. Class.forName("com.jvm.classloader.Demo",false,classLoader);
  5. }

}

```

双亲委派模型意义总结来讲就是:
1、系统类防止内存中出现多份同样的字节码
2、保证Java程序安全稳定运行
**

加载类的三个方式

  1. 静态加载,也就是通过new关键字来创建实例对象
  2. 动态加载,也就是通过Class.forName()方法动态加载(反射加载类型),然后调用类的newInstance()方法实例化对象
  3. 动态加载,通过类加载器loadClass()方法来加载类,然后调用类的newInstance()方法实例化对象

区别

  1. 第一种和第二种方式使用的类加载器是相同的,都是当前类加载器。(this.getClass.getClassLoader)。而3由用户指定类加载器
  2. 如果需要在当前类路径以外寻找类,则只能采用第3种方式。第3种方式加载的类与当前类分属不同的命名空间
  3. 第一种是静态加载,而第二、三种是动态加载。

注意事项

  1. 类加载器不需要等到每个类被“首次主动使用”时候再去加载它。