首先要知道,我说的这个类,也就是我们上篇内容说的那个
Class 文件
通常指一串 “二进制字节流”。
从我们码出 public class Cafe
的时候,可以说。这个种子 bean
就已经埋下了。然后我们将它通过前端编译器 javac
编译成了 Class
文件 。
紧接着它被 JVM
加载,后被应用到程序中,最后被 JVM
卸载。短短的几句话,概括了 Java 类
的一生。但这其中每一步,都有着可以用来喝一宿的故事。今天我还是长话短说,少喝点,讲一讲 Java
类的一生。
当你敲完最后一个 ;
或 )
时,你高兴的执行了你的代码。而此时,类
的一生,算是开始了。
类
首先经历的就是被 Javac
这个前端编译器进行编译。因为这样,它才能被 JVM
所使用,这一步 Javac
都对它做了什么呢?
Javac
把 .java
文件中的每个 标记
通过词法和语法分析构建出一颗抽象语法树。我们在编码的时候,都是敲下 abcde
的单个字符为最小单元,但在 Javac
这里,最小的单元就是 public
int
a
=
11
,这些,每一项都会作为一个 标记
。把每个单元转成 标记
集合的过程称为词法分析,根据 标记
构建出来抽象语法树这个过程叫语法分析。有了这颗 树
就方便后面的操作了。
对刚刚的语法树进行遍历,将出现的符号定义和符号信息保存到符号表中;
这一步是干啥呢,这么跟你说吧, lombok
用过吗?就干那个了。没错,代码的修改。这些还要得益于 插入式注解处理器
,它可以让你很轻松的来操作第一步生成出来的那颗抽象语法树,来达到对代码进行额外的操作。哦对了,如果这一步有对抽象语法树进行过操作,那么需要重新执行填充符号表的动作。
对程序进行语义逻辑分析。也是我们经常说的编译报错的地方,就在这了。比如你写个 int i = 0
boolean j = false
int k = i + j
你这样写肯定没问题,不过在编译的时候,就会报错了。不要说你在 IDE
中写的时候就会爆红。细品。还有一个例子,就是你定义的 final
的变量。你在编写的时候,是必须要初始化,而且不允许再被修改的,这个值有没有被改过,也是在这个阶段来检查的。你不要说在 IDE
编写的时候改就会爆红。
关于 Java 的语法糖,我们几乎只要在编码,就会多多少少的使用到,比如 泛型
、自动拆装箱
、增强 for 循环
。这一步就是将其还原成他们本来的样子。
经历了上面几步之后,最终会得到一个 .class
的字节码文件。其使用的就是第二步填充的那个符号表的信息。这一步比较关键的内容就是生成 方法和 方法。类构造器和实例构造器。这个实例构造器和我们代码中的构造函数不同。 和 的作用主要是代码收敛,比如 可以确定父类的 static
代码块一定先于子类执行。
关于符号表的这里,我相信大家很多人都看过一些相关的博文或资料,不过几乎无人谈及这个符号表里到底是个什么东西,所以,我也就不说了。那必不可能,其实这个符号表是编译原理层面的内容,还是需要了解一下编译原理的这块内容才行,不过我之前也说过,关于编译原理这块自己真的是一概不知。所以这没办法在这里写清楚,不过抱着技术分享的认真态度,还是去了解了一些,这块还是相对较基础的。所谓的符号表,在编译原理中,它是讲,将程序中出现的有关标识的符号的属性信息保存下来。主要是用于语法分析和内存分配阶段。保存的形式也不单一,可以用数组、散列表、栈、树等数据结构来进行登记。内容的话,比如 Java 中的方法为例
public void fun(param1,param2)
那么在符号表中就要保存fun
与之对应的值,param1
与之对应的值,以此类推。这里的举例为个人理解所写,真正编译原理所表达的符号与之类似。如感兴趣可进行编译原理内容学习。
当编译器 Javac 孕育出 .class 字节码之后,接下来类的生命周期就围绕以下 加载 - 验证 - 准备 - 解析 - 初始化 - 使用 - 卸载 7步 展开了。
将字节码二进制流加载到内存,当我们的代码经历过前端编译器,便成为了可以被虚拟机加载的字节码文件。当然,这个类,可能是我们自己写的(编码)也有可能是通过运行时生成的字节码内容,所以前面说加载的时候说的是 将字节码二进制流加载到内存
,而不是 class
文件加载到内存,因为字节码包含了各种形式的内容,比如 class
文件,或者本地或网络传输的二进制流等。
加载的过程中,主要是将这个字节码二进制流转换成虚拟机所能使用的信息,基本内容包含
以上的操作,是由一个叫做 类加载器 的家伙完成的,关于这哥们,待会我们再好好看看它。
验证加载的字节码内容是否合法,这一步主要是防止虚拟机遭到破坏
准备阶段为类中定义的静态变量(类变量)分配内存并赋默认值(非程序中的默认值)
public static int id = 123
默认值赋值 0 而非 123<clinit>()
方法中完成,public static final int id = 123
会在此阶段完成赋值继续深入:
123 会在对应程序的类构造器(注意与实例构造器区别)中的<clinit>()
方法中完成,这个 <clinit>()
方法是由 javac 编译器编译出来的方法
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问<clinit>()
方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()
方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()
方法执行前,父类的<clinit>()
方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()
方法的类型肯定是java.lang.Object<clinit>()
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作<clinit>()
方法这步操作<clinit>()
方法在多线程环境会被正确同步解析阶段将类的符号引用替换为直接引用
对于类加载,虚拟机规范没有严格要求,具体可由虚拟机的实现自行决定。
但对类,虚拟机规范明确了类必须初始化的 6 个场景(有且仅有)
main()
方法的类)会被初始化。上面说的是 6 种 主动 方式触发的初始化,还有 3 种 被动 引用不会引发类的初始化。
static final
共同修饰的字段。这个原因上面有说明过,在这里再墨迹一下,会在编译器 javac 阶段将其放入常量池。就是使用咯。
还记得之前我们写的一篇文章 JVM 是怎么把“送”出去的内存又“要”回来的 其中有写到 类卸载 的相关内容,没读过的朋友可以点击链接阅读全文,效果更好,也可以阅读下面截取的片段。
类的卸载还是比较严格的,而且这个条件也比较苛刻,判断一个类型信息是否可以被回收(卸载)需要 同时 满足以下三个条件:
具体内容如下:
多了解一点:
其中第三点与我们经常见到的操作诸如 spring 这种大量使用反射的框架、JDK 的动态代理、以及 CG lib 这种操作字节码的框架基本上都需要 JVM 拥有类卸载的功能,否则会导致一些自定义加载器加载的临时类信息占据着方法区的内存,带来不必要的压力。
当类被卸载后,类的一个生命周期就结束了,还有,上面的顺序并非一成不变,不过 加载 - 使用 - 卸载,这个大的框架顺序还是必须这样的,简单说明下这是因为 Java 动态语言的支持导致的。以上,就是我这次要分享的主要内容 Java 类的一生,谢谢你的阅读。
下面我们简单扩展一下类加载这哥儿们。
关于这哥们,它的工作很简单,只需要负责把二进制流加载到内存中,哦对了,加载完了会打个标。完事。
打标就是通过类的 完全限定名 来为每个 Class文件的二进制流
命名,这样做是可以方便应用程序加载自己需要的类,还有一个原因我们下面会看到。
类加载器只实现类的加载动作。
类之间的比较,前提条件是同一个类加载器。如果由不同类加载器加载的相同完全限定名的类,那他们也是完全不同的(打标的原因其二)。也不能这样去做比较。
负责加载 JAVA_HOME/lib 目录下的 jar,注意:识别方式为文件名识别,即使放入不符合规范的文件也不能被加载。 该加载器不能被 Java 程序引用,访问时会返回 null
负责加载 JAVA_HOME/lib/ext 目录下的类,可被程序调用。也可加载 java.ext.dirs
系统变量所指定的路径中所有的类库。
这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”
它负责加载用户类路径(ClassPath)上所有的类库
可以直接在代码中使用这个类加载器。
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
不遵守这个模型规则就可以破坏双亲委派模型
关于 SPI 的话,我们后面有机会在深入聊聊,因为其不属于虚拟机范畴,暂时先不深入讨论了。这部分内容和 JNDI 有关(Java Naming and Directory Interface,Java命名和目录接口)是 Java 的一个关键技术点。不知道可不太好,关注我,一起学习。
(正文完)
如果觉得写的还不错,可以关注点赞在看支持我。(#.#)