JVM——类加载机制

类文件结构

  • class文件是一组以8个字节为基础单位的二进制字节流(可能是磁盘文件,也可能是类加载器直接生成的)各个数据项严格按照顺序排列,中间没有任何符号间隔
  • class文件采取一种类似于c语言结构体的储存结构,只有两种数据类型:无符号数和表
  • 无符号数属于基本数据类型,以u1、u2、u4和u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串
  • 表是由多个无符号数获取其他表作为数据项构成的复合数据类型,习惯用“_info”结尾
  • 无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
    在这里插入图片描述

类的加载

类加载机制的定义: 虚拟机将描述类信息的.class文件(二进制字节流)读取到内存,并对数据进行校验、转换解析和初始化(即将类信息放在方法区中,并在堆中创建.class对象,用来封装在方法区中的类的数据结构),最终形成可被虚拟机直接使用的java类型的过程。

类加载时机

运行期进行加载,即类型的加载、连接、初始化过程都在程序的运行期完成。

编译期:编译器将源代码翻译为机器能识别的代码,java呗编译为Jvm认识的字节码文件

运行期:java代码运行过程

JVM并不是在首次主动使用到这个类的时候才去进行类的加载,JVM规范允许类加载器预期即将使用到这个类时进行加载,如果在预期加载过程中遇到了.class文件缺失或者错误,类加载器必须在程序首次主动使用该类时才会报告错误,如果这个类一直没有被程序主动使用,那么一直不会报错。

在何处加载.class文件

类的加载过程使用类的加载器实现,同时我们也可以通过继承ClassLoader基类自主实现类的加载器,不同的加载器可以从不同来源加载类的二进制数据,通常来源有以下几种:

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有的数据库中提取.class文件
  • 将java源文件动态编译为.class文件

类的生命周期

  • 类的生命周期:
    在这里插入图片描述
  • 解析在某些情况下可以在初始化阶段开始之后,为了支持java的动态的绑定
  • 需要注意的是这几个阶段是按顺序开始的,并不是按顺序进行或者结束的,通常是互相交叉的混合进行,通常在一个阶段执行的过程中调用或激活另一个阶段

加载

这一阶段主要是将类的class文件加载进内存,并为之创建一个java.lang.class对象的过程。
此阶段的可控性比较强(可以由用户自己定义的类加载器进行加载)加载阶段一般由类加载器完成,一般完成三件事情:

通过类的全限定名获得其定义的二进制字节流

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

在内存中生成代表这个类的class对象,作为方法区中这个数据的访问入口

验证

这一阶段主要是对加载进内存的class文件进行验证,确保文件信息不会威胁到虚拟机的安全。主要分为以下四种验证过程:

  • 文件格式验证

验证字节流是否符合class文件格式规范,能够被当前虚拟机加载处理,如:主次版本号是否在当前虚拟机处理范围之内,常量池中是否有不支持的类型等等

  • 元数据验证(元数据:数据的数据,一般用来描述代码之间的关系)

对字节码描述的信息进行语义分析,保证其描述的信息符合java语言规范,如:验证这个类是不是有父类,方法字段与父类是否有冲突等

  • 字节码验证

整个验证过程中最复杂的阶段,在元数据验证阶段完成对数据类型的验证后,这个验证阶段主要对类的方法做出分析,保证类的方法不会威胁到虚拟机的安全。

  • 符号引用验证

验证的最后阶段,主要发生在将符号引用转化为直接引用的过程中,确保解析动作可以完成。

准备

此阶段主要为类的变量分配内存并设置初始默认值(如果为final修饰的静态变量则直接为设置的初始值),内存都在方法区进行分配。以下为几种列举类型的初始值:
在这里插入图片描述

需要注意的是:此时是为类变量分配内存而不是实例变量,实例变量随着对象的初始化时进行分配


解析

此过程主要将常量池中的符号引用转化为直接引用,在方法区中进行。
特点:时间不可预料,有可能和初始化交换位置

为什么此阶段的时间具有不可预料的特质?

JVM的实现大多是延迟加载,如在加载A类的过程中,类A中引用了类B,虚拟机并不会将类B加载进来,而是等到执行的时候才去加载,而类B在类A中的表现形式被记录在符号表中,而解析的过程就是将符号表中的类B表现形式换为直接引用,这也是时间上发生不可预料的原因。

初始化

此阶段JVM才会真正执行java中的代码。一般在以下情况发生时会触发类的初始化:

  • 创建类的实例,也就是new一个对象
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(Class.forName(“…”))
  • 初始化一个类的子类(会首先初始化子类的父类)
  • JVM启动时标明的启动类,即文件名和类名相同的那个类 (一般main方法所在类)

不会导致类的初始化的情况:

  • 使用类的final修饰的静态常量(基本数据类型和字符串类型)
  • 创建类的数组不会进行类的初始化
  • 访问类的.class对象

通过以上情况我们可以实现类的懒惰初始化的单例模式:

public class Person {
    private Person(){}
    private static class Name {
         static final Person person = new Person();
    }
    public Person getInstance() {
        return Name.person;
        //只有在此时调用Name类的静态变量,才会进行初始化,创建单例对象
    }
}


类加载器

类加载器分为三类:启动类加载器、扩展类加载器、应用程序类加载器

启动类加载器:加载java的核心类(位于JAVA_HOME/jre/lib目录下的类),用原生代码来实现,并不继承自java.lang.ClassLoader,无法直接访问,用C++代码编写

扩展类加载器:加载位于JAVA_HOME/jre/lib/ext目录下的类

应用程序类加载器:加载classpath目录下的类

双亲委派模型

调用loadclass时,查找类的规则

比较两个类是否相等,只有当两个类是由同一个类加载器加载的前提下才有意义。

在这里插入图片描述

  • 除了启动类加载器之外,其他类加载器均有父类加载器
  • 当一个类收到加载请求时,就会先交给父类加载器,所有的加载最终都会给启动类加载器,当父类加载器无法加载时,子类加载器才会尝试自己去加载。

为什么使用双亲委派机制加载?

  • 保证java基本类的进行,比如我们自定义了Object类,并定义加载器为自定义加载器,这时就会使得系统有多个同名类,使得基础的java类系统混乱,而使用双亲委派模型,无论我们在怎样的类加载环境中,Object类永远都是由启动加载器加载的,都是同一个类。

双亲委派模型的实现

(1)检查类是否被加载
(2)没有加载的话,调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。
(2)如果父类加载依然失败的话,抛出ClassNotFoundException异常后,调用自身加载器的findClass()方法进行加载

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值