写点什么

☕【JVM 技术之旅】带你重塑对类加载机制的认识

发布于: 2021 年 05 月 25 日
☕【JVM 技术之旅】带你重塑对类加载机制的认识

📕 每日一句

极限就是为了超越而存在的,如何挑战自己的极限,只能苦练


📕 为什么又要写类加载器?

为什么有些一篇相关与对类加载器的文章?个人觉得之前的侧重点在于 ClassLoader 本身,以及双亲委托机制,而本篇更多站在 JVM 虚拟机的层面上去讲述和描述,前者侧重于使用和实际,后者本篇注重于原理和深入分析

📕 类加载的时机与作用

一段 java 程序在被执行的过程中,需要经历以下几个阶段:

📕 编译阶段

编译器将源码文件编译成 class 文件


class 文件是.java 文件的二进制字节流表示,在 class 文件中,包含了对应的类或接口的定义信息等常量池数据。


  • 内部存放的数据有:元数据常量池,访问标志,当前类索引、父类索引和接口索引的集合,字段表集合(类中声明的变量),方法表集合等,他们共同描述了一个类的信息

  • 每个 class 文件一定对应一个类,但反过来未必成立例如,动态生成的类信息,直接生成二进制字节流送入类加载器完成类加载

  • 因此广义上来讲,class 并不一定要是一个 class 文件,也可以仅仅就是一串二进制字节流


📕 类加载阶段

  • class 文件本质上是对某个类的静态描述,他需要被加载到内存,转化成运行时数据,才能被虚拟机执行,这个加载到内存的过程就是类加载过程

  • 类加载完成之后,在方法区内存放了类的类型信息、常量、静态变量(jdk8 之后随类对象存储在堆内)等信息,在堆中存放了与 class 文件对应的 Class 对象。通过 Class 对象,可以获取到类的字段、方法、构造器等信息,它是反射的基础。


📕 类加载的作用

类加载在程序执行的过程中起到了承上启下的作用将静态的二进制字节流数据转化为了运行时数据供执行引擎去操作数据


如图,加载-验证-准备-解析-初始化这五个阶段都属于类加载过程。


📕 类加载的时机

虚拟机规范并没有严格规定什么时候开始类加载。但是,规定了 6 种必须对类进行初始化的情况,它们被称为主动引用


!!!由于初始化类对象需要在加载、验证、准备之后进行,因此这三步必然要在这之前完成。这里前 4 种是非常常见的,需要深刻掌握


遇到new, getstatic, putstatic, invokestatic这四条字节码指令的时候,如果类型还没有被初始化,则需要初始化


  • new :实例化对象(对象实例调用表达式所创建的对象)

  • getstatic/putstatic: 读取/设置类的静态字段(被 final 修饰的静态常量除外)

  • invokestatic: 调用类的静态方法

📕 其他的初始化条件


  • unsafe 方法进行调用对象操作

  • clone 方法进行操作,进行申请

  • 通过文件 IO 的 ObjectInputStream/ObjectOutputStream 进行处理构造

  • 通过反射对类进行调用的时候,需要确保类已经被初始化过。也好理解,反射的核心是 Class 对象

  • 当前类被初始化时,要先确保其父类已被初始化

  • 虚拟机启动时,要执行的主类(包含 main 方法的那个类)要先被初始化

  • 接口中定义了默认方法(被 default 修饰,可以有方法体的方法,比较少见),当该接口的实现类初始化时,该接口需要先被初始化


除了以上的情况之外,所有其他对类的引用都不会触发类的初始化,他们被称为被动引用。

📕类加载的过程

加载


  • 加载是类加载过程的第一步,在类加载器的控制下,将二进制字节流转化为运行时数据。加载阶段需要完成 3 件事。

  • 根据类的全限定名获取对应的二进制字节流/定义对应的二进制数据流。

  • 将二进制字节流转化成方法区的运行时数据结构。

  • 在堆中创建代表这个类的 java.lang.Class 对象 ,作为方法区内数据的访问入口

  • 这里二进制字节流的获取,有多种方式,源文件也可以有多种形式。比较常见的形式有

  • 从压缩包中获取,如 jar 包,war 包等。

  • 在程序运行时,动态计算产生。应用场景:动态代理。

  • 最常见的,编译.java 文件生成.class 文件


验证


验证的作用是确保 Class 文件内的信息符合虚拟机规范的要求,保证程序运行过程中的安全。


准备


为类变量(即静态变量)分配内存,并设初始值。(0, null, false ...)。


有两点需要留意:


  • 从概念上讲,应当在方法区内为静态变量赋初值(常量会执行定制化赋值,不是单纯的默认值),但实际上 jdk8 以后,静态变量随着类对象一起存放在堆内存中

  • 准备阶段并不会为非静态变量(即实例变量)分配内存,实例变量会在对象实例化的时候,分配内存并赋初始


解析


将运行时常量池中符号引用替换成直接引用。


举个例子,在解析完成之前,被引用的目标还没有被加载到内存中,只能先用一个符号来表示,如"java.lang.Object"。


  • 解析的作用就是,在引用的对象被加载到内存中以后,将引用替换成指向该对象的指针或句柄需要被解析的引用有:类或接口的解析、字段解析、方法解析、接口方法解析。

  • 解析的发生时间并没有严格规定,它并非一定发生在准备和初始化之间动态链接或者动态加载、动态分派等功能实现的场景会延迟到运行阶段)。


初始化


  • 在初始化之前,加载-验证-准备这 3 步必然是完成了,部分的解析工作也可能完成了。

  • 准备阶段:对类对象中的类变量都是系统默认的初始值(常量会直接赋值,不会等到运行阶段)。

  • 初始化阶段:对类变量赋予我们在代码中指定的值


在初始化阶段,需要执行类构造器(与实例对象的构造器区分开来)。类构造器并非我们直接编写的方法,而是编译器收集类变量的赋值语句和 static 代码块的产物。


初始化阶段就是对静态变量赋值和执行静态代码块的过程(父类会在前,子类会在后)


实例化阶段就是执行实例构造器和实例代码块(构造代码块)的过程

📕 执行指令(不是重点)

在这个阶段,字节码执行引擎根据指令,去操作内存中的数据,完成计算任务.

用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
☕【JVM 技术之旅】带你重塑对类加载机制的认识