版权声明:本文为北京尚学堂原创文章,未经容许不得转载。前端
做为一名程序猿 ,咱们天天都在写Code,但你真的了解它的生命周期么?今天就来简单聊下它的生命历程,提及一段Java Code,从出生到game over大致分这么几步:编译、类加载、运行、GC。java
Java语言的编译期实际上是一段“不肯定 ”的过程,由于多是一个前端编译器把.java文件转变为.class文件的过程;也多是指JVM的后端运行期编译器(JIT编译器)把字节码转变为机器码的过程;还多是指使用静态提早编译器(AOT编译器)直接把.java文件编译成本地机器码的过程。可是在这里咱们说的是第一类。也是符合咱们大众对编译认知的。编译在这个时间段经历了哪些过程呢?算法
词法分析是将源代码的字符流转变为Token集合,而语法分析则是根据Token序列抽象构造语法树(ATS)的过程,ATS是一种用来描述程序代码语法结构的树形表示形式,语法树的每一个节点都表明着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至代码注释均可以是一个语法结构。数据库
完成了语法和词法分析以后,下一步就是填充符号表的过程,符号表中所登记的信息在编译的不一样阶段都要用到。在这里延伸一下符号表的概念。符号表是什么呢?它是由一组符号地址和符号信息构成的表格,最简单的能够理解为哈希表的K-V值对的形式。为何会用到符号表呢?符号表最先期的应用之一就是组织程序代码的信息。最初,计算机程序只是一串简单的数字,但程序猿们很快发现使用符号来表示操做和内存地址(变量名)要方便得多。将名称和数字关联起来就须要一张符号表。随着程序的增加,符号表操做的性能逐渐变成了程序开发效率的瓶颈,为此从而诞生了许多提高序号表效率的数据结构和算法。至于所谓的数据结构和算法有哪些呢?大致说下:无序链表中的顺序查找、有序数组中的二分查找、二叉查找树、平衡查找树(在这咱们主要接触到的是红黑树)、散列表(基于拉链法的散列表,基于线性探测法的散列表)。像Java中的java.util.TreeMap和java.util.HashMap分别是基于红黑树和拉链法的散列表的符号表实现的。这里提到的符号表的概念再也不细说,感兴趣的能够查找相关资料。编程
通过上两步以后,咱们得到了程序代码的抽象语法树表示,语法树能表示一个正确的源代码抽象,但没法保证源程序是符合逻辑的,这时候语义分析登场了,它的主要任务就是对结构上正确的源程序进行上下文有关性质的审查。标注检查、数据及控制流分析、解语法糖是语义分析阶段的几个步骤,在这具体说下语法糖的概念。语法糖是指在计算机语言中添加的某种语法,这种语法对语言的功能并无影响,但更方便程序猿使用。Java中最经常使用的语法糖主要是泛型、变长参数、自从装箱/拆箱、遍历循环,JVM在运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程也就是解语法糖。举个泛型擦除的例子,List<Integer>和List<String>在编译以后会进行泛型擦除,变成同样的原生类型List<E>。后端
字节码生成是Javac编译过程的最后一个阶段,在这个阶段会把前面各步骤生成的信息转化成字节码写到磁盘中,还会进行了少许代码添加和转换的工做。实例构造器<init>()方法和类构造器<clinit>()方法(这里的实例构造器并非指默认构造函数,若是用户代码没有提供任何构造函数,那编译器将会添加一个没有参数的、访问性与当前类一致的默认构造函数,这个工做在填充符号表阶段已经完成,而类构造器<clinit>()方法指的是编译器自动收集类中的全部类变量赋值动做和静态语句块中的语句合并产生的)就是在这个阶段添加到语法树中的。到此为止整个编译过程结束。数组
编译将程序编译成字节码以后,下一步就是类加载到内存的过程。安全
类加载的过程是在虚拟机内存的方法区进行,这地方涉及到虚拟机内存,因此在这首先简单介绍下程序在内存区域分布的概念。虚拟机内存区域划分为:程序计数器、栈、本地方法栈、堆、方法区(部分区域为运行时常量池)、直接内存。微信
程序计数器是一块较小的内存空间,它能够看作是当前线程所执行的字节码的行号指示器。在JVM概念模型中,字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令。网络
栈用于存储局部变量表、操做数栈、动态连接、方法出口等信息。其中局部变量表存放了编译期克制的各类基本数据类型、对象引用。它与程序计数器同样都是线程私有的。
本地方法栈与上面介绍的虚拟机栈做用类似,它们的区别不过是虚拟机栈为虚拟机执行Java方法(字节码)服务,而本地方法栈则为虚拟机使用的Native方法服务,甚至有的虚拟机会把这两块合二为一。
堆是JVM管理内存最大的一块。它是被全部线程共享的一块区域,它的惟一目的是存放对象实例,几乎全部的对象实例都在这里分配内存(像特殊的类对象会在方法区分配内存)。这地方也是垃圾收集管理的主要区域,从内存回收角度看,如今垃圾收集器都采用分代收集算法(后面会详细介绍),因此Java堆还能够进一步细分:新生代和老年代,而新生代进一步细分:Eden空间、From Survivor空间、To Survivor空间。为了效率考虑,堆还可能划分为多个线程私有的分配缓冲区(TLAB)。不管如何划分,都与存放内容无关,不管哪一个区域,存放的依然是对象实例,它们存在的目的只是为了更好的回收和分配内存而已。
方法区与堆同样,都是线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。而运行时常量池是方法区的一部分,它主要用于存放编译期声明各类字面量和符号引用。
直接内存并非虚拟机运行时数据区的一部分,也是不Java规范中定义的内存区域,你能够简单理解为堆外内存,内存分配不受Java堆大小的限制但受整个内存大小的限制。
说完了虚拟机内存区域的概念,咱们回到正题,类加载的流程究竟是什么呢?加载、验证、准备、解析、初始化五步。其中加载、验证、准备、初始化是顺序执行的,而解析则不必定,它有可能会在初始化以后执行。
在加载阶段,JVM须要完成三个步骤:首先经过类的全限定名来获取定义此类的二进制字节流,而后将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构,最后在内存中生成一个表明这个类的java.lang.Class对象,做为方法区这个类的各类数据入口。在第一步获取二进制字节流中并无明确指出从一个*.class文件中获取,规定的灵活性致使咱们能够从ZIP(为JAR、EAR/WAR格式提供基础)包中获取,从网络获取(Applet),运行时计算生成(动态代理),其余文件产生(JSP文件生成的Class类),从数据库获取。
验证,顾名思义,其实就是为了确保Class文件字节流中包含信息符合JVM的要求,由于Class文件的来源途径不必定中规中矩的从编译器产生,也有可能用十六进制编辑器直接编写Class文件。校验流程为文件格式校验、元数据验证、字节码验证,这地方的具体安全校验方式再也不细说。
准备阶段正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都在方法区进行分配。
解析阶段是JVM将常量池内的符号引用替换为直接引用(指向目标的指针、相对偏移量或句柄)的过程,前面咱们谈到的编译填充符号表的价值在这地方体现出来了。解析过程无非就是对类或接口、字段、接口方法进行解析。更多精彩内容关注微信公众号:北京尚学堂(858568103)
类初始化阶段是类加载过程的最后一步,在准备阶段,变量已经赋过一次初始值,而在这一步,则会根据程序猿定制的要求进行初始化类变量和其余资源。在这个阶段就是执行前面编译字节码生成流程提到的<clinit>()方法的过程。虚拟机也保证在多线程环境下这个方法被同时调用时被正确的加锁、同步,保证只有一个线程去执行这个方法而其余线程阻塞等待。这地方还涉及到另外一个咱们比较关心的知识点,Java什么时候触发对类的初始化操做呢?
通过了上面两个阶段,程序开始正常跑起来了,咱们都知道程序执行过程涉及到了各类指令的计算操做, 程序如何执行的呢?这地方就会使用到文章开头谈到的后端编译器(JIT即时编译器)+解释器这种搭配使用的混合模式(HotSpot虚拟机默认采用了解释器与一个编译器),字节码执行引擎则负责着这类各类程序计算操做的任务,它在执行Java代码的时候有可能会有解释执行(经过解释器执行)和编译执行(经过即时编译器产生本地代码执行)两种选择,也可能二者兼备。栈帧是用于支持虚拟机进行方法调用和执行的数据结构,具体的压栈弹栈各类指令计算的思路涉及到了一个经典的算法——Dijkstra算法,至于如何执行有兴趣的本身查资料吧这地方不会过多深刻。运行期的优化问题在这个阶段一样重要,而JVM设计团队则把对性能的优化集中到了这个阶段,这样可让那些不是由Javac产生的Class文件一样享受到编译器优化带来的好处,至于具体的优化技术有哪些呢?有不少,这里简单提几个具备表明性的优化技术:公共子表达式消除、数组边界检查消除、方法内联、逃逸分析等等。
也许你们对一段Java Code的生命史有点概念了,Java程序的生命史也就给你们介绍到这里,从Jdk的安装到配置,再到后面的项目编程,大体也就是如此,小编有幸在北京尚学堂学学Java,更多精彩内容关注微信公众号:北京尚学堂。