Class类文件的结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(类和接口也可以用反射的方式通过类加载器直接生成)

Class文件时一组以8位字节为基础单位的二进制流,各个数据都严格按照顺序紧凑排列在Class文件中,没有任何分隔符。

Class文件格式采用一种类似C语言结构体的伪结构存储数据,这种结构中只包含无符号数两种类型。

无符号数

  • 无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数
  • 无符号数可以用来描述数字、引用、数量值或者按照utf编码的字符串值。

  • 表是由多个无符号整数或者其他表构成的符合数据类型,都由"_info"结尾。

  • 表用于描述有层次关系的复合结构的数据,整个Class文件实际上就是一张表。

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

编辑器用16进制打开类文件

0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F
ca fe ba be 00 00 00 34 00 3f 0a 00 0a 00 2b 08
00 2c 09 00 0d 00 2d 06 40 59 00 00 00 00 00 00
09 00 0d 00 2e 09 00 2f 00 30 08 00 31 0a 00 32....

magic

类文件第一个数据为u4,我们查看16进制文件前4个字符是cafebabe,它用来确定这个文件是否为一个能被虚拟机接受的Class文件。

minor_version、major_version

u4后的两个u2,即00 00 00 34用来代表jdk的主次版本。

常量池

常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据,也是Class文件中第一个出现的表类型数据项目。

存放类型

存放类型包含:

  • 字面量:文本字符串、声明为final的常量值等。
  • 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。不会保存它们最终的信息,因为这是无意义的,它们不经过运行期转换的话得不到真正的入口地址,也就无法被虚拟机使用

类加载的时机

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括以下 7 个阶段:加载、验证、准备、解析、初始化、使用、卸载

加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始(注意是“开始”,而不是“进行”或“完成”),而解析阶段则不一定:它在某些情况下可以在初始化后再开始,这是为了支持 Java 语言的运行时绑定(多态)。

对类进行初始化的情况

虚拟机规范严格规定了有且只有5钟情况必须立即对类进行初始化:

  • 使用 new、getstatic、putstatic、或invokestatic这四条字节码命令时,后三个命令分别代表对类的静态变量进行操作,调用类的静态方法。生成这四条指令最常见的场景为:new一个对象的时候、读取或者赋值给类的静态变量的时候(被final修饰的除外,因为已经在编译期把结果放入了常量池)、以及调用一个静态方法的时候。

  • 反射调用类的时候,如果类未被初始化需要进行初始化

  • 当实例化某类时,其父类没被初始化,需要初始化父类

  • 当虚拟机启动时,用户指定的执行的主类(包含main方法的类),虚拟机会先初始化这个主类

  • 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发其初始化。

这 5 种场景称为对一个类进行主动引用(有且只有这五种才可以触发类的初始化),除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用

被动引用反例

/**
* 被动引用 Demo1:
* 通过子类引用父类的静态字段,不会导致子类初始化。
*/
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
// SuperClass init!
}
}

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

/**
* 被动引用 Demo2:
* 通过数组定义来引用类,不会触发此类的初始化。
*/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];
}
}

new数组对象时并不会触发SuperClass类的初始化,而是在这段代码里触发一个名为Lorg.fenixsoft.classloading.SuperClass的类初始化,他直接继承自Object类,由虚拟机来产生和触发。

/**
* 被动引用 Demo3:
* 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
*/
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLO_ZHIYIN = "Hello ZhiYin";
} public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO_ZHIYIN);
}
}

JVM在编译期进行了传播优化,将ConstClass类中的常量放入了NotInitialization的常量池中,事实上这个常量已经和ConstClass类没有了联系,不会触发初始化。

类加载的过程

类加载过程包括 5 个阶段:加载、验证、准备、解析和初始化。

加载

“加载”是“类加载”过程的第一步,在加载阶段,虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名(com.zhiyin.TestClass)来获取定义此类的二进制字节流

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

  • 在内存中(HostSpot在方法区)生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

获取二进制字节流

对于 Class 文件,虚拟机没有指明要从哪里获取、怎样获取。除了直接从编译好的 .class 文件中读取,还有以下几种方式:

  • 从 zip 包中读取,如 jar、war等
  • 从网络中获取,如 Applet
  • 通过动态代理技术生成代理类的二进制字节流
  • 由 JSP 文件生成对应的 Class 类
  • 从数据库中读取,如 有些中间件服务器可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

