|
|
51CTO旗下网站
|
|
移动端

看了这篇你还不懂JVM 中的类加载机制?

验证是连接阶段的第一个步骤,验证的目的是为了确保`.class`文件中的字节流所包含的信息是符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全的。

作者:泽泽vlog来源:今日头条|2020-05-20 22:13

开门见山

首先引入一道面试题

看了这篇你还不懂JVM 中的类加载机制?
  • 错误答案:count1=1;count2=1
  • 正确答案:count1=1;count2=0

为神马?为神马?这要从java的类加载时机说起。

本来是准备把分析结果写在最下面的但是怕大家没有耐心看到最后我这边先大概分析下,如果看不懂下面的分析。真心建议大家能看到最后,文章不算长。

  1. `Single single = Single.getInstance();`调用了类的`Single`调用了类的静态方法,触发类的初始化
  2. 类加载的时候在准备过程中为类的静态变量分配内存并初始化默认值 `single=null count1=0,count2=0`
  3. 类初始化化,为类的静态变量赋值和执行静态代码快。`single`赋值为`new Single()`调用类的构造方法
  4. 调用类的构造方法后`count=1;count2=1`
  5. 继续为`count1`与`count2`赋值,此时`count1`没有赋值操作,所有`count1`为1,但是`count2`执行赋值操作就变为0

类的加载时机

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)。

看了这篇你还不懂JVM 中的类加载机制?

其中加载、验证、准备、初始化和卸载五个步骤的顺序都是确定的,解析阶段在某些情况下有可能发生在初始化之后,这是为了支持 Java 语言的运行期绑定的特性。

