原文链接;https://blog.csdn.net/qq_44543508/article/details/102983363 🌟🌟🌟🌟🌟


原文不在可以的复制过来了,但是一些重点内容 和有意思的东西,选择性的复制

1 一些基本概念

0 什么叫类的加载(初始化)

在java代码中,类型加载连接、与初始化过程都是在程序运行期间完成的(类从磁盘加载到内存中经历的三个阶段)【牢牢记在心里】

两个关键词 1 类型: 定义的类、接口或者枚举称为类型而不涉及对象,在类加载的过程中,是一个创建对象之前的一些信息 2 程序运行期间 程序运行期间完成典型例子就是动态代理,其实很多语言都是在编译期就完成了加载,也正因为这个特性给Java程序提供了更大的灵活性,增加了更多的可能性

1类的生命周期(🌟🌟🌟)

这个东西一定要熟悉一下,后边的内容都是围绕这个来的;
image.png
从上图可知,类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括 7 个阶段,而验证、准备、解析 3 个阶段统称为连接。
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是固定确定的,类的加载过程必须按照这种顺序开始(注意是“开始”,而不是“进行”),而解析阶段则不一定:它在某些情况下可以在初始化后再开始,这是为了支持 Java 语言的运行时绑定【也就是java的动态绑定/晚期绑定】

1 加载(🌟)

加载阶段是第一阶段;就是将类的.class文件中的二进制数据读入内存中;将其放在运行时数据区的方法区呢;然后再堆内创建一个Java.lang.class对象,用来放封装类在方法区内的数据结构,并向java程序员提供了访问方法区内的数据结构的接口;
总结:
.class文件(二进制数据)——>读取到内存——>数据放进方法区——>堆中创建对应Class对象——>并提供访问方法区的接口

2验证(看一眼就行)

确保被加载的类的正确性。 验证是连接阶段的第一阶段,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作: 1 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 2 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了 java.lang.Object之外。 3 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。 4 符号引用验证:确保解析动作能正确执行。 验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3 准备【重点🌟🌟🌟🌟🌟】

当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。准备阶段是正式为类变量分配内存设置类变量初始值的阶段,这些内存都将在方法区中分配;
这里有两个关键点:即内存分配的对象,以及初始化的类型

  1. 内存分配的对象:要明白首先要知道Java 中的变量有类变量以及类成员变量两种类型,类变量指的是被 static 修饰的变量,而其他所有类型的变量都属于类成员变量。

