• JVM类加载器(详解)


    JVM类加载器

    1.类加载子系统的作用

    类加载

    类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识。

    2.类加载过程

    当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成3个步骤,所以有时也把这3步骤统称为类加载或类初始化。

    类被加载到 JVM 开始,到卸载出内存,整个生命周期如图:

    生命周期

    1.加载
    1. 通过类名(地址)获取此类的二进制字节流。
    2. 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构。
    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口。
    2.链接

    就是将已经读入内存的类的二进制数据合并到JVM运行时环境中去,包含以下步骤:

    1. 验证

      检验被加载的类是否有正确的内部结构,并和其他类协调一致。

    2. 准备

      准备阶段则负责为类的静态属性分配内存,并设置默认初始值;不包含final修饰的static实例变量,在编译时进行初始化。不会为实例变量初始化。

    3. 解析

      将类的二进制数据中的符号引用替换成直接引用。

    3.初始化

    类什么时候初始化?

    1. 创建类的实例,new对象
    2. 访问某个类或接口的静态变量,或者对该静态变量赋值
    3. 调用类的静态方法
    4. 反射(Class.forName(" "))
    5. 初始化一个类的子类(首先会初始化子类的父类)
    6. JVM启动时标明的启动类,即文件名和类名相同的那个类

    注意:对于一个final类型的静态变量,如果该变量的值在编译时就可以确定下来,那么这个变量相当于“宏变量”。Java编译器会在编译时直接把这个变量出现的地方替换成它的值,因此即使程序使用该静态变量,也不会导致该类的初始化。反之,如果final类型的静态Field的值不能在编译时确定下来,则必须等到运行时才可以确定该变量的值,如果通过该类来访问它的静态变量,则会导致该类被初始化。

    类的初始化顺序

    对static修饰的变量或语句块进行赋值。

    如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

    如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。

    顺序是:父类static -> 子类static -> 父类构造方法 -> 子类构造方法

    3.类加载器分类

    JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)

    无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个:

    加载器

    1. 自定义类加载器(User-Defined ClassLoader)

      从概念上来讲,自定义类加载器一般指的是程序汇总有开发人员自定义的一类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

      1. 扩展类加载器(Extension ClassLoader)

        Java语言编写的,由sun.misc.Launcher$ExtClassLoader实现,父类加载器为null。

        派生于ClassLoader类。

        上层类加载器为引导类加载器。

        它负责加载JRE的扩展目录。

        从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK系统安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的jar放在此目录下,也会自动由扩展类加载器加载。

      2. 应用程序类加载器(系统类加载器 Application ClassLoader)

        Java语言编写的,由sun.misc.Launcher$AppClassLoader实现,父类加载器为ExtClassLoader。

        派生于ClassLoader类。

        上层类加载器为扩展类加载器。

        加载我们自己定义的类。

        该类加载器是程序中默认的类加载器。

        通过类名.class.getClassLoader(),ClassLoader.getSystemClassLoader()来获得。

    2. 引导类加载器(启动类加载器/根类加载器)(Bootstrap ClassLoader)

      这个类加载器使用C/C++语言实现,嵌套在JVM内部。用来加载Java核心类库。

      并不继承于java.lang.ClassLoader没有父加载器。

      负责加载扩展类加载器和应用类加载器,并为它们指定父类加载器。

      出于安全考虑,引用类加载器只加载器包名为java,javax,sun等开头的类。

    注意:ClassLoader类,它是一个抽象类,其后所有类加载器都继承自ClassLoader(不包括启动类加载器)

    类加载器加载Class大致要经过如下8个步骤:

    1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
    2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
    3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
    4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
    5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
    6. 从文件中载入Class,成功后跳至第8步。
    7. 抛出ClassNotFountException异常。
    8. 返回对应的java.lang.Class对象。

    4.类加载机制JVM的类加载机制主要有3种

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

    1. 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。
    2. 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
    3. 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

    细讲一下双亲委派机制(面试):

    工作原理:

    如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器区执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父加载器无法完成此加载任务,子加载器才会尝试自己去加载,如果均加载失败,就会抛出ClassNotFoundException异常,这就是双亲委派模式。即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了了时,儿子自己才想办法去完成。

    双亲委派

    双亲委派优点

    1. 安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String。,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
    2. 避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)。Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。5

    5.沙箱安全机制

    作用:防止恶意代码污染java源代码。

    比如我们定义了一个类名为String所在包也命名为java.lang,因为这个类本来属于jdk的,如果没有沙箱安全机制,这个类将会污染到系统中的String,但是由于沙箱安全机制,所以就委托顶层的引导类加载器查找这个类,如果没有的话就委托给扩展类加载器,再没有就委托到系统类加载器。但是由于String就是jdk源代码,所以在引导类加载器那里就加载到了,先找到先使用,所以就使用引导类加载器里面的String,后面的一概不能使用,这就保证了不被恶意代码污染。

    6.类的主动使用/被动使用

    JVM规定,每个类或者接口被首次主动使用时才对其进行初始化,有主动使用,自然就有被动使用。

    主动使用

    1. 通过new关键字被导致类的初始化,导致类的加载并初始化。
    2. 访问类或接口的静态变量,包括读取和更新,或者对该静态变量赋值。
    3. 访问类的静态方法。
    4. 对某个类进行反射操作,会导致类的初始化。
    5. 初始化子类会导致父类的初始化。
    6. 执行该类的main函数。
    7. Java虚拟机启动时被表明为启动类的类(JavaTest)

    被动使用

    除了上面的几种主动使用其余就是被动使用了。

    1. 引用该类的静态常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致初始化。

      public final static int NUMBER = 5 ; //不会导致类初始化,被动使用
      public final static int RANDOM = new Random().nextInt() ;//会导致类的初 始化,主动使用
      
      • 1
      • 2
    2. 构造某个类的数组时不会导致该类的初始化。

      Student[] students = new Student[10] ;
      
      • 1

    注意:主动使用和被动使用的区别在于类是否会被初始化。

    7.类装载方式

    面试题:
    描述一下JVM加载Class文件的原理机制
    
    • 1
    • 2

    java中的所有类,都需要由类加载器装载到JVM中才能运行。类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的用法,像是反射,就需要显示的加载所需要的类。

    类装载方式:

    1. 隐式装载,程序在运行过程中当碰到通过new等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
    2. 显式装载,通过class.forName()等方法,显式加载需要的类。

    java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类完全加载到JVM中,至于其他类,则在需要的时候才加载。节省内存开销。

    面试题:
    在jvm中如何判断两个对象是属于同一个类?
    
    
    1.类的全类名(地址)完全一致。
    2.类的加载器必须相同。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
  • 相关阅读:
    SV-线程的使用与控制
    [CSS入门到进阶] 用transform后z-index失效了?总结transform的注意事项!
    Baklib帮助中心|如何设置好客户服务帮助您的客户?
    多线程,进程
    抖音步骤计划设置|成都聚华祥
    c++ unordered_map和map的区别
    go的解析命令行库flag
    海外网站服务器的5种优化手段
    Spring高手之路——深入理解与实现IOC依赖查找与依赖注入
    HIVE SQL regexp_extract和regexp_replace配合使用正则提取多个符合条件的值
  • 原文地址:https://blog.csdn.net/m0_67391121/article/details/126412314