“数组类”与“非数组类”加载情况的不同

  • 非数组类由加载器来进行加载
  • 数组类由于没有字节流,由jvm直接创建,如果数组中的对象是引用类,递归采用加载器进行加载

注意事项

  • 虚拟机规范未规定 Class 对象的存储位置,对于 HotSpot 虚拟机而言,Class 对象比较特殊,它虽然是对象,但存放在方法区中。
  • 加载阶段与连接阶段的部分内容交叉进行,加载阶段尚未完成,连接阶段可能已经开始了。但这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证意义

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

验证过程

  1. 文件格式验证:第一阶段是验证字节流是否符合Class文件的规范

    • 是否以0xCAFEBABE开头
    • 主次版本号是否能被当前版本虚拟机处理
    • 常量池中常量是否有不被支持的类型
    • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
    • Class文件中各个部分是否有被删除或额外添加的内容等....
  2. 元数据验证:第二阶段是对字节码描述的信息进行语义分析,保证符合Java语言要求

    • 这个类是否有父类(除了Object之外都应该有父类)
    • 这个类是否继承了不允许被继承的类(final类)
    • 如果不是抽象类是否实现了父类或接口中要求被实现的方法
    • 类中的字段和方法是否与父类发生矛盾(同名同参函数等)....
  3. 字节码验证:本阶段是验证过程中最复杂的一个阶段,是对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。

  4. 符号引用验证:最后一个阶段的验证时发生在虚拟机将符号引用转化为直接引用的时候。这个动作在连接的第三阶段——解析阶段中发生,校验以下内容:

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

    符号引用验证如果没有通过,会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如常见的java.lang.NoSuchFieldError、java.lang.NoSuchMethodError

注意事项

对于虚拟机的类加载机制而言,验证是一个很重要的、但不是必须的(因为对程序运行期无影响)一个阶段,如果运行的全部代码(包括自己编写的以及第三方包中的代码)都已经被反复使用和验证过,那么在实施阶段就可以使用-Xverify:none来关闭大部分类的验证过程,以缩短虚拟机类加载的时间

准备

准备阶段是正式为类变量(被 static修饰的变量)分配内存并设置类变量初始值(通常为零值,引用类型为null)的阶段,这些变量所使用的内存将在方法去区中进行分配。如下语句中:

public static int value = 666;

value变量在准备阶段之后初始值变为0而不是666,变为666的过程是在初始化阶段进行。

上面说到通常情况下是零值,特殊情况为该变量同时被final修饰,是常量。

public static final int value = 666;

编译时value就会生成ConstantValue属性(定义为常量),在准备阶段虚拟机就会依据ConstantValue的设置将value赋值为666.

解析

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

class二进制字节流中的引用关系都是符号引用没有真正的意义,解析之后将会变成直接指向目标的指针。

初始化

类初始化阶段是类加载过程的最后一步,是执行类构造器方法的过程。

类构造器方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。如下方代码所示:

public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.println(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}

类构造器方法不需要显式调用父类构造器,虚拟机会保证在子类的类构造器方法执行之前,父类的类构造器方法方法已经执行完毕。

由于父类的类构造器方法方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。如下方代码所示:

static class Parent {
public static int A = 1;
static {
A = 2;
}
} static class Sub extends Parent {
public static int B = A;
} public static void main(String[] args) {
System.out.println(Sub.B); // 输出 2
}

类构造器方法不是必需的,如果一个类没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成它。

接口中不能使用静态代码块,但接口也需要通过类构造器方法为接口中定义的静态成员变量显式初始化。但接口与类不同,接口的类构造方法不需要先执行父类的类构造方法方法,只有当父接口中定义的变量使用时,父接口才会初始化。

虚拟机会保证一个类的类构造方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造方法。

参考《深入理解Java虚拟机》、Jvm官方规范