在准备阶段,JVM 只会为类变量分配内存,而不会为类成员变量分配内存。类成员变量的内存分配需要等到初始化阶段才开始

  1. public class User{
  2. static int age; //类变量 static修饰
  3. String name; // 类成员变量 无static
  4. }
  1. 初始化类型:准备阶段,jvm会为类变量分配内存,并为其初始化,但是这里的初始化值得是为变量赋予java语言中该数据类型的零值(默认值);而不是用户代码里的初始值; ```java public static int age = 18; // int零值是0 初始化0 public static Integer age = 18; // Integer零值为null

publice static final int age = 18; // static final 修饰的类变量有点特殊

如果一个变量是常量(被`static final`修饰)的话,在准备阶段的初始化就不再是初始化成零值了,而是直接赋值成用户希望的值;
<a name="I3N9R"></a>
#### 4 解析
不重要
<a name="QLNPm"></a>
#### 5 初始化【重要🌟🌟🌟🌟🌟】
到了初始化阶段,用户定义的java程序代码才真正的开始运行<br />java程序对类的使用方式分为两种: **主动使用**和**被动使用,一般来说:只有当对类的首次主动使用的时候才会导致类的初始化,**所以主动使用又叫做类加载过程中“初始化”开始的时机;<br />类的主动使用场景(**超级重要🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟**)
> 1、 创建类的实例,也就是new的方式
> ~~2、 访问某个类或接口的静态变量,或者对该静态变量赋值(~~~~凡是被final修饰的,不不不,其实更准确的说是在编译器把结果放入常量池的静态字段除外~~~~)~~
> ~~ 3、 调用类的静态方法~~
> 2、3合并成一条: 访问or调用 类/接口的 【静态变量、静态方法、给静态变量赋值】;
>  4、 反射(如 Class.forName(“com.gx.yichun”))
>  5、 初始化某个类的子类,则其父类也会被初始化  【注意是如果子类走到初始化的化,其父类也会执行初始化,如果子类仅仅是走到【准备阶段】,那父类无所谓的】
>  6、 Java虚拟机启动时被标明为启动类的类( JavaTest ),还有就是Main方法的类会 首先被初始化
>      最后注意一点对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承、多态中最为明显!为了方便理解下文会陆续通过例子讲解 (这个解释就是:如果父类定义了 f_age 静态变量,子类继承了父类,但是用的时候访问的是son.f_age,由于子类没有直接定义f_age这个变量,所以访问的父类的静态变量,所以完成了父类的初始化,子类的初始化可能压根没触发!! 后续又实例)

**实际上Java代码编译成字节码之后,最开始是没有构造方法的概念的,只有类初始化方法 和 对象初始化方法 。**<br />**1 类初始化方法**:编译器会按照其出现顺序,收集:**类变量(static变量)的赋值语句**、**静态代码块**,最终组成**类初始化方法**。类初始化方法一般在**类初始化**的时候执行。即:类初始化就是执行类变量代码和类静态代码块!  如果没有显式调用类的构造函数的化,对象初始化(构建对象)是不会执行的;<br />**2 对象初始化方法**:编译器会按照其出现顺序,收集:**成员变量的赋值语句**、**普通代码块**,最后收集**构造函数**的代码,最终组成**对象初始化方法**,值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在**实例化类对象**的时候执行。
<a name="lETPi"></a>
#### 6 使用
<a name="n2g3w"></a>
#### 7 卸载
<a name="KZ8M0"></a>
#### 8 生命周期结束
<a name="fBnQM"></a>
#### 9 接口的加载过程(与类的加载略有差异)
接口加载过程与类加载过程稍有不同。<br />当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会初始化。
<a name="Jd9Sn"></a>
####  10 示例题
上述规则了解之后,有几个面试题了解下
<a name="rBn6s"></a>
##### 1 第一题
```java
class Father2{
    public static String strFather="HelloJVM_Father";
    static{
        System.out.println("Father静态代码块");
    }
}

class Son2 extends Father2{
    public static String strSon="HelloJVM_Son";
    static{
        System.out.println("Son静态代码块");
    }
}

public class InitativeUseTest2 {
    public static void main(String[] args) {
       System.out.println(Son2.strSon);
    }
}

结果解析:

由于Son2.strSon是调用了Son类自己的静态方法属于主动使用,所以会初始化Son类,又由于继承关系,类继承原则是初始化一个子类,会先去初始化其父类,所以会先去初始化父类! 运行结果: Father静态代码块 Son静态代码块 HelloJVM_Son

2 第二题
class YeYe{
    static {
        System.out.println("YeYe静态代码块");
    }
}

class Father extends YeYe{
    public static String strFather="HelloJVM_Father";
    static{
        System.out.println("Father静态代码块");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";
    static{
        System.out.println("Son静态代码块");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather); 
    }
}

结果解析

首先看到Son.strFather,你会发现是子类Son访问父类Father的静态变量strFather;这个时候就千万要记住我在归纳主动使用概念时特别提到过的一个注意点了:对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),这句话在继承、多态中最为明显;Son.strFather中的静态字段是属于父类Father的对吧,也就是说直接定义这个字段的类是父类Father,所以在执行 System.out.println(Son.strFather); 这句代码的时候会去初始化Father类而不是子类Son!是不是一下子明白了?

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

