JVM(三) - 类加载

一、类的生命周期 

类的加载过程包括了加载验证准备解析初始化五个阶段,其中验证、准备、解析又合称为连接阶段。在加载的5个阶段加载、验证、准备和初始化这四个阶段发生是按顺序开始的(而不是按顺序进行或完成,因为这些阶段通常都是交叉混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段),而解析阶段则不一定,在某种情况下可以在初始化阶段之后开始,为了支持Java语言的运行时绑定(即动态绑定或晚期绑定)。

结束类生命周期的几种场景:

  • 执行System.exit()方法
  • 程序正常执行结束
  • 程序执行中遇到了异常或错误而异常终止
  • 操作系统出现错误或强制结束程序而导致Java虚拟机进程终止

二、类的加载过程

虚拟机的类加载机制:Java使用某个未加载的类时,JVM会通过加载class文件、连接(验证、准备和解析)、初始化三个步骤来对该类进行初始化,最终形成可以被虚拟机直接使用的 Java 类型。

类的加载是指把类的.class文件中的数据读入到内存中,生产与所加载类对应的Class对象,加载完成后,Class对象还不完整;加载后就进入连接阶段,包括验证(验证加载类的安全性)、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,初始化中还包括:父类的初始化 和 按顺序执行类中的初始化语句。

1、加载Loading

查找并加载类的二进制数据

虚拟机把描述类的数据源(.class文件二进制数据)加载到内存中,映射为 JVM 认可的数据结构(Class 对象)。在加载阶段完成三件事:

  • 通过一个类的全限定名来获取其定义的二进制字节流;
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  • 在Java堆中生成代表这个类的Class对象,作为对方法区中这些数据的访问入口。

加载完成后,数据放在运行时数据区的方法区内 ,最终在堆区创建一个java.lang.Class对象,封装类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。相比其他阶段,加载阶段是用户参与(可控性最强)的阶段,可以使用系统提供的类加载器来完成加载,也可以自定义类加载器完成加载。

 

这里加载的数据源可以是:

  • 从本地系统文件中加载.class文件;
  • 通过网络下载.class文件,典型的应用是 Applet;
  • 把一个java源文件动态编译、并执行加载;
  • 从zip,jar等归档文件中提取.class文件加载,JDBC编程时使用到的数据库驱动就是放在JAR文件中,JVM可以直接从JAR包中加载class文件;
  • 运行时计算生成,这种场景使用得最多的就是动态代理技术,java.lang.reflect.Proxy中,就是用了 ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。

2、验证Verification

确保被加载的类的正确性

验证作为连接阶段的第一步,是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的安全。验证阶段会完成下面四个阶段的检验:

a.文件格式验证验证字节流是否符合Class文件格式的规范,且能被当前版本的虚拟机处理。

  • 是否以魔数 0xCAFEBABE开头
  • 主、次版本号是否在当前虚拟机处理范围之内
  • 常量池的常量中是否有不被支持的常量类型(检査常量tag 标志)
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合装型的常量
  • CONSTANT_Utf8_info型的常量中是否有不符合 UTF8编码的数据
  • Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息

上面只是从 HotSpot虚拟机源码中摘抄的一小部分。只有通过该阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

b.元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

  • 这个类是否有父类(除了 java.lang.0bject之外,所有的类都应当有父类)
  • 这个类的父类是否继承了不允许被继承的类(被finaI修饰的类)
  • 如果这个类不是抽象类, 是否实現了其父类或接口之中要求实现的所有方法
  • 类中的字段、 方法是否与父类产生了矛盾(例如覆盖了父类的final字段, 或者出現不符合规则的方法重载, 例如方法参数都一致, 但返回值类型却不同等)等等。

c.字节码验证通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的

  • 保证跳转指令不会跳转到方法体以外的字节码指令上
  • 保证方法体中的类型转换是有效的,如可以把子类对象赋值给父类数据装型,这是安全的;但把父类对象意赋值给子类数据类型,甚至把对象赋值给毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的
  • 保证任意时刻操作数栈的数据装型与指令代码序列都能配合工作,例如不会出现这种情况:在操作栈中放置了一个 int类型的数据, 使用时却按long类型来加载入本地变量表中

字节码验证阶段是整个验证过程中最复杂的一个阶段,在第二阶段对元数据信息中的数据类型做完校验后,这阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。

d.符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候 , 这个转化动作将在连接的第三个阶段——解析阶段中发生。确保解析动作能正常执行

  • 符号引用中通过字将串描述的全限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的类、字段和方法的访问性(private、 protected、 public、 default)是否可被当前类访问

符号引用验证可以看做是对类自身以外(常量池中的各种符号引用) 的信息进行匹配性的校验,如没有通过符号引用验证, 将会抛出一个 java.lang.IncompatibleClassChangError异常的子类, 如 java.lang.IllegalAccessError、 java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

为什么需要验证阶段?

Java语言本身是相对安全的语言,但Class文件并不一定要求用Java源码编译而来,可以使用任何途径, 甚至可用十六进制编译器直接编写来产生 Class 文件。类的加载是JVM针对class文件的二进制数据,所以虚拟机如果不检査输入的字节流,可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

对于虚拟机的加载机制来说,验证阶段是一个非常重要的、 但不一定是必要的阶段。如果所运行的全部代码已被反复使用和验证过,可以考虑使用-Xverify:none 参数来关闭大部分的验证措施,以缩短虚拟机类加载的时间。

3、准备Preparation

为类的静态变量分配内存并设置初始默认值

  • 进行内存分配的仅是类变量(static修饰的变量),所使用的内存都将在方法区中进行分配;不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中;
  • 初始值是指数据类型的零值(如0、0L、null、false等)。
  • 如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中
// 假设一个类变量的定义
public static int a = 3;
// 变量a在准备阶段过后的初始值为0,因为这时候尚未开始执行任何Java方法
// 把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中,所以把a赋值为3的动作将在初始化阶段才会执行

// 假设一个类变量的定义
public static final int b = 3;
// 编译时Javac会为b生成ConstantValue属性,,在准备阶段虚拟机就会根据ConstantValue的设置将b赋值为3

注意:

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值;而局部变量,在使用前必须显式地为其赋值,否则编译时不通过。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;
  • 而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值