何时开始类的初始化

  1. 创建类的实例
  2. 访问类的静态变量(除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(`constant variable`),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。
  3. 访问类的静态方法
  4. 反射如(`Class.forName("my.xyz.Test")`)
  5. 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化
  6. 虚拟机启动时,定义了main()方法的那个类先初始化
  • 主动引用:上面这些种行为称为对一个类的的主动引用,会触发类的初始化
  • 被动引用:除上面五种主动引用之外,其他引用类的方式都不会触发类的初始化,称为类的被动引用

接口的加载过程与类的加载过程稍有不同。接口中不能使用`static{}`块。当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有真正在使用到父接口时(例如引用接口中定义的常量)才会初始化。

被动引用例子

示例一

对于静态字段,只有直接定义这个字段的类会被初始化,如果是通过子类引用父类的字段,父类会被初始化,子类不一定会被初始化,子类会不会被初始化 JVM 虚拟机规范并没有明确规定,取决于虚拟机的具体实现

看了这篇你还不懂JVM 中的类加载机制?

上面代码运行之后输出结果如下所示

  1. SuperClass init! 
  2. The value is 24 

示例二

看了这篇你还不懂JVM 中的类加载机制?

上面代码运行之后,并不会输出 “SubClass init!“,因为在上面Demo#main()方法中,并没有初始化SubClass类,而是初始化了一个SubClass[]数组类,SubClass[]数组类代表了一个元素类型为SubClass的一维数组,继承自Object类,由newarray字节码创建。

示例三

看了这篇你还不懂JVM 中的类加载机制?

上面代码运行之后也并不会输出”Constant init!“,因为这涉及到一个概念 —- “常量传播优化”。虽然在代码中Demo类引用了Constant类中的常量VALUE,但是在编译阶段,会将VALUE的实际值”Hello World!“放到Demo类中的常量池中,Demo类每次使用”Hello World!“常量的时候都会从自己的常量池中去找。Demo类不会持有Constant类的符号引用,所以Constant类也并不会被初始化。

类的加载过程

加载

在加载阶段有三个步骤:

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将二进制字节流所代表的静态存储结构转换为方法区中的运行时数据结构
  3. 在内存中生成一个代表此类的`java.lang.Class`的对象,作为方法区这些数据的访问入口

在这个阶段,有两点需要注意:

  1. 并没有规定从哪里获取二进制字节流。我们可以从`.class`静态存储文件中获取,也可以从`zip、jar`等包中读取,可以从数据库中读取,也可以从网络中获取,甚至我们自己可以在运行时自动生成。
  2. 在内存中实例化一个代表此类的`java.lang.Class`对象之后,并没有规定此`Class`对象是方法`Java`堆中的,有些虚拟机就会将`Class`对象放到方法区中,比如`HotSpot`。

验证

验证是连接阶段的第一个步骤,验证的目的是为了确保`.class`文件中的字节流所包含的信息是符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全的。

`Java`语言本身是相对安全的语言,使用Java编码是无法做到如访问数组边界以外的数据、将一个对象转型为它并未实现的类型等,如果这样做了,编译器将拒绝编译。但是,`Class`文件并不一定是由`Java`源码编译而来,可以使用任何途径,包括用十六进制编辑器(如`UltraEdit`)直接编写。如果直接编写了有害的“代码”(字节流),而虚拟机在加载该Class时不进行检查的话,就有可能危害到虚拟机或程序的安全。

  1. 不同的虚拟机,对类验证的实现可能有所不同,但大致都会完成下面四个阶段的验证:文件格式验证、元数据验证、字节码验证和符号引用验证。文件格式验证,是要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。如验证魔数是否0xCAFEBABE;主、次版本号是否正在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型……该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区中,经过这个阶段的验证后,字节流才会进入内存的方法区中存储,所以后面的三个验证阶段都是基于方法区的存储结构进行的。
  2. 元数据验证,是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。可能包括的验证如:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法……
  3. 字节码验证,主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。
  4. 符号引用验证,发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析阶段”中发生。验证符号引用中通过字符串描述的权限定名是否能找到对应的类;在指定类中是否存在符合方法字段的描述符及简单名称所描述的方法和字段;符号引用中的类、字段和方法的访问性(`private、protected、public、default`)是否可被当前类访问

验证阶段对于虚拟机的类加载机制来说,不一定是必要的阶段。如果所运行的全部代码确认是安全的,可以使用`-Xverify:none`参数来关闭大部分的类验证措施,以缩短虚拟机类加载时间。

准备

准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

有几点需要注意:

1.在方法区中分配内存的只有类变量(被`static`修饰的变量),而不包括实例变量,实例变量将会跟随着对象在 Java 堆中为其分配内存

2.初始化类变量的时候,是将类变量初始化为其类型对应的`0`值,比如有如下类变量,在准备阶段完成之后`val`的值是`0`而不是 `123`,为 `val`复制为`123`,是在后面要讲的初始化阶段之后

  1. //在准备阶段value初始值为0 。在初始化阶段才会变为123 。 
  2. public static int val=123; 

3. 对于常量,其对应的值会在编译阶段就存储在字段表的`ConstantValue`属性当中,所以在准备阶段结束之后,常量的值就是`ConstantValue`所指定的值了,比如如下,在准备阶段结束之后,`val`的值就是`123`了。

  1. public static final int val = 123; 

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。

初始化

类的初始化阶段才是真正开始执行类中定义的 Java 程序代码。初始化说白了就是调用类构造器`()`的过程,在类的构造器中会为类变量初始化定义的值,会执行静态代码块中的内容。下面将介绍几点和开发者关系较为紧密的注意点

1. 类构造器`()`是由编译器自动收集类中出现的类变量、静态代码块中的语句合并产生的,收集的顺序是在源文件中出现的顺序决定的,静态代码块可以访问出现在静态代码块之前的类变量,出现的静态代码块之后的类变量,只可以赋值,但是不能访问,比如如下代码

看了这篇你还不懂JVM 中的类加载机制?

2. `()`类构造器和`()`实例构造器不同,类构造器不需要显示的父类的类构造,在子类的类构造器调用之前,会自动的调用父类的类构造器。因此虚拟机中第一个被调用的`()`方法是 `java.lang.Object`的类构造器

3. 由于父类的类构造器优先于子类的类构造器执行,所以父类中的`static{}`代码块也优先于子类的`static{}`执行

4. 类构造器`()`对于类来说并不是必需的,如果一个类中没有类变量,也没有`static{}`,那这个类不会有类构造器`()`

5. 接口中不能有`static{}`,但是接口中也可以有类变量,所以接口中也可以有类构造器 `{}`,但是接口的类构造器和类的类构造器有所不同,接口在调用类构造器的时候,如果不需要,不用调用父接口的类构造器,除非用到了父接口中的类变量,接口的实现类在初始化的时候也不会调用接口的类构造器

6. 虚拟机会保证一个类的`()`方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的类构造器`()`,其他线程会被阻塞,直到活动线程执行完类构造器`()`方法

【编辑推荐】

  1. 学习JVM参数前你需要了解些什么
  2. Java虚拟机之对象存活判断与垃圾回收算法
  3. Java虚拟机底层原理和流程,看懂你就掌握60%JVM
  4. JVM内幕:Java虚拟机详解
  5. 聊一聊JVM类加载子系统解毒
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

云计算从入门到上瘾

云计算从入门到上瘾

传统IT工程师的转型
共26章 | 51CTO阿森

197人订阅学习

从头解锁Python运维

从头解锁Python运维

多维度详解
共19章 | 叱诧少帅

345人订阅学习

Active Directory 架构规划实战

Active Directory 架构规划实战

4类AD架构规划
共15章 | wx5918591c0084b

324人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微