3 第三题
class YeYe{
    static {
        System.out.println("YeYe静态代码块");
    }
}

class Father extends YeYe{
    public final static String strFather="HelloJVM_Father";
    static{
        System.out.println("Father静态代码块");
    }
}

class Son extends Father{
    public static String strSon="HelloJVM_Son";
    static{
        System.out.println("Son静态代码块");
    }
}

public class InitiativeUse {
    public static void main(String[] args) {
        System.out.println(Son.strFather);
    }
}

结果分析

这个题唯一的特点就在于final static !是的Son.strFather所对应的变量便是final static修饰的,依旧是在本篇文章中归纳的类的主动使用范畴第二点当中:访问某个类或接口的静态变量,或者对该静态变量赋值(凡是被final修饰的,不不不,其实更准确的说是在编译器把结果放入常量池的静态字段除外);即:一旦某个静态变量被 static final 修饰了, 这个变量的赋值再准备阶段就完成了,没有必要走到初始化不走,调用这样的静态变量也不会触发初始化过程; 所以,这个题并不会初始化任何类,当然除了Main方法所在的类!于是仅仅执行了System.out.println(Son.strFather);所以仅仅打印了Son.strFather的字段结果HelloJVM_Father,

运行结果:HelloJVM_Father

4 第四题
package com.jvm.classloader;
import sun.applet.Main;
import java.util.Random;
import java.util.UUID;

class Test{
    static {
        System.out.println("static 静态代码块");
    }

//    public static final String str= UUID.randomUUID().toString();
    public static final double str=Math.random();  //编译期不确定
}


public class FinalUUidTest {
    public static void main(String[] args) {
        System.out.println(Test.str);
    }
}

代码解析

上面这个程序完全说明本篇文章中归纳的类的主动使用范畴第二点当中的这句话:凡是被final修饰的,不不不,其实更准确的说是在编译器把结果放入常量池的静态字段除外

分析:其实final不是重点,重点是编译器把结果放入常量池!当一个常量的值并非编译期可以确定的(即:虽然Test.random 是static final 修饰了,但是它的值在编译期并不知道结果,所以这个值没有放在常量池中;后续调用的时候也就没有被排除在主动使用的场景之外,即:这里是主动使用,即:需要初始化;需要初始化, 就需要执行static代码块),那么这个值就不会被放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,所以这个类会被初始化 运行结果: static 静态代码块 0.7338688977344875


2 理解【首次主动使用】

上面提到过Java程序对类的使用方式可分为两种:主动使用与被动使用。一般来说只有当对类的首次主动使用的时候才会导致类的初始化,其中首次关键字很重要,因此特地用一小结将其讲解!

package com.jvm.classloader;

class Father6{
    public static int a = 1;
    static {
        System.out.println("父类粑粑静态代码块");
    }
}
class Son6{
    public static int b = 2;
    static {
        System.out.println("子类熊孩子静态代码块");
    }
}

public class OverallTest {
    static {
        System.out.println("Main方法静态代码块");
    }

    public static void main(String[] args) {
        Father6 father6;
        System.out.println("======");

         father6=new Father6();
        System.out.println("======");

        System.out.println(Father6.a);
        System.out.println("======");

        System.out.println(Son6.b);

    }
}

代码解释

运行结果:

    Main方法静态代码块
    ======
    父类粑粑静态代码块
    ======
    1
    ======
    子类熊孩子静态代码块
    2

首先根据主动使用概括的第六点:Main方法的类会首先被初始化。 所以最先执行Main方法静态代码块,而 Father6 father6;只是声明了一个引用不会执行什么,当运行到father6=new Father6();的时候,看到关键字new并且将引用father6指向了Father6对象,说明主动使用了,所以父类Father6将被初始化,因此打印了:父类粑粑静态代码块 ,之后执行 System.out.println(Father6.a);属于访问静态变量所以也是主动使用,这个时候注意了,因为在上面执行father6=new Father6();的时候父类已经主动使用并且初始化过一次了,这次不再是首次主动使用了,所以Father6不会在被初始化,自然它的静态代码块就不再执行了,所以直接打印静态变量值1,而后面的System.out.println(Son6.b);同样,也是只初始化自己,不会去初始化父类,只因为父类Father6以及不再是首次主动使用了!明白了没?