jvm字节码和类加载机制的更多相关文章

  1. Java基础篇(JVM)——字节码详解

    这是Java基础篇(JVM)的第一篇文章,本来想先说说Java类加载机制的,后来想想,JVM的作用是加载编译器编译好的字节码,并解释成机器码,那么首先应该了解字节码,然后再谈加载字节码的类加载机制似乎 ...

  2. Java finally语句到底是在return之前还是之后执行(JVM字节码分析及内部体系结构)?

    之前看了一篇关于"Java finally语句到底是在return之前还是之后执行?"这样的博客,看到兴致处,突然博客里的一个测试用例让我产生了疑惑. 测试用例如下: public ...

  3. 一夜搞懂 | JVM 字节码执行引擎

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习字节码执行引擎? 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一 ...

  4. JVM总结(五):JVM字节码执行引擎

    JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ...

  5. JVM源码分析-类加载场景实例分析

    A类调用B类的静态方法,除了加载B类,但是B类的一个未被调用的方法间接使用到的C类却也被加载了,这个有意思的场景来自一个提问:方法中使用的类型为何在未调用时尝试加载?. 场景如下: public cl ...

  6. JVM 字节码执行实例分析

    前言 最近在看<Java 虚拟机规范>和<深入理解JVM虚拟机>,对于字节码的执行有了进一步的了解.字节码就像是汇编语言,是 JVM 的指令集.下面我们先对 JVM 执行引擎做 ...

  7. 从JVM字节码执行看重载和重写

    Java 重写(Override)与重载(Overload) 重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变.即外壳不变,核心重写! 重写的 ...

  8. JVM 字节码(四)静态方法、构造代码、this 以及 synchronized 关键字

    JVM 字节码(四)静态方法.构造代码.this 以及 synchronized 关键字 一.静态代码 public class ByteCodeStatic { private static fin ...

  9. JVM 字节码(三)异常在字节码中的处理(catch 和 throws)

    JVM 字节码(三)异常在字节码中的处理(catch 和 throws) 在 ClassFile 中到底是如何处理异常的呢? 一.代码块异常 catch catch 中的异常代码块在异常是如何处理的呢 ...

随机推荐

  1. vue使用 video.js动态切换视频源视频源不刷新问题

    网上的垃圾代码太多,最后翻了video.js的官方文档,就这么简单,浪费了我这么久,注:我这里使用的vue //html <video  id="my-player" con ...

  2. 大话计算机网络一 聊聊UDP

    引言 UDP是一个简单的面向数据报的运输层协议 UDP不提供可靠性,它把应用程序传给IP层得数据发送出去,不保证它们能达到目的地 UDP首部 端口号表示发送进程和接受进程. UDP长度字段指的是UDP ...

  3. 20184302 2019-2020-2 《Python程序设计》实验四报告

    20184302 2019-2020-2 <Python程序设计>实验四报告 课程:<Python程序设计> 班级: 1843 姓名: 李新锐 学号:184302 实验教师:王 ...

  4. 🧑🏻‍💻数据库简介及Mac平台环境搭建🧑🏻‍💻

    数据库 存储数据的演变过程 如果没有使用数据库,我们自己存放文件,数据格式是千差万别的,完全取决于我们自己,例如: """ # 张三 zhangsan|123|read ...

  5. VS Code项目中共享自定义的代码片段方案

    VS Code项目中共享自定义的代码片段方案 一.问题背景 项目中注释风格不统一,如何统一注释风格 一些第三方组件库名称太长,每次使用都需要找文档,然后复制粘贴 部分组件库有自己的Snippets插件 ...

  6. 大数据之Hudi + Kylin的准实时数仓实现

    问题导读:1.数据库.数据仓库如何理解?2.数据湖有什么用途?解决什么问题?3.数据仓库的加载链路如何实现?4.Hudi新一代数据湖项目有什么优势? 在近期的 Apache Kylin × Apach ...

  7. BT.656视频信号解码

    BT.656视频信号解码   BT.656协议标准 ITU-R BT.601和ITU-R BT.656是ITU-R(国际电信联盟)制定的标准.严格来说ITU-R BT.656是ITU-R BT.601 ...

  8. WEditor(元素定位工具)安装和定位界面元素

     1. 安装adb(安装方法——百度网盘(无邪)) 2. 安装python-uiautomator2 pip install --pre -U uiautomator2 3. 手机设备安装atx-ag ...

  9. Jupyter notebook中的Cell and Line Magics

    参考资料: https://www.jianshu.com/p/81ada9234788 https://my.oschina.net/u/2306127/blog/832510 首先,Cell an ...

  10. 通过char与varchar的区别,学习可变长的字符类型

    转自http://www.uphtm.com/database/232.html 在mysql教程中char与varchar的区别呢,都是用来存储字符串的,只是他们的保存方式不一样罢了,char有固定 ...