天天看点

JVM--类加载机制

类加载机制就是要搞清楚类加载器是如何找到指定的Class文件以及怎样将Class文件装载进内存,以便执行引擎执行Class文件中存在的数据和指令,从而使你的Java程序跑起来

类的生命周期

JVM--类加载机制

结合上图,类加载机制主要学习加载、验证、准备、解析、初识化这些过程,然后就是需要了解真正可以将类加载进内存的一个玩意(还是代码实现)———类加载器!

 加载、验证、准备、初始化和卸载5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以再初始化阶段后再开始,这是为了支持Java语言的运行时绑定(也称动态绑定或晚期绑定)。注意:类的加载过程必须按照这种顺序按部就班地开始,而不是按部就班地进行或完成,因为这些阶段通常都是相互交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

类加载的时机

Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

JVM--类加载机制

这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。

需要特别指出的是,类的实例化和类的初始化是两个完全不同的概念:

  • 类的实例化是指创建一个类的实例(对象)的过程;
  • 类的初始化是指为类各个成员赋初始值的过程,是类生命周期中的一个阶段

被动引用常见的三种场景

  1. 通过子类引用父类的静态字段,不会导致子类初始化
  2. 通过数组定义来引用类,不会触发此类的初始化
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

类的加载过程

加载

在加载阶段,虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,如:网络、动态生成、数据库等)
  2. 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中(对于HotSpot虚拟机而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口;

加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在夹在阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会威胁虚拟机自身的安全。验证阶段大致会完成资格阶段的检验动作:

  1. 文件格式验证:验证字节流是否符合Class文件格式的规范(如:是否以魔数0xCAFEBABE开头,主次版本号是否在当前虚拟机的处理范围之内、常量池中是否有不被支持的类型)
  2. 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求(如:这个类是否有父类,除了java.lang.Object之外)
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;
  4. 符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响。如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段是正式为类变量(static成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值通常情况下是数据类型的零值,假设一个类变量定义为:

下面展示一些

内联代码片

那么,变量value在准备阶段过后的值为0而不是123。因为这时候尚未开始执行任何java方法,而把value复制为123的putstatic指令时程序被变异后,存放于类构造器方法()之中,所以把value赋值为123的动作将在初始化阶段才会执行。至于“特殊情况”是指:当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0;

解析

解析阶段是把常量池内的符号引用替换成直接引用的过程,符号引用就是Class文件中的CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量。

初始化

类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。

初始化阶段是执行类构造器clinit()方法的过程,虚拟机会保证这个类的构造方法的线程安全。

虚拟机会保证一个类的类构造器clinit()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器clinit(),其他线程都需要阻塞等待,直到活动线程执行clinit()方法完毕

clinit()方法时由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

初始化时机

概括的说,类初始化是【懒惰的】

  1. main方法坐在的类,总是会首先初始化
  2. 首次访问某个类的静态变量或静态方法时
  3. 子类初始化时,如果父类还没被初始化,会先初始化父类
  4. 子类访问父类的静态变量,只会触发父类的初始化
  5. Class.forName
  6. new会导致初始化

不会导致类初始化的情况

  • 访问类的static final静态常量(基本类型和字符串),不会触发初始化,已在链接阶段完成
  • 类对象.class不会触发初始化,
  • 创建类的数组不会触发初始化
  • 类加载器的loadClass方法
  • Class.forName的参数2为false

类加载器

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。

JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

JVM--类加载机制

1.启动类加载器(bootstrap class loader)

它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。由引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

2.扩展类加载器

负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。

3.应用类加载器(AppClassLoader):

 它一般用来加载程序所在目录下的类。程序可以通过ClassLoader的静态方getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

4.自定义类加载器:

通过继承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对象。

类加载机制:

JVM的类加载机制

所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

JVM--类加载机制

双亲委派模型的代码实现

双亲委派模型的代码实现集中在java.lang.ClassLoader的loadClass()方法当中。

  1. 首先检查类是否被加载,没有则调用父类加载器的loadClass()方法;
  2. 若父类加载器为空,则默认使用启动类加载器作为父加载器;
  3. 若父类加载失败,抛出ClassNotFoundException 异常后,再调用自己的findClass() 方法。

loadClass源代码如下:

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    //1 首先检查类是否被加载
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
             //2 没有则调用父类加载器的loadClass()方法;
                c = parent.loadClass(name, false);
            } else {
            //3 若父类加载器为空,则默认使用启动类加载器作为父加载器;
                c = findBootstrapClass0(name);
            }
        } catch (ClassNotFoundException e) {
           //4 若父类加载失败,抛出ClassNotFoundException 异常后
            c = findClass(name);
        }
    }
    if (resolve) {
        //5 再调用自己的findClass() 方法。
        resolveClass(c);
    }
    return c;
}
           

双亲委派机制的优势:

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

破坏双亲委派模型

双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了

下面介绍两个例子来讲解破坏双亲委派模型的过程。

  1. JNDI破坏双亲委派模型

    JNDI是Java标准服务,它的代码由启动类加载器去加载。但是JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。

    为了解决这个问题,引入了一个线程上下文类加载器。 可通过Thread.setContextClassLoader()设置。

    利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。

  2. Spring破坏双亲委派模型

    Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。

    那么Spring是如何访问WEB-INF下的用户程序呢?

    使用线程上下文类加载器。 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。

    利用这个来加载用户程序。即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。

原文链接:https://blog.csdn.net/m0_38075425/article/details/81627349

原文链接:https://blog.csdn.net/noaman_wgs/article/details/74489549

原文链接:https://www.cnblogs.com/ywb-articles/p/11219325.html