3 类加载器(🌟🌟🌟🌟🌟)

我们之前讲的类加载都是给类加载器做的一个伏笔,在这之前讲的所有类被加载都是由类加载器来完成的,可见类加载器是多么重要。由于上面的面试题并不涉及类加载器的相关知识,所以到这里再涉及涉及类加载器的知识!
类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载入JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。

关于唯一标识符: 在Java中,一个类用其全限定类名(包括包名和类名)作为标识; 但在JVM中,一个类用其全限定类名 和 其类加载器作为其唯一标识。

类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),如下:image.png
站在Java开发人员的角度来看,

1 类加载器可以大致划分为以下三类 +一个特殊的

  1. 启动类加载器: BootstrapClassLoader,启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,负责加载存放在 JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被 BootstrapClassLoader加载)。启动类加载器是无法被Java程序直接引用的。总结一句话:启动类加载器加载java运行过程中的核心类库JRE\lib\rt.jar, sunrsasign.jar, charsets.jar, jce.jar, jsse.jar, plugin.jar 以及存放在JRE\classes里的类,也就是JDK提供的类等常见的比如:Object、Stirng、List…

  2. 扩展类加载器: ExtensionClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 JDK\jre\lib\ext目录中,或者由 java.ext.dirs系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器: ApplicationClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器总结一句话:应用程序类加载器加载CLASSPATH变量指定路径下的类 即指你自已在项目工程中编写的类

  4. 线程上下文类加载器:除了以上列举的三种类加载器,其实还有一种比较特殊的类型就是线程上下文类加载器。类似Thread.currentThread().getContextClassLoader()获取线程上下文类加载器,线程上下文加载器其实很重要,它违背(破坏)双亲委派模型,很好地打破了双亲委派模型的局限性,尽管我们在开发中很少用到,但是框架组件开发绝对要频繁使用到线程上下文类加载器,如Tomcat等等…

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,因为JVM自带的类加载器(ClassLoader)只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

1、在执行非置信代码之前,自动验证数字签名。 2、动态地创建符合用户特定需要的定制化构建类。 3、从特定的场所取得java class,例如数据库中和网络中。

需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机默认采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面将会详细讲到!
下面看一段程序。

package com.jvm.classloaderQi;

public class ClassloaderTest {
    public static void main(String[] args) {
        //获取ClassloaderTest类的加载器
        ClassLoader classLoader= ClassloaderTest.class.getClassLoader(); 

        System.out.println(classLoader);
        System.out.println(classLoader.getParent()); //获取ClassloaderTest类的父类加载器
        System.out.println(classLoader.getParent().getParent());//获取ClassloaderTest类的父类加载器的父类加载器
    }
}

运行结果:

sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@1b6d3586 null 从上面的结果可以看出,并没有获取到ExtClassLoader的父加载器(Loader),原因是Bootstrap Loader(启动类加载器)是用C++语言实现的(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),找不到一个确定的返回父Loader的方式,于是就返回null。至于$符号就是内部类的含义。


2 java虚拟机入口应用:sun.misc.Launcher

到这里,我为什么要讲Launcher呢?如果你没有接触过这个Launcher类可能你会特别陌生,但是它却特别重要!为啥说它重要呢,不知道你有没有想过为啥类加载器首先会去到应用程序类加载器 ApplicationClassLoader,是的!我在介绍应用程序类加载器 ApplicationClassLoader和扩展类加载器 ExtensionClassLoader的时候就已经提到过这两个类加载器是由sun.misc.Launcher实现的!为了更好的理解,我们可以查看Launcher源码:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(loader);
    }

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader {}

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader {}

