程序是一组计算机能够识别和执行的二进制指令。计算机能够识别和执行的永远都是二进制语言,这样子讲可能有点抽象了。举个简单的例子,比如我们用电脑打开图片,当图片在磁盘上时,它只是一个数据。当图片被打开了,图片会被转化为二进制数据装在到内存中,此时内存中的图片数据,其实就是机器可以识别和执行的了,确切来说,内存中的二进制数据,都是程序。

我们回到java程序这个话题上来,java程序也是程序。我们编写的xxx.java文件,是什么呢?我们称它为源代码。计算机是不能直接执行源代码的,因为机器不认识它啊。那怎么办呢?只好将源代码翻译成机器能识别的二进制代码了!这个翻译的过程,我们把它称之为“编译”。可是这种直接能运行的二进制代码,在不同的操作系统的运行方式是不同。JAVA程序为了达到所谓的“一次编写,到处运行”,那么想了一个中间一点的办法,先把源代码翻译为java字节码,也就是我们看到的.class文件啦,然后针对不同的操作系统搞了不同的JVM,这个JVM再负责把java字节码转化为计算机能够识别的二进制代码。

JVM实际上是一种抽象化的计算机体系结构,通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。不同的操作系统上有不同的JVM实现,所谓的跨平台,一次编写,到处编写就是这么来的。


java smail翻译 java代码在线翻译_java代码翻译器


我们都知道,源代码就是一段有规则的字符串,我们要把源代码,转换为JVM能够识别的字节码。肯定需要编译器来做这个事情。开动你OO的大脑,跟上节奏,一起分析分析。

你看哈,所有的源代码都是有规则的。比如java语言的源代码,有很多关键字,比如int, long,float…这些都代表了数据类型,这些关键字是不可分割的,是一种标记我们首先需要讲这些东西提取出来吧。这些都是不能分割的东西,是最小的元素,除了关键字,还有修饰符、变量名、运算符、字面量等等,这些东西比较多,我们可以考虑使用集合来存储它,一般这样的集合我们称之为token集合。这个提取token的过程我们叫做词法分析,最后啊,把源代码转化为一个树型结构的集合,我们称之为抽象语法树。

然后呢,java程序还有一些默认的事情需要做,比如一个类没有编写构造函数,这个时候,还要去填充默认的构造函数,再比如,对于一些annotation的处理等等,这一类操作都是建立在之前的抽象语法树之上噢。这个过程被叫做符号表填充,符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。符号表在编译程序工作的过程中需要不断收集、记录和使用源程序中一些语法符号的类型和特征等相关信息。这些信息一般以表格形式存储于系统中。如常数表、变量名表、数组名表、过程名表、标号表等等,统称为符号表。对符号名进行地址分配时,符号表是地址分配的依据。

源代码的信息经过收集和填充之后,是需要做一些检查的事情的,比如变量使用前是否已被声明、变量和赋值之间的数据类型是否匹配,这类事情被叫做标注检查。还有一些事情,比如去检查方法体的每条途径是否都有返回值,返回值是否正确,异常是否都正确处理等等这一类的事情,被叫做数据及控制流分析,总之就是检查语法是否满足语言特性。还有一些支持语言特性的事情,比如支持泛型、支持枚举、自动装箱与拆箱、支持可变参数等等,被做解语法糖,以上这些过程都可以被称为语义分析。

做完了这些事情自然就是生成java字节码了。这个过程其实就是将之前各个阶段产生的信息,写入到磁盘中,另外还要进行少量的代码添加和转换工作。

我们可以简单的画个图,java代码的编译过程如下:


java smail翻译 java代码在线翻译_java代码翻译器_02


我们先来搞懂什么是环境变量吧,环境变量一般是指在操作系统中,用来指定操作系统运行环境的一些参数,比如系统文件夹位置等等。配置JAVA程序的环境变量,等于是告诉操作系统,“我”在哪个位置。这个“我”就是JVM的位置。如果不做配置,操作系统是找不到我的,java程序的执行也就无从谈起了。


在思考字节码的加载过程前,我们不妨看看,一个类什么时候会被加载JVM规范要求,一个类或接口在初次使用时,才会被初始化。也就是在第一次才会被初始化。那么我们什么时候才会使用到一个类呢?创建实例、静态方法调用、调用main方法、初始化一个子类必先初始化其父类也算吧、比如使用某个静态常量(非final的那种)、使用反射调用方法……这些行为属于主动去触发的,算主动使用了,除此之外的行为,都属于被动使用了。比如编写main方法,直接访问子类从父类继承而来的静态属性,这种情况下,父类会被初始化,而子类不会,子类只会被加载。


除了加载之外,还会干什么事情呢?开动你OO的大脑想想肯定需要验证吧?要不数据对不对还不知道呢。在使用之前,总还是需要准备一下吧比如为变量分配点内存什么的。Java代码的引用是不是也需要换为内存地址或者偏移量呢?还是需要做些初始化的事情吧?比如静态代码变量,静态代码块什么的。具体步骤嘛,大概率长下面这样:


