一文搞懂虚拟机类加载全阶段


前言

首先说一下连接:java的连接需要在运行过程中完成,其他很多语言比如C++的连接在编译汇编后就完成了,这个时候所有数据在内存地址就已经分配好了,不像java。


1. 类加载全阶段

类加载的阶段:
加载,验证,准备,解析,初始化,使用,卸载
这些步骤只有加载,验证,准备,初始化和卸载这五步是确定会做的!
这个阶段是按部就班的进行,但不是按部就班的完成,比如有些解析阶段会在初始化之后。
在这里插入图片描述

1.1 加载(和类加载这个名词不同)

加载阶段和部分连接阶段的行为是交替进行的

步骤

1、 根据一个类的全限定名来获取到对应的字节码数据(注意:这里只需要字节码数据,給了程序猿很大的自主设定空间,我们可以通过war,jar等各种方式激活类加载器,只要里面是字节码)
2、 根据字节码数据,将class文件种静态的(也就是以字节码显示出来的固定死的)类结构转化为运行时动态得类结构
3、 为类在方法区种分配空间,并提供给其他访问该类型数据的渠道,最后会在堆中实例化java.lang.class对象,供程序从外部访问类的数据

数组类加载方式

数组类不由类加载器创建,而是在内存中动态创建,但是因为该数组的元素是一个类,这个类的数据还是会由类加载器加载。
如果数组的组件类型不是引用类型(如int[]),则将其与类加载器绑定,把数组所有元素的访问权限设置为与组件类型的访问权限相同。

1.2验证

因为java语言程序是相对安全,但类加载器接收的字节码文件不一定是java语言编写的,为了保证字节码数据安全,需要验证

步骤

1、 文件格式验证:这部分主要针对class文件中存储的静态数据是否符合JVM的规范。
2、 元数据验证:这一步主要对元数据进行语义验证,举个例子,去验证一个类的一些属性是否违反了规定,比如类继承了不该继承的父类等等
3、 字节码认证:这一步主要验证存在于Class文件中方法体的方法语义是否正确,是否会影响到虚拟机的正常运行。没通过字节码的程序一定有问题,但是通过了不代表安全,因为用一个程序去验证另一个程序是会有疏漏的。

因为该环节需要消耗大量的时间,设计者们想了一个优化方案:在方法体中增加一个属性StackMapTable,该属性中包含方法体中所有块在运行之前应该的变量表和操作栈状态,每次验证字节码时检查这些状态是否符合标准就OK,但是可以通过恶意修改这个属性中存储的东西,达到破坏验证的目的。
4、 符号引用验证:说白了就是验证该类对于类外一些数据的引用是否合法。

1.3准备

准备阶段即在逻辑上的方法区(JDK7取消方法区,都是分配到堆中)为静态变量分配内存,但是注意像下面这样的代码:

public static int value=123;

Value在准备阶段之后已经存在于内存中,但是其值为0,因为其赋值语句putstatic在类构造器方法()中,该方法是在初始化阶段才执行。

1.4解析

符号引用转化为直接引用的过程,一般是当某个字节码指令执行之前,对其相关的字段进行解析,比如对方法访问,在解析过程中会去查询其可访问权限。
对同一个符号引用进行多次解析请求非常正常,虚拟机会设置一个缓存存储解析过后的结果,并标志该符号已经被成功解析过一次了,后面地解析请求都会很快从缓存中拿到想要的直接引用。如果第一次解析都没成功,会向后面所有的解析请求发送异常信号。
但是对于invokedynamic方法不起作用,因为其表达了Java的动态性,只有当程序具体运行到哪个invokedynamic方法时,才知道这个方法真正的直接引用是什么,不能使用第一次存储的缓存。

符号引用

使用符号代表对一个对象的引用,这个符号可以是随便什么字符,只要虚拟机在查找想要的对象时能够通过这个符号找到对应的地址就行,且该对象不一定已经在内存中,和虚拟机以后分配内存的操作基本不相关,就是一个表达地址的方式。