当然这份源码是从OpenJDK上找到的,并不是所有的JDK源码都开源,如果不知道啥是OpenJDK的童鞋可以自行谷歌一下,OpenJDK中有很多JDK不开源的开源代码,当然也可以直接在idea中找到Launcher的源码,只不过由源码是由Intelliu IDEA反编译器提供的,里面一些变量都是以var+数字组成,其代码差别并不大,好了不扯了,虽然博主还能在哔哔几句。

通过以上源码我们可以得到相关的信息:
1、Launcher初始化了ExtClassLoader和AppClassLoader,首先是创建了Extcl扩展类加载器
2、之后的App应用类【系统类】加载器作为Launcher中的一个成员变量,至于为啥不把Extcl扩展类加载器也做为成员变量的原因,大家可以仔细想一想,是为啥呢?其实很简单,因为没必要,因为直接把App系统加载器.parent()方法即可得到Extcl扩展类加载器!
3、Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty(“sun.boot.class.path”)得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。我们可以用输出语句代码测试一下sun.boot.class.path是什么内容,其实就是JRE目录下的jar包或者是class文件。

System.out.println(System.getProperty("sun.boot.class.path"));
结果
D:\program_my\java8\jdk1.8.0_271\jre\lib\resources.jar;D:\program_my\java8\jdk1.8.0_271\jre\lib\rt.jar;D:\program_my\java8\jdk1.8.0_271\jre\lib\sunrsasign.jar;D:\program_my\java8\jdk1.8.0_271\jre\lib\jsse.jar;D:\program_my\java8\jdk1.8.0_271\jre\lib\jce.jar;D:\program_my\java8\jdk1.8.0_271\jre\lib\charsets.jar;D:\program_my\java8\jdk1.8.0_271\jre\lib\jfr.jar;D:\program_my\java8\jdk1.8.0_271\jre\classes

3 ExtClassLoader、AppClassLoader源码

如果想更进一层次的理解或者足够的好奇心,可以看ExtClassLoader、AppClassLoader源码

我们可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径,有兴趣的童鞋可以进行测试。

源码中可以看到AppClassLoader加载的就是java.class.path下的路径

4 小结:

从上面源码也可以看出,父加载器并不是指其父类,ExtClassLoaderAppClassLoader同样继承自URLClassLoader,这个时候小白童鞋就不耐烦了:那为啥调用AppClassLoader.getParent()方法会得到ExtClassLoader的实例呢?

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

public abstract class ClassLoader {

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
    // @GuardedBy("ClassLoader.class")
private static ClassLoader scl;

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    ...
}
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
    if (parent == null)
        return null;
    return parent;
}
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    return scl;
}

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            //通过Launcher获取ClassLoader
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}
}

从上面的源码可以看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:

1、由外部类创建ClassLoader时直接指定一个ClassLoader为parent。
2、由getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。简单的说,就是一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。


4 命名空间

我觉得讲类加载器,还是很有必要知道命名空间这个概念!实际上类加载器的一个必不可少的前提就是命名空间!

命名空间概念: 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。 特别注意: 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。 由子加载器加载的类能看见父加载器的类,由父亲加载器加载的类不能看见子加载器加载的类

我们已经知道每个类只能被加载一次,其实这样说是不够准确的,怎样才算是准确的呢?那就涉及到命名空间的概念了!只有在相同的命名空间中,每个类才只能被加载一次,反过来说就是一个类在不同的命名空间中是可以被加载多次的,而被加载多次的Class对象是互相独立的

1如何理解


5 jvm加载机制

JVM的类加载机制主要有如下3种。

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

  2. 父类委托(这个最优先):先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类,通俗讲就是儿子们都他么是懒猪,自己不管能不能做,就算能加载也先不干,先给自己的父亲做,一个一个往上抛,直到抛到启动类加载器也就是最顶级父类,只有父亲做不了的时候再没办法由下一个子类做,直到能某一个子类能做才做,之后的子类就直接返回,实力坑爹!

  3. 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效


