JVM——字节码文件的加载过程(类的加载过程)以及双亲委派机制

JVM运行加载字节码文件

简图如下

类加载器子系统

类加载器:

  • 负责从文件系统(简单说就是硬盘)或者网络上加载class文件,class文件在文件开头有特点的标识。
  • ClassLoader只负责class文件的加载,只要是符合JVM对字节码文件规范的要求就可以,至于它是否可以运行,由执行引擎决定。
  • 加载的类信息存放在方法区(一块内存空间),除了类信息之外,方法区中还会存放运行时常量池信息,可能还会包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)

例如一个磁盘上的xxx.class文件,会被JVM的ClassLoader(有很多类加载器)通过二进制流的方式,加载到JVM中,此时会生成对应的xxx的class对象,通过该class对象就可以调用构造函数生成xxx的对象放在方法区当中

类加载器加载字节码文件主要分为三个阶段:

  • 加载字节码文件
    • 通过一个类的路径,加载此class文件的二进制字节流
    • 将这个字节流所代表的静态存储结构转换为方法区(方法区具体来说,jdk7之前叫永久代,之后叫元数据或者元空间,泛称为方法区)的运行时数据结构
    • 在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类各种数据的访问入口
    • 加载字节码文件的方式:
      • 从系统磁盘直接读取
      • 通过网络获取,如:Web Applet(我没有了解过~~~)
      • 从zip压缩包中读取,典型的就是jar包和war包,这些压缩包中都是已经编译好的字节码文件
      • 运行时生成,如:动态代理(java.lang.reflect)
      • 其他文件生成,典型的形式如:JSP
      • 从专用的数据库中提取class字节码文件(我没有了解过~~~)
      • 从加密文件中获取,防止class文件被反编译(如安卓代码的混淆和加密)
  • 链接
    • 验证
      • 确保加载的class字节码文件内容信息是符合当前JVM规范的,保证被加载类的正确性,不会有安全风险
      • 包括文件格式验证、元数据验证、字节码验证、符号引用验证(了解一下就好,如校验二进制文件头是否是:CA FE BA BE,CA FE BA BE是JAVA虚拟机识别的标识符)
    • 准备
      • 为类变量分配内存,并且设置变量的初始值(0、false或者null等)
      • //在链接阶段的准备过程中,a会被赋值为0,1的赋值会在初始化阶段中进行
        int a = 1;
      • 这里不包含final修饰的static(也就是常量),因为final修饰的常量在编译的时候就会分配值了,准备阶段会显式的初始化
      • 不会为实例变量初始化(未被static修饰的)成员变量,类变量会被分配到方法区中,实例变量随对象创建后分配到JVM的堆中
    • 解析
      • 将常量池中的符号引用转换为直接引用的过程
      • 符号引用就是使用符号来描述所引用的目标(符号引用的字面量形式定义在《Java虚拟机规范》中的class文件格式,可以理解为一个符号从字面上定义了引用的目标),直接引用就是直接指向目标的指针、相对偏移量、或者一个间接定位到的目标句柄
      • 解析的目标主要针对:类、接口、字段、类方法、接口方法、方法类型等。对应常量池当中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodred_info。(这一块不是很懂)
  • 初始化
    • 初始化阶段就是执行类构造方法<clinit>()的过程
    • 构造器方法<clinit>()中的指令,按照源文件中代码出现的顺序执行(其实java的IDE会有相应的语法检查的,不会让你在static静态代码块中调用静态代码块之后声明的变量的)
    • 该方法是定义好的,不是类的构造方法,构造方法对应的class文件中<init>()方法,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来(简单的理解就是<clinit>()是在编译的时候根据类变量赋值操作代码和类静态代码块中的代码拼接出来的方法,如果类中没有定义静态变量或者是静态代码块那么就不会有<clinit>()方法产生
    • 如果该类存在父类,JVM会保证在执行该类的<clinit>()之前,首先执行其父类的<clinit>()
    • JVM要保证一个类的<clinit>()在多线程情况下是同步执行的(加锁),饿汉式单例模式,就是利用了这一特性来防止多线程情况下的多次初始化执行。

双亲委派机制

JVM对class文件采用的是按需加载的方式,也就是说需要使用该类的时候才会将该类的class文件加载到内存中,生成其对应的Class对象。

在加载某个类的时候,JVM采用的是双亲委派机制:即把加载请求交给父类处理,是一种任务委派模式

类加载的处理顺序自上而下。

原理

  1. 如果一个类加载器收到了类加载的请求,其自身并不会马上对该类进行加载,而是将类加载请求委托给父类进行加载
  2. 如果上级类加载器还有上层的类加载器,就一直将该类加载请求向上传递,直到顶层的启动类加载器(boostrap classloader)
  3. 如果上层的类加载器可以完成该类的加载,就对其进行加载并返回,如果上层加载器无法完成该类的加载,才会返回下层的加载器对该类进行加载
    1. 需要注意的是,各层级加载器判断某个类是否是自己加载范围内的依据是:该类的加载路径是否是当前类加载器所包含的路径

基于上述原理,分析如下:

当开发者自己在src目录下创建了java.lang的包,并且在改包下创建了String类,那么java.lang.String str = new java.lang.String()创建的是哪个类呢?

答案是Java类库的String类,同时可以得到这样一个结论,该自定义的String类中的main方法是不可以运行的,因为当前String类没有被加载,报错:java.lang.String没有main方法,这就是沙箱安全机制

双亲委派机制的作用

  • 避免类重复加载
  • 保护程序的安全,防止核心的API被随意修改
    • 自己创建的与核心类库中同包同名的类是无法被加载的
    • 自己创建的与核心类库同包的类加载将会报错:java.lang.SecurityException(即禁止自定义类使用核心类库中的包名)

拓展

JVM中判断两个Class对象是否是同一个类来源的两个必要条件:

  • 包名和类名必须相同
  • 加载这个类的ClassLoader是否是同一个

注意:即使这两个Class对象来源于同一个class字节码文件被JVM所加载,但是只要两个Class对象使用的ClassLoader不同,那么就认为这两个Class对象是不相等的

对加载器的引用

  • JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的
  • 加载器加载一个类的时候,会将当前类加载器的一个引用作为类型信息同Class对象一并放在方法区当中
  • 当解析到一个类引用了另一个类的时候,JVM将保证这两个类使用的加载器是相同的

类的主动使用和被动使用

两者的区别在于会不会导致类的初始化——即会不会经历类加载的三个阶段中的初始化阶段,是否执行了<clinit>()方法

两者的区别在于会不会导致类的初始化——即会不会经历类加载的三个阶段中的初始化阶段,是否执行了<clinit>()方法

两者的区别在于会不会导致类的初始化——即会不会经历类加载的三个阶段中的初始化阶段,是否执行了<clinit>()方法

  • 主动使用
    • 创建一个类的实例
    • 访问某个类或者接口的静态变量,或者对静态变量进行赋值
    • 调用类的静态方法
    • 通过反射加载类
    • 初始化类的子类(父类会随之初始化、并先于子类初始化)
    • JVM启动时被表明为启动类的类
    • JDK7开始的提供的动态语言支持:
  • 被动使用(除主动使用外的归为被动使用)

参考文章:类的主动使用和被动使用java类的主动使用/被动使用

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值