聊聊JVM——类加载机制

目录

概述:

JVM基本结构:

一.类加载机制:

1.加载:

2.验证:

3.准备:

4.解析:

5.初始化:

6.使用:

7.卸载:

二.类加载器与双亲委派模型:

1.类加载器定义:

2..主要的四种类加载器:  

3.双亲委派机制:

4.双亲委派机制的作用:

5.破坏双亲委派机制:

6.历史上有哪⼏次双亲委派机制的破坏?


概述:

JVM(Java Virtual Machine)是一种虚拟机,用于执行Java程序。在java程序运行时,编译器将java文件编译为平台无关的java字节码文件(.class),通过对应平台jvm进行解释,翻译成对应平台的机器码运行。这也是java可以跨平台的原因。

聊聊JVM——类加载机制_第1张图片

JVM基本结构:

聊聊JVM——类加载机制_第2张图片

一.类加载机制:

一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载 (Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称 为连接(Linking)。这7个阶段也就是类的生命周期。如下图所示:

聊聊JVM——类加载机制_第3张图片

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

1.加载:

        什么情况下需要开始加载,《Java虚拟机规范》中并没有进行 强制约束,这点可以交给虚拟机的具体实现来自由把握。在加载阶段,Java虚拟机需要完成以下三件事情:

        1)通过一个类的全限定名来获取定义此类的二进制字节流。

        2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

        3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入 口。

2.验证:

        大致上会完成下面四个阶段的检验动作:

        1)文件格式验证[基于二进制字节流]:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。

        2)元数据验证[基于方法区的存储结构]:对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息。

        3)字节码验证[基于方法区的存储结构]:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。

        4)符号引用验证[基于方法区的储存结构]:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个java.lang.IncompatibleClassChangeError的子类异常,典型的如: java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

3.准备:

        正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值。对于final static修饰的变量, 直接赋值为用户的定义值。如下面的例子:这里在准备阶段过后的初始值为0,而不是7。

public static int a=7
4.解析:

        Java虚拟机将常量池内的符号引用替换为直接引用的过程。 

  • 符号引用是在编译期和类加载阶段使用的引用,用于描述类、字段和方法的类型信息。
  • 直接引用是在虚拟机运行时使用的引用,用于访问实际的对象和方法,它包含了实际的内存地址或偏移量。
  • 符号引用通过解析过程转化为直接引用,这通常在类加载阶段的解析步骤中完成。
  • 直接引用的作用是实际的数据访问和方法调用,它是在虚拟机运行时执行的。
5.初始化:

        到了初始化阶段,jvm才真正开始执行类中定义的java代码。

        1)始化阶段就是执行类构造器()方法的过程。类构造器()方法是由编译器自动收集 类中的所有类变量的赋值动作和静态语句块(static块)中的语句合并产生的。

        2)当初始化一个类的时候,如果发现其父类还没有进行过初始化、则需要先触发其父类的初始化。

        3)虚拟机会保证一个类的()方法在多线程环境中被正确加锁和同步。

6.使用:

        包括创建类的实例、调用类的方法、访问类的字段等操作。类的使用是在程序运行时进行的,可以多次发生。

7.卸载:

        在某些情况下,虚拟机可能会卸载已加载的类,释放相关的内存资源。卸载通常发生在类加载器被回收时,或者在类加载器的类不再被引用时。

二.类加载器与双亲委派模型:

1.类加载器定义:

        Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节 流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动 作的代码被称为“类加载器”(Class Loader)。