6 双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。也就是实力坑爹!(即:相当于jvm中的加载机制中的父类委托优先)

双亲委派机制:

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

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

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                  } else {
                  //如果没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  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;
      }
  }

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

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


7 classloader源码分析(挺好玩的)

ClassLoader类是一个抽象类,所有的类加载器都继承自ClassLoader(不包括启动类加载器),因此它显得格外重要,分析ClassLoader抽象类也是非常重要的!
简单小结一下ClassLoader抽象类中一些概念:
二进制概念(Binary name):格式如下
image.png
把二进制名字转换成文件名字,然后在文件系统中磁盘上读取其二进制文件(class文件),每一个class对象都包含了定义了这个类的classload对象,class类都是由类加载器加载的,只有数组类型是有JVM根据需要动态生成。

特别注意数组类型

1、 数组类的类对象不是由类加载器创建的,而是根据Java运行时的需要自动创建的。 2、 数组类的类加载器getClassLoader()与它的元素类型的类加载器相同;如果元素类型是基本类型,则数组类没有类加载器也就是null,而这个null不同于根类加载器返回的null,它是单纯的null。

到这里,下面就主要分析ClassLoader抽象类中几个比较重要的方法。

1.loadClass

该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作:

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                  } else {
                  //如果没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  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;
      }
  }

正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass(“className”),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

2.findClass

在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下:

//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

3.defineClass(byte[] b, int off, int len)

defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
      // 获取类的字节数组
      byte[] classData = getClassData(name);  
      if (classData == null) {
          throw new ClassNotFoundException();
      } else {
          //使用defineClass生成class对象
          return defineClass(name, classData, 0, classData.length);
      }
  }

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

4.resolveClass (Class<?>c)

使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

5.ClassLoader小结

以上上述4个方法是ClassLoader类中的比较重要的方法,也是我们可能会经常用到的方法。接看SercureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联,前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能,在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

检查完父类加载器之后loadClass会去默认调用findClass方法,父类(ClassLoader)中的findClass方法主要是抛出一个异常。
findClass根据二进制名字找到对应的class文件,返回值为Class对象Class<?>
defineClass这个方法主要是将一个字节数组转换成Class实例,会抛三个异常,但只是threws一个,因为其他两个是运行时异常。
loadClass方法是一个加载一个指定名字的class文件,调用findLoadedClass (String)检查类是否已经加载…如果已经加装就不再加载而是直接返回第一次加载结果 所以一个类只会加载一次


8 自定义加载器

这部分内容跟第7章内容相关,其实就是继承jdk提供的一些classload并实现一些方法;
自定义核心目的是扩展java虚拟机的动态加载类的机制,JVM默认情况是使用双亲委托机制,虽然双亲委托机制很安全极高但是有些情况我们需要自己的一种方式加载,比如应用是通过网络来传输 Java类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。因此自定义类加载器也是很有必要的。

自定义类加载器一般都是继承自 ClassLoader类,从上面对 loadClass方法来分析来看,我们只需要重写 findClass 方法即可。自定义加载器中点:重写findClass,下面直接看自定义类加载器代码的流程:

package com.yichun.classloader;
import java.io.*;