直接引用

直接指向对象的指针,句柄等,出现直接引用说明引用对象肯定在内存中了,和分配内存操作息息相关。

1.4.1类或接口的解析

当解析类的时候会经历以下步骤:
1、 如果要解析的是非数组类或接口的符号引用,则将该符号引用代表的全限定名发给类加载器,类会经历加载,验证等过程
2、 如果是类元素的数组类型,先将元素类型的符号引用全限定名发给类加载器,再在内存中生成该数组的组件类型如java.lang.Integer,当两步完成后,虚拟机生成代表其元素和维度的
3、 上两步如果没有异常,那么该类C在内存或是虚拟机中就已经是个有效的类或接口了,但是还需要查验该类所在的运行类D是否对C有访问权限
访问权限一般有三种情况(引入模块化之后):
1、 C和D在同一模块且C为public
2、 C为public,C和D不在同一模块,但D的模块对C的模块有访问权限。
3、 C不为public,但C和D在同一个包

1.4.2字段解析

1、因为java是面向对象编程的,所有东西都是由类构成,那么我们解析一个字段,首先去字段表查找它属于哪个类,查不到抛出异常,查到我们这里称它为C类。
2、如果C类中已经有被解析的描述符和简单名称匹配的字段,直接返回该字段的直接引用。
3、C类中没有,但C实现了接口或者说C类不是java.lang.Object,那就自下而上根据继承关系去查询其父类等有没有相同字段
4、否则,查找失败返回错误

1.4.3类方法解析

1、 和字段解析一样,先去方法表中查找方法所在的类或者接口的符号引用,但是类方法和接口方法的符号引用类型是分开的,如果在方法表中查出来的类是接口,则抛出异常,反之亦然。
2、 先查看类本身有没有描述符和简单名称都相匹配的方法,没有就看父类
3、 否则,查看该类实现的接口中是否含有匹配的方法 ,如果有则说明该类是抽象类。
4、 否则抛出异常

1.4.4接口方法解析

1、 先查接口方法表,找到所属接口的符号引用
2、 查找所属接口,继承的父接口中有没有简单名称和描述符都相匹配的方法,有就返回直接引用
3、 因为接口允许多重继承(一个接口继承好几个),所以对于第二步,可能会有很多父接口有相匹配的方法,那么返回其中一个就行了。
4、 否则抛出异常

1.5初始化

真正地开始执行java程序的代码,将主导权移交给应用程序,初始化阶段就是执行构造方法的过程。
方法是收集了类中变量和静态块赋值方法的集合,所以如果一个类没有赋值操作,那么它就不需要方法,因为收集的过程是顺序执行的,这就导致了静态语句块只能访问到之前定义的静态变量。
函数与构造函数不同,它不会显式地调用父类的,因为虚拟机会保证父类完了再开始类的,所以第一个初始的化的类一定是Object类。
但是接口的父接口在该接口不使用父接口的变量和方法时,就不需要初始化,甚至说实现某接口的类,只要没用这个接口内的东西,那这个接口就不需要初始化。
当某个对象时,会持有同步锁,防止多个线程初始化同一个对象。

初始化时机

只有遇到一下6种情况才需要在初始化阶段对类立刻进行初始化(这些场景被称为主动引用)
1、 New和静态字段
2、 主类还没被初始化的时候
3、 使用动态语言方法对应的类没有初始化时,需要初始化
4、 反射调用时
5、 类初始化时,其父类还未被初始化,这时赶紧初始化其父类
6、 某个接口含有default关键字修饰时,其实现类初始化了,那么这个接口也应该初始化

被动引用有三个例子:
1、 子类引用父类的静态字段,子类不会初始化
2、 用数组来定义类,类不会初始化
3、 调用常量,常量在编译时就存入了Class文件的常量池,调用这个常量和拥有其的类没有直接的引用关系,所以该类不会被初始化。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值