2..主要的四种类加载器:  
  • 启动类加载器(Bootstrap ClassLoader)⽤来加载 java 核⼼类库,⽆法被 java 程序直接引⽤。
  • 扩展类加载器(extensions classLoader):它⽤来加载 Java 的扩展库。Java 虚拟机的实现会提供⼀个扩展库⽬录。该类加载器在此⽬录⾥⾯查找并加载 Java 类。
  • 系统类加载器(system classLoader):它根据 Java 应⽤的类路径(CLASSPATH)来加载 Java 类。⼀般来 说,Java 应⽤的类都是由它来完成加载的。可以通过ClassLoader.getSystemClassLoader()来获取它。
  • ⽤户⾃定义类加载器 (user classLoader),⽤户通过继承 java.lang.ClassLoader 类的⽅式⾃⾏实现的类加载器。
3.双亲委派机制:

        当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

聊聊JVM——类加载机制_第4张图片

4.双亲委派机制的作用:
  • 1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
  •  2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载, 即使加载也不会是同一个 .class 对象了。不同的加载器加载同一个.class也不是同一个Class对象。 这样保证了 Class 执行安全。
5.破坏双亲委派机制:

        如果不想打破双亲委派模型,就重写ClassLoader 类中的findClass()⽅法即可,⽆法被⽗类加载器加载的类最终会通过这个⽅法被加载。⽽如果想打破双亲委派模型则需要重写 loadClass()⽅法或者使用线程上下文类加载器通过Thread.setContextClassLoader(ClassLoader cl)来设置,然后在需要的地方使用它来加载类。

6.历史上有哪⼏次双亲委派机制的破坏?

 第⼀次破坏—— 双亲委派模型的第⼀次“被破坏”其实发⽣在双亲委派模型出现之前——即 JDK 1.2 ⾯世以前的“远古”时代。 由于双亲委派模型在 JDK 1.2 之后才被引⼊,但是类加载器的概念和抽象类 java.lang.ClassLoader 则在 Java 的第 ⼀个版本中就已经存在,为了向下兼容旧代码,所以⽆法以技术⼿段避免 loadClass()被⼦类覆盖的可能性,只能 在 JDK 1.2 之后的 java.lang.ClassLoader 中添加⼀个新的 protected ⽅法 findClass(),并引导⽤户编写的类加载逻辑时尽可能去重写这个⽅法,⽽不是在 loadClass()中编写代码。

第⼆次破坏—— 双亲委派模型的第⼆次“被破坏”是由这个模型⾃身的缺陷导致的,如果有基础类型⼜要调⽤回⽤户的代码,那该怎 么办呢? 例如我们⽐较熟悉的 JDBC: 各个⼚商各有不同的 JDBC 的实现,Java 在核⼼包 \lib ⾥定义了对应的 SPI,那么这个就毫⽆疑问由启动类加载器加载。 但是各个⼚商的实现,是没办法放在核⼼包⾥的,只能放在classpath⾥,只能被应⽤类加载器加载。那么,问题来了,启动类加载器它就加载不到⼚商提供的 SPI 服务代码。 为了解决这个问题,引⼊了⼀个不太优雅的设计:线程上下⽂类加载器 (Thread Context ClassLoader)。这个 类加载器可以通过 java.lang.Thread 类的 setContext-ClassLoader()⽅法进⾏设置,如果创建线程时还未设置,它将会从⽗线程中继承⼀个,如果在应⽤程序的全局范围内都没有设置过的话,那这个类加载器默认就是应⽤程序类加载器。 JNDI 服务使⽤这个线程上下⽂类加载器去加载所需的 SPI 服务代码,这是⼀种⽗类加载器去请求⼦类加载器完成类加载的⾏为。

第三次破坏—— 双亲委派模型的第三次“被破坏”是由于⽤户对程序动态性的追求⽽导致的,例如代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。 OSGi 实现模块化热部署的关键是它⾃定义的类加载器机制的实现,每⼀个程序模块(OSGi 中称为 Bundle)都有 ⼀个⾃⼰的类加载器,当需要更换⼀个 Bundle 时,就把 Bundle 连同类加载器⼀起换掉以实现代码的热替换。在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,⽽是进⼀步发展为更加复杂的⽹状结构。

你可能感兴趣的:(JVM,jvm)