public class MyClassLoader extends ClassLoader {
    private String root;

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\dirtemp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("com.yichun.classloader.Demo1");
            Object object = testClass.newInstance();
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密。上面代码程序只是简单Demo,并未对class文件进行加密,因此省略了解密的过程。这里有几点需要注意:

1、这里传递的文件名需要是类的全限定性名称,即com.yichun.test.classloading.Test格式的,因为defineClass 方法是按这种格式进行处理的。 2、最好不要重写loadClass方法,因为这样容易破坏双亲委托模式。 3、这类Test 类本身可以被 AppClassLoader类加载,因此我们不能把com/yichun/test/classloading/Test.class放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载。


9 加载类的三种方式

到这里,相信大家已经对类的加载以及加载器有一定的了解了,那么你知道吗,其实加载类常见的有三种方式,如下:

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

1.三种方式的区别:

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

2.两种异常(exception)

1、静态加载的时候如果在运行环境中找不到要初始化的类,抛出的是NoClassDefFoundError,它在JAVA的异常体系中是一个Error
2、动态态加载的时候如果在运行环境中找不到要初始化的类,抛出的是ClassNotFoundException,它在JAVA的异常体系中是一个checked异常

3.理解Class.forName

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

Class.forName()是一个静态方法,同样可以用来加载类,Class.forName()返回与给定的字符串名称相关联类或接口的Class对象。注意这是一种获取Class对象的方法

@CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

//  native 本地方法
 private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

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

4.Class.forName与ClassLoader.loadClass区别

image.png
首先,我们必须先明确类加载机制的三个过程主要是:加载 —> 连接 —> 初始化

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

来看一段程序:

package com.binc.testspring.study.copilot;


/**
 * FileName: Test   测试copilot插件的强大
 * Autho: binC
 * Date:  2022/4/2 15:39
 */
public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader=Test.class.getClassLoader();
        //1、使用ClassLoader.loadClass()来加载类,不会执行初始化块
        classLoader.loadClass("com.binc.testspring.study.copilot.Demo");
        System.out.println("11111111111111");

        //2、使用Class.forName()来加载类,默认会执行初始化块
        Class.forName("com.binc.testspring.study.copilot.Demo");
        System.out.println("22222222222222");
        //3、使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
        Class.forName("com.binc.testspring.study.copilot.Demo",false,classLoader);
        System.out.println("33333333333333");
    }

}

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

执行结果
11111111111111
static 静态代码块
22222222222222
33333333333333

10 总结:

image.png
首先,我们必须先明确类加载机制的三个过程主要是:加载 —> 连接 —> 初始化

1 类的加载 连接 初始化

1、加载:查找并加载类的二进制数据到java虚拟机中 2、 连接验证: 确保被加载的类的正确性 准备:为类的静态变量分配内存,并将其初始化为默认值,但是到达初始化之前类变量都没有初始化为真正的初始值(如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。) 解析:把类中的符号引用转换为直接引用,就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程 3、 初始化:为类的静态变量赋予正确的初始值 (类的初始化,并非对象的初始化) 类从磁盘上加载到内存中要经历五个阶段:加载、连接、初始化、使用、卸载

2 Java程序对类的使用方式可分为两种

(1)主动使用
(2)被动使用
所有的Java虚拟机实现必须在每个类或接口被Java程序“首次主动使用”时才能初始化他们
主动使用
(1)创建类的实例 (new的时候)
(2)访问某个类或接口的静态变量 ,或者对该静态变量赋值 (这里有特殊情况,即 final关键字)
(3)调用类的静态方法
(4)反射(Class.forName(“com.test.Test”))
(5)初始化一个类的子类是,会初始化其父类
(6)Java虚拟机启动时被标明启动类的类以及包含Main方法的类
(7)JDK1.7开始提供的动态语言支持(了解)

被动使用
除了上面七种情况外,其他使用java类的方式都被看做是对类的被动使用,都不会导致类的初始化


11 特别注意

初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器(),之后初始化对象构造器()。

初始化类构造器:JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行。(即:类初始化只负责初始化类变量和静态代码块)
初始化对象构造器:JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。值得特别注意的是,如果没有监测或者收集到构造函数的代码,则将不会执行对象初始化方法。对象初始化方法一般在实例化类对象的时候执行。(对象初始化: 负责的是对成员变量 普通代码块 构造方法的执行;并且严格遵循 普通变量—普通代码块—构造方法的顺序,且:如果没有明确的调用构造方法的化,对象初始化不会执行的;)

如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。