[深入理解JVM] 第七章 虚拟机类加载机制

ref: https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/类加载器.md
在这里插入图片描述

  • 类加载过程步骤
    [外链图片转存失败(img-LnRMVVBt-1564157597329)(leanote://file/getImage?fileId=5d3b0c4a2657b8417200000d)]

  • 什么时候在加载验证、准备之后需要初始化:

    1. 遇到new、getstatic、putstatic或invokestatic字节码指令;
      即遇到new一个对象、**读取或设置【并不是定义就会初始化】**一个类的静态字段的时候(被final修饰的除外->会被放入常量池中);
    2. 对类进行反射调用的时候(java.lang.refect);
    3. 先出发父类的初始化;(接口不会初始化父类,用到时才初始化)
    4. 虚拟机启动时初始化main所在的主类;
    5. 当使用HDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法的句柄所对应的类没有进行过初始化,则需要先触发其初始化;

  1. 加载

    • 加载分为三部:
      1. 通过全类名获取定义此类的二进制字节流;
      2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构;
      3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口;
        [外链图片转存失败(img-gQLWnZ6Y-1564157597331)(leanote://file/getImage?fileId=5d3b12e22657b8417200000e)]
    • 程序员可以通过各种方式对字节流进行读取:
      • zip、网络、数据库。。。
      • 相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。
  2. 验证

    1. 文件格式验证: 是否符合class文件格式的规范.以保证可以正确的使数据存储在方法区之类;
    2. 元数据验证: 对类的元数据信息进行语义校验,如检验是否其父类继承了不允许被继承的类;
    3. 字节码验证: 通过对数据流和控制流的分析,确保程序语义时合法的,如保证跳转指令不会跳到方法体之外的字节码之中;
    4. 在符号引用转化为直接引用时(解析阶段)进行检验,确保解析动作能正常进行;
  3. 准备

    • 为类变量分配内存并设置初始值;
    • 内存分配只有类变量(static)_,实例变量会在对象初始化的时候和对象一起分配在堆上;
    • 本阶段的初始值均为0,并不会赋予实际的值(未执行java方法),但final修饰的static变量会;
  4. 解析

    • 将常量池中分符号引用替换为直接引用;
    • 符号引用: 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中(就是用来表明程序中有这个东西…)。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
    • 直接引用: 与虚拟机的内存布局相关
      (1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
      (2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
      (3)一个能间接定位到目标的句柄

    常量池分为两种,静态常量池运行时常量池:
    静态常量池主要用于存放两大类常量:字面量(Literal)和符号引用量(Symbolic References),字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:
    1. 类和接口的全限定名
    2. 字段名称和描述符
    3. 方法名称和描述符
    运行时常量池,则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

  5. 初始化

    • 初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码),初始化阶段是执行类构造器 clinit ()方法的过程。
    • clinit()方法: 编译器会自动收集类中所有类变量的赋值动作和静态语句块,把他们合到一起;
    • 静态语句块中只能访问定义在静态语句块之前的内容;
    • 虚拟机会保证子类执行clinit()的时候,父类的clinit都已经执行过了;
    • 对于clinit() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为clinit() 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。

  • 类加载器
    • 类加载器和类本身一同确定其在虚拟机中的唯一性,每一个类加载器,都有一个独立的名称空间–>比较两个类是否"相等",只有在这个类是由同一个加载器加载的前提下才有意义;
    • 分类:
      1. 启动类加载器:将\lib目录下或别指定的路径中的被虚拟机识别的类库加载到虚拟机之中;
      2. 扩展类加载器:加载<JAVA_HOME>\lib\ext下或别指定的路径中的类库,开发者可以使用;
      3. 引用程序加载器(系统类加载器):加载用户路径中的类库,开发者可以直接使用;
    • 双亲委派模型:
      [外链图片转存失败(img-sTb88EIY-1564157597332)(leanote://file/getImage?fileId=5d3b23c32657b8417200000f)]
      • 好处: 双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值