java smail翻译 java代码在线翻译_java代码翻译器_03


嗯,我们先想想,java程序中都包含哪些东西吧,具体步骤会不会先不说,我们可以利用OO的思想考虑一下吧。首先呢,java源代码需要被执行,那么它就是java字节码吧。Java字节码需要被执行,肯定需要一个加载字节码的系统,因为我们加载的是一个又一个类,那么姑且叫做类加载系统吧,字节码被加载了,肯定需要被执行,那么这个执行代码的东西,我们就叫它执行引擎好了。我们知道,每一个操作系统都提供了系统函数用于调用,但是每一个操作系统都有所不同。所以呢,我们可以考虑对这些系统函数做一个封装,这些被封装的系统函数又好多的,我们就叫做本地方法库吧。考虑到操作系统也可能不断升级,我们考虑利用面向对象的特性去解决它,嗯,先封装为接口吧,这样把接口与执行引擎和本地方法库进行对接,如果操作系统发生变化,接口不用变化了,改本地方法库就好了。这些接口当然是叫本地接口了。

执行层面的事情考虑好了,我们想一想,java程序都包含了哪些东西?java程序会创建对象吧?把这些对象集中起来管理,这个地方就叫堆。我们运行的java程序,都是要调用方法才能执行的,一个对象中,比如方法,常量,有静态变量,这些东西我们就把它放到方法区吧,他们和程序的运行是相关的。可是好多时候,java还需要调用系统函数,还可能执行其他语言编写的代码啊,这个怎么办呢?也好办,再搞一个本地方法区去存放它就好了。

这些东西都想好了,可是程序依然没能运行起来鸭,那么每一个方法又有什么东西?有方法变量,有返回值,有我们编写的基本语句,有其他方法的调用,方法怎么才能被调用到?肯定是需要地址的啊。好嘛,这堆东西我们一股脑儿的放一起吧,交给执行引擎去执行吧。可是这个地方还没有名字鸭,就叫虚拟机栈吧。嗯,好像还没遇到本地方法,遇到了怎么办?再搞个本地方法栈吧。

接下来我们要考虑程序的具体执行了。一个程序虽然被编译了,但是程序与程序之间还有一些调用关系,这些调用关系需要装配成到一起,程序才能正确执行。这个装配的过程叫做链接。考虑到库函数是很多的,如果在程序运行前需要全部装配好的话,会占用大量的资源。那么我们可以考虑在程序加载时,在来做这个装配。这个过程我们叫做动态链接吧。

那么这样一来,我们可以把每个方法的执行,放到虚拟机栈中,为其分配一个栈帧,每个栈帧都装着方法变量,操作栈数,动态链接,和方法出口就好了。

嗯,当然,程序想要连续的运行下去,必须让CPU知道下一条指令的地址在哪儿,那么这个自然是程序计数器的功能啦。Java是一个多线程的语言,为了让程序更快的执行,我们每一个线程分配虚拟机栈,本地方法栈,程序计数器就好啦。到此为止,为了方便理解和记忆,我们得到了下面这个草图:


java smail翻译 java代码在线翻译_JVM_04


缓存是什么?是计算结果。什么场景下需要缓存?多次需要结果的地方需要缓存。字节码被解释执行一次,就可以得到相关机器码的执行结果,为什么不把这个相关的机器码“缓存起来”,将多次执行的字节码直接转换为机器码,下次就不解释了,直接执行机器码就好了。

JDK的 HotSpot虚拟机就搞定了这个问题。提供了两种代码的执行方式:

1.解释执行,即逐条将字节码翻译成机器码并执行。

2.即时编译(Just-In-Time compilation, JIT), 即将一个方法中包含的所有字节码编译成机器码后再执行。

HotSpot是一种热点探测技术,它又是怎么知道哪些代码是热点呢?当然是需要计数器了。没有统计就没有准确嘛。当方法执行超过一定阈值,就认为是热点,就把这段字节码转换为机器码就好了鸭。要统计方法的执行热点怎么办鸭?自然是看方法被了用了多少次了,方法里还有循环体,是不是也应该统计下呢?

HotSpot提供了两种计数器,一种是方法调用计数器,用于统计方法的调用次数,一种是回边计数器,用于统计方法体中循环体代码的执行次数。那你说代码怎么执行呢?画个流程图吧,大概率是这样的:


java smail翻译 java代码在线翻译_java代码翻译器_05


我还是会坚持写技术文章,再难也要坚持,只是速度可能没以前快了,但是很多内容是需要你看完认真思考的,知识的掌握不是学来的,敲视频代码敲会的,学会思考,学会真正的OO,慢慢的你就会快速进入一个高手的境界。