【博学谷学习记录】超强总结,用心分享|JVM类加载篇

类加载器 ClassLoader 概念


类加载器他是JVM的核心组件,他主要就是把外部的class类加载到内存中,加载到内存后该class类才可以执行,
类加载器会获取class类二进制数据流然后把其装载到系统后,在将其交给JVM进行,链接,初始化等操作,形成可以直接被JVM使用的类型

深化理解
假如有一个 Cat 类,现在通过 “new” 来创建对象:

     Cat cat = new Cat();
     这是我们最常见的使用方式,但你有没有思考过这一行代码的背后到底发生了什么?

在这里插入图片描述

  1. 类加载器执行步骤 :
    主要分为三个步骤:
    第一步 :
    代码编译: JAVA 编译器把代码编译为 Class 字节码文件
    第二步:
    类加载器会将我们的class字节码转换为Class对象"Class"
    第三步:
    JVM通过Class对象"Class“实例化出实例对象"Cat”
  2. 类加载器调用时机:
    1, 在构造类实例的时候,例如通过 “new” 构建对象,或者通过反射方式构造;
    2,调用类的静态方法、静态属性的时候;
    3,如果你使用的类是个子类,那么在使用子类的时候,其父类就需要被加载。
    上面内容让我们了解了类加载器的概念下面我们来进一步认识下他

类加载器都有哪些类型?它们各自负责什么

类加载器有 4 种类型:

1,BootStrapClassLoader 根类加载器 负责加载 JAVA 的核心类,例如 java.lang 这个包。

2, ExtClassLoader 扩展类加载器 负责加载扩展类,例如 javax.* 下的包。

3, AppClassLoader 系统类加载器 负责加载 classpath 指定的类。

4, Custom ClassLoader 自定义加载器 为我们提供定制化加载的能力。

深化理解下
在这里插入图片描述
1), BootStrapClassLoader
根类加载器,是内嵌在 JVM 内核中的,负责加载的是 “jre/lib” 目录下的核心类,也可以通过 Xbootclasspath 选项来指定。
这个加载器是不能被我们的应用程序直接调用的,例如 HotSpot JVM 中 BootStrapClassLoader 是使用 C++ 写的。

2), ExtClassLoader
扩展类加载器,就是我们可见的了,负责加载 “jre/lib/ext” 等目录下的扩展类。
这个扩展目录也可以通过系统属性 “java.ext.dirs” 来指定;如果没有指定,就默认加载 “jre/lib/ext”。
我们可以通过代码查看一下这个属性:

public class ExtDir {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.ext.dirs"));
    }
}

我们下面看下这块代码的输出信息 :
在这里插入图片描述
可以看到,这是一连串的路径,通过 “:” 来分隔的,这样我们就知道 ExtClassLoader 可以加载哪些目录下的类了。
现在请你思考一个小问题,加载这些类的时候是一次性加载所有吗?,还是用到时候在加载?
3), AppClassLoader
系统类加载器,负责加载 classpath 下的类,这应该是我们很熟悉的,我们 JAVA 设置环境变量的时候就会指定 classpath。

如下图,在 AppClassLoader 源码中我们可以看到,它加载的就是 classpath。

在这里插入图片描述
我们还是通过代码来输出一下这个属性的内容:

public class AppClasspath {
    public static void main(String[] args) {
        System.out.println(System.getProperty("java.class.path"));
    }
}`

输出信息如下:
在这里插入图片描述
内容太多,我只截取了一部分,可以看到,这也是是一连串的路径,其中最重要的就是我们项目下的编译路径。

4), Custom ClassLoader
自定义加载器,就比较有意思了,我们可以定义自己的类加载器,下面给大家演示一个案例
先带大家了解两个方法 :
4.1)Class<?> findClass(String name)
根据名称加载 Class 字节码。

4.2)Class<?> defineClass(String name, byte[] b, int off, int len)
解析 Class 字节流。
加载的时候,会先调用 findClass 找到类,然后调用 defineClass 解析,返回 Class 对象。
测试类代码如下:

package com.test;

public class TestClass {
    static {
        System.out.println("hello my classloader");
    }
}

这里我们只定义了一个静态代码块儿,加载此类时会执行其中代码,可以用来验证是否加载成功。

自定义类加载器代码如下:

package com.test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class CustomClassLoaderDemo extends ClassLoader{
    private String path;
    private String classLoaderName;
    public CustomClassLoaderDemo(String path, String classLoaderName){
        this.path = path;
        this.classLoaderName = classLoaderName;
    }

    @Override
    public Class findClass(String name){
        name = path + name + ".class";
        InputStream inputStream = null;
        ByteArrayOutputStream outputStream = null;
        try{
            // 加载二进制流
            inputStream = new FileInputStream(new File(name));
            outputStream = new ByteArrayOutputStream();
            int i = 0;
            while ((i = inputStream.read()) != -1){
                outputStream.write(i);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                outputStream.close();
                inputStream.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        byte[] b=  outputStream.toByteArray();
        return defineClass(name, b, 0, b.length);
    }

}

这里我们只重写了 findClass,把文件流写入二进制数组,然后调用 defineClass 生成 Class 对象。这样就完成了类加载器的定义。

验证代码如下:

package com.test;

public class ClassLoaderChecker {
    public static void main(String[] args) throws Exception{
        // 目标class路径(改为自己的路径)
        String path = "/Users/dys/IdeaProjects/jvmclassloader/out/production/jvmclassloader/com/test/";
        CustomClassLoaderDemo customClassLoaderDemo = new CustomClassLoaderDemo(
                path,
                "myClassLoader");
        Class c= customClassLoaderDemo.loadClass("com.test.TestClass");
        System.out.println(c.getClassLoader());
        c.newInstance();
    }
}

运行后输出信息如下:

在这里插入图片描述
输出了类加载器对象信息,还有测试类中静态代码块儿的输出内容,说明我们自定义的类加载器正常工作了。

双亲委派模式

前面我们了解了类加载器的4种类型,它们各自负责加载不同位置的类,那现在就有个问题,这些类型的加载器,它们是如何协作的?
下面先带大家了解下:

双亲委派模式定义了4种类加载器的层级与协作流程。

  • 当一个类加载器收到类加载任务后,如果自己没有加载过此类,就会交给其父类加载器去完成。在向上传递过程中,每个加载器都会查看自己是否加载过此类。
    如果都没加载过,那么最终加载任务一定会向上传递到顶层的 BootStrapClassLoader 根类加载器。
    如果顶层加载器无法加载,则会向下传递加载任务,直到找到可以完成加载任务的类加载器。 如果最后谁都加载不了,就会抛出
    ClassNotFoundException 异常。 下面我们在结合流程图给大家详细讲解下

在这里插入图片描述

  • 双亲委派模式其实很简单,你可以理解为是一种 ”推诿“。

    比如,老师给孩子安排了一项家庭任务,孩子懒得做,就叫爸爸帮忙做,爸爸也懒得做,就去找爷爷做。

    爷爷能做的话就做了,实在做不了就踢回给爸爸;爸爸一看爷爷不做,那就自己试试,能做就做,否则就踢回给孩子;孩子一看实在没人帮他了,只能自己做了

下面我们再看看类加载的源码:
在这里插入图片描述

  • 从代码中我们就可以看到双亲委派这个逻辑:

    第一个红线代码,表示加载的时候,先看看自己是否已经加载过,如果加载过,就直接返回了;

    第二个红线代码,表示如果 parent 不为空,就让父加载器去加载;

    第三个红线代码,表示如果 parent 为空,说明已经到了顶级的类加载器,就使用 BootstrapClassLoader 去加载。

那么你先看肯定会有疑问了
”为什么要使用这种加载模式呢?直接加载不好吗?“

  • 使用双亲委派加载模式,是出于两方面的思考:

    1)避免类的重复加载

    如果不使用双亲委派,各自加载,那么一个类就完全可能被加载多次,产生多个 Class 对象,这不就浪费宝贵的内存空间了吗。

    2)防止核心 API 被篡改

    比如用户自己开发了一个 String 类,与核心库中的 String 类重名了。

    如果不使用双亲委派,那么先加载的可能是用户开发的 String 。此时那些想要调用核心类库中 String 的代码,实际调用不就是这个假的
    String 了吗?这是不是就出现了安全隐患啊。

类加载的工作流程

了解类加载器之后,现在我们就要研究最核心的问题了–看看类加载器到底是怎么工作的?

  • 类加载过程分为以下几个阶段:

    1)加载 – 加载 Class 文件到内存,创建一个 Class 对象。

    2)验证 – 验证类文件内容,确保符合 JVM 标准,以及安全性。

    3)准备 – 内存分配,设置初始值。

    4)解析 – 符号引用转直接引用。

    5)初始化 – 完成静态块执行与静态变量的赋值。

深化理解

  • 其实,类的【加载过程】大体上分为3步:

    1)加载

    2)链接

    3)初始化

在这里插入图片描述

  • 而第二步 ”链接“ 内部又包含了3个子步骤:

    1)验证

    2)准备

    3)解析
    在这里插入图片描述

  • 所以整体上类加载过程共分为了5步:

    1)加载

    2)验证

    3)准备

    4)解析

    5)初始化
    在这里插入图片描述1)【加载】-- 是把文件加载到内存的过程。

  • 通过类的名字查找此类字节码文件,并根据字节码文件创建一个 Class 对象。
    注意:只要是加载类,系统都会为其建立一个 java.lang.Class 对象。

    2)【验证】-- 对被加载类的内部结构进行验证。

  • 目的是确保 Class 文件符合 JVM 的要求,保障 JVM 的安全。
    主要包括四种验证:
    文件格式验证,看是否符合 Class 格式规范。 元数据验证,分析字节码的描述信息,是否符合语法规范。 字节码验证,分析整体语义是否合法,逻辑是否正常。 符号引用验证,为后面解析阶段做好准备,保障解析可以顺利执行。

    3)【准备】-- 进行内存分配,为类变量分配内存,并设置初始值。

    注意:这里的初始值是指 ”0“ 或者 ”null“,而不是代码中设置的具体值,具体值的设置是初始化阶段的工作。

  • 此处还有2个概念需要注意一下:

    类变量 ,指类中由 static 修饰的变量。 成员变量 ,指类中非 static 的变量。

  • 类变量是加载类时就存在了,无需创建对象就可以调用,是类层面的。
    而成员变量则只能通过对象调用,实例化对象的时候才存在,是对象层面的。

下面我们举个例子:

public String a = "abc";
  • 请思考:变量 ”a“ 此时会被分配内存吗?
    答案:不会的,因为它不是类变量。

我们再看一个例子:

public static String d = "ddd";
  • 变量 ”d“ 会被分配内存吗?会的,因为它是类变量。
    那么它会分配什么值呢?是 ”ddd“ 吗?
    不是的,因为在准备阶段是不会分配具体值,所以 ”d“ 此时分配的是 ”null“

再来一个例子:

public static final String f = "xyz";
  • 变量 ”f“ 是类变量,但它还是 final 的,那这种情况下会怎么分配内存呢?
    对于 final 的变量,在准备阶段就直接设置具体值了,因为它后面是不会改变的。
    好,通过以上几个例子,你是不是就明白准备阶段是如何分配内存的了。

4)【解析】-- 对字段、接口、方法进行解析,将常量池中的符号引用变为直接引用。

  • 直接引用就是直接指向目标的指针、相对偏移量等。

    那这里的 【符号引用】、【直接引用】是什么意思呢?

在这里插入图片描述

  • 比如,类 A 中有一个变量,引用了类 B。实际运行的时候,B 就得有一个内存地址吧,然后变量 ”objectB“
    就指向这个地址。但是,在代码编译的时候,还没运行呢,B 有内地址吗?

    答案是没有,那此时变量 ”objectB“ 引用哪儿呢?

    编译器是没办法提供真实地址的,所以只能用一个符号来代替,这就叫 【符号引用】。而到了加载的解析阶段,A B 都已经进入了内存,B
    已经有真实的内存地址了,所以,现在就要把之前的 【符号引用】 替换掉,改为真实地址,这就是 【直接引用】。

5)【初始化】
这一步就很好理解了,就是为类变量设置实际值,有静态代码块儿的话,就开始执行。

注意:如果此类是有父类的,并且父类还没有执行初始化,则优先初始化父类。

前面几个小章节理解之后,类加载这块儿的主要知识就差不多了,下面在带大家了解下类加载方式

类加载方式有哪些

  • 类的加载方式可以分为2类:

    1)隐式加载

    使用 new 构建对象的方式。

    2)显示加载

    使用 loadClass、forName 的方式。

    注意:loadClass 和 forName 是有区别的,loadClass 得到的是还没有经过链接的 Class,forName
    得到的是已经初始化完成的 Class。

    相比来讲,类加载的2种方式new 的方式是最常用的。创建对象实例的时候,如果 Class 还没有被加载,则会自动加载,所以称为隐式加载。

    loadClass、forName 这种方式只是加载类,并没有生成对象实例,想要对象的话,还需要调用 newInstance()
    方法,所以称为显示加载。

深化理解

下面咱们重点看看 loadClass 与 forName 的区别。

可以先回顾一下类的加载过程:
在这里插入图片描述
loadClass 只会执行 ”加载“ 这个过程,后面2个都不会执行。
forName 会把这3个过程都执行完。

下面是 LoadClass 的源码:
在这里插入图片描述

  • 这是 Java 中类加载器 ClassLoader 的源码,”loadClass(String name) “ 这个方法调用了
    ”loadClass(String name, boolean resolve)“ 方法。其第二个参数 ”resolve“ 值为
    ”false“。
    如果参数 ”resolve“ 为 true,表示会执行 ”resolveClass()“ 方法,否则不会执行

下面是 ”resolveClass()“ 方法的代码:

在这里插入图片描述

  • 注释中说明了,这个方法是用来链接 Class 的。

    这样咱们就理解 loadClass 的工作了吧,它只是做了加载流程中的第一步(加载),不会执行第二步链接,更加不会执行第三步的初始化。

下面咱们继续看 ”forName“ 方法的源码:
在这里插入图片描述

  • ”forName(String className)“ 方法调用了 ”forName0“ 方法,其中有一个参数
    ”initialize“,表示 ”是否执行初始化“,传递的值为 true,表示执行初始化。 现在就很清晰了,forName 是完成了
    Class 初始化的,也就是类加载过程中的3个步骤都完成了。

到此,JVM 类加载基本完毕希望本篇文章能对你们学习上有所帮助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值