深入学习JVM- (6) 全面理解jvm类加载机制

  • 6.1 类加载机制

    • Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
    • 在Java里面,类型的加载、连接和初始化都是在程序的运行期间完成,这种机制一方面增加了提前编译时的额外困难,增加了类加载时的性能开销,另一方面为Java应用提供了极高的扩展性和灵活性,Java的动态扩展特性就是依赖于运行期动态加载和动态连接的特点实现。
  • 6.2 类加载时机

    • 类型的生命周期:加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载;【验证、准备、解析三个部分统称为连接】
      • ⚠️ “解析”不一定按上述顺序,某些情况下可以在初始化之后再开始【动态绑定】;
      • 加载、验证、准备、初始化和卸载这五个阶段按部就班地开始,而不是进行/完成,这些阶段通常相互交叉地混合进行;
    • 《规范》没有对“加载”阶段的开始有严格约束;
    • 《规范》严格规定了有且只有以下六种情况必须立即对类进行“初始化”<<对一个类型的主动引用>>:
      • (1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段;==》**使用new关键字实例化对象时; **读取或设置一个类型的静态字段[被final修饰、已在编译期把结果放入常量池的静态字段除外]; 调用一个类型的静态方法时;
      • (2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化;
      • (3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化;
      • (4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;
      • (5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_p utStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化;
      • (6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始;
    • 不会发生类初始化的情况——被动使用类字段时:
      • (1)通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要触发子类的加载和验证阶段,在《Java虚拟机规范》中并未明确规定,所以这点取决于虚拟机的具体实现。
      • (2)通过数组定义来引用类,不会触发此类的初始化,触发的是由newarray指令创建的一个由虚拟机自动生成的Object子类;
      • (3)常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。Java源码中确实引用了常量但是在编译阶段通过常量传播优化常量的值已经直接存储在调用类的常量池。
    • 接口的加载过程与类略有不同:一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化;
  • 6.3 类加载过程

    • (1)加载:
      • Loading只是Class Loading过程的一个阶段,注意不要混淆
      • 要做的事:➀从类的全限定名找定义类的二进制字节流;➁将字节流代表的静态存储结构转化成方法区的运行时数据结构;➂生成代表类的对象作为类的访问入口;
      • 非数组类的加载:既可以使用Java虚拟机里内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass()方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性;
      • 数组类的加载:
        • 如果数组的组件类型是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将被标识在加载该组件类型的类加载器的类名称空间上;
        • 如果组件类型不是引用类型,Java虚拟机将会把数组C标记为与引导类加载器关联。
        • 数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为p ublic,可被所有的类和接口访问到;
    • (2)验证
      • 1.文件格式验证
        • 主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求;
        • 基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入Java虚拟机内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构上进行的,不会再直接读取、操作字节流;
      • 2.元数据验证
        • 对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求;
      • 3.字节码验证
        • 整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的;
        • 如果一个方法体通过了字节码验证,也仍然不能保证它一定就是安全的;通过程序去校验程序逻辑是无法做到绝对准确的,不可能用程序来准确判定一段程序是否存在Bug;
      • 4.符号引用验证
        • 发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生;
        • 符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源;
        • 符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将 会 抛 出 一 个 java.lang.IncompatibleClassChangeError的 子 类 异 常 , 典 型 的 如 :java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等
    • (3)准备
      • 准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段;
      • ⚠️:(1)这个阶段只为类变量分配空间,实例变量不分配;(2)初始值通常情况下是数据类型的零值;(3)被final修饰的话会被初始化为指定的初始值;
    • (4)解析
      • 解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程;
      • 虚拟机可以自行决定是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用被使用前再去解析;
      • 同一个符号引用的多次解析非常常见,
        • 除了invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存,从而避免重复解析;jvm需要保证对同一个实体多次解析结果的一致性;
        • 对于invokedynamic指令,这个指令的目的是用于动态语言支持,它对应的引用称为“动态调用点限定符(Dynamically-Computed Call Site Specifier)”,这里“动态”的含义是指必须等到程序实际运行到这条指令时,解析动作才能进行。
      • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行;
    • (5)初始化
      • 初始化是类加载的最后一步,前面四个步骤中除了加载阶段用户应用程序可以自定义类加载器局部参与,其他的都有JVM主导完成,直到初始化阶段才开始真正执行类中编写的Java程序代码;
      • 初始化阶段就是执行类构造器<clinit >()方法的过程。<clinit >()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物;
      • <clinit >()方法
        • 产生过程:由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的;
        • 收集顺序:由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问;
        • 执行过程中的细节:
          • ➀不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit >()方法执行前,父类的<clinit >()方法已经执行完毕;
          • ➁由于父类的<clinit >()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作;
          • ➂<clinit >()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit >()方法;
          • ➃执行接口的<clinit >()方法不需要先执行父接口的<clinit >()方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化; 接口的实现类在初始化时也一样不会执行接口的<clinit >()方法;
          • ➄Java虚拟机必须保证一个类的<clinit >()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类<clinit >()方法,其他线程都需要阻塞等待,直到活动线程执行完毕<clinit >()方法。如果在一个类的<clinit >()方法中有耗时很长的操作,那就可能造成多个进程阻塞;
        • 同一个类加载器下,一个类型只会被初始化一次;
  • 6.4 类加载器

    • 类加载器指实现“通过一个类的全限定名来获取描述该类的二进制字节流”这一动作的代码。JVM设计团队有意把这一动作让应用程序自己决定怎么实现。
    • 类与类加载器
      • 对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立这个类在JVM中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较类相等需要在同一类加载器下比较;
    • 双亲委派模式
      • 三层类加载器
        • 启动类加载器Bootstrap
        • 扩展类加载器Extension
        • 应用程序类加载器Application/系统类加载器
      • 双亲委派模型
        • 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。但是父子关系通常不是由继承实现,而是通过组合关系来复用父加载器;
        • 工作过程:当类加载器收到类加载的请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载;
        • 很好地解决了各个类加载器协作时基础类型的一致性问题
      • 破坏双亲委派模型
        • 第一次破坏:是在模型被引入之前,为了保护这个双亲委派模型,设计者们在java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码;
        • 第二次破坏:为了解决基础类型调用回用户代码的情况(JNDI服务),引入线程上下文加载器Thread Context ClassLoader;
        • 第三次破坏:由于用户对程序动态性的追求而导致,
  • 6.5 Java模块化系统

    • 模块化系统
      • 模块化的关键目标——可配置的封装隔离机制,为了实现这一目标,JVM对类加载架构做了相应的变动调整;
      • JDK 9的模块不仅仅像之前的JAR包那样只是简单地充当代码的容器,除了代码外,Java的模块定义还包含:(1)依赖其他模块的列表。(2)导出的包列表,即其他模块可以使用的列表。(3)开放的包列表,即其他模块可反射访问模块的列表。(4)使用的服务列表。(5)提供服务的实现列表。
      • 由于可以申明对其它模块的显示依赖,因此可以避免很大一部分由于类型依赖而引发的运行时异常;
      • 模块化机制可以提供更精细的可访问性控制;
    • 模块的兼容性
      • 某个类库到底是模块还是传统的JAR包,只取决于它存放在哪种路径上;
      • 模块化系统的3条规则保证了向后兼容性:
        • (1)JAR文件在类路径的访问规则:所有类路径下的JAR文件及其他资源文件,都被视为自动打包在一个匿名模块(Unnamed Module)里,这个匿名模块几乎是没有任何隔离的,它可以看到和使用类路径上所有的包、JDK系统模块中所有的导出包,以及模块路径上所有模块中导出的包;
        • (2)模块在模块路径的访问规则:模块路径下的具名模块(Named Module)只能访问到它依赖定义中列明依赖的模块和包,匿名模块里所有的内容对具名模块来说都是不可见的,即具名模块看不见传统JAR包的内容;
        • (3)JAR文件在模块路径的访问规则:如果把一个传统的、不包含模块定义的JAR文件放置到模块路径中,它就会变成一个自动模块(Automatic M odule)。尽管不包含module-info.class,但自动模块将默认依赖于整个模块路径中的所有模块,因此可以访问到所有模块导出的包,自动模块也默认导出自己所有的包;
    • 模块化下的类加载器
      • 模块化下的类加载器的变动:
        • (1)扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代;
        • (2)平台类加载器和应用程序类加载器都不再派生自java.net.URLClassLoader,现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader
        • (3)当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值