JVM之Java编译到执行(1)--引
Java语言特点
一次编写,到处运行。也就是跨平台。 因为这个跨平台的实现原理,而导致Java的编译流程,与以往的C++之类语言有不同。
各个操作系统的底层实现,资源的协调,和硬件操作各有各的不同。就意味着,如果按照早期使用C++这类语言写,每个系统都有不一样的API,不仅存在大量的API调用区分,而且那就要为每一个操作系统都要编译一次。说来简单,楼主做过这种工作,出bug的时候,会编译很多次,光编译这里,就要消耗不少精力。真的比较烦。
想要做到一次编写到处运行的话,Java采取的方案是,将程序员编写的代码首先优化为一个字节码文件,然后为每一个操作系统提供虚拟机,也就是JVM。 这个虚拟机里可以解释这个字节码文件,并将其生成相应系统可以识别的机器码。所以最简化的路子是下图所示。
这样的话有什么好处呢?
- 只要字节码文件一样,JVM虚拟机又是专业的人提供的,可以保证上层调用的API是一致的。也可以保证得到良好的解析,最终生成各自系统可识别的机器码,并执行。
- JVM虚拟机是被人提供的,就意味着,他们对这块的操作权是很大的,可以用顶级的智慧,顶级的开发人员,对各个系统的虚拟机之后进行不断的优化。不用程序员管。
- 对于语言的支持方面,需要提供良好的API,这就意味着,API的开发操作权也在专业的公司手里。他们足可以按照比较先进的思想,提供比C++好用多得多的API。减少下游程序员的学习难度。
编译过程
Java语言的编译,因为上述的设计方案。会导致编译过程分为两个过程
- 1 从源代码编译到字节码的环节 (称为前端编译)
- 2 从字节码到可执行程序的环节 (称为后端编译,运行时做的工作)
其中前端编译,就是那种在计算机中输入一个javac指令,然后就编译成了class文件。在浅层的理解上,认为这就是编译完成了。事实上class文件它不能直接执行的,离执行还差的远。
这些class文件,在真正执行的时候,会让JVM去处理,这个时候属于运行时了已经。在运行时,JVM会拿到已经生成的class文件集,将这些文件里的一条条指令解释为相应平台可以识别的机器码。这样两种配合的情况下,我们写的代码才得以运行。
“解释”
解释型语言简述
解释型语言指的是程序不需要程序员进行编译,程序在运行时才会被翻译成机器码的那类语言。这就意味着,每次执行都得解释一次。因此这类语言有个普遍的缺陷,是效率较低。但是其优势就是跨平台性比较好,我们这里的跨平台烦恼,主要就是编译不同平台所带来的工作工时还有失误的消耗,这个是比较折磨开发者的。解释型是可以解决这种问题的。代表语言为:Python/JavaScript / Perl /Shell 等。 Java也算是。但又没那么彻底。
编译型语言简述
编译型语言的特点是,写的程序执行之前,需要一个专门的编译过程,把程序编译成为机器语言的文件,比如exe文件,以后要运行的话就不用重新翻译了,直接使用编译的结果就行了(exe文件),因为翻译只做了一次,运行时不需要翻译,所以编译型语言的程序执行效率高。缺点就是,开发和维护成本高。其代表语言为 C C++。
解释器VS编译器
上图中有个环节,有一个叫解释器的部分。这块最开始是令我比较迷惑的。原因是我认为Java作为一个解释型语言,那直接解释就好喽。解释不就是编译么?为什么我看到书上,解释和编译要区分呢?为什么这个流程甚至是一边解释一边翻译呢?
原来这里面存在一个区别,解释器和编译器,是实现同一种目标的两种不同的工作方式。
解释器
解释器是直接执行用编程语言编写的指令的程序。它逐行读取源代码,将其转换为机器语言(或某种中间形式),然后立即执行。这意味着解释器在执行程序时,需要逐行解释代码,因此执行速度通常比编译器慢。
编译器
编译器则是将源代码整体转换(或“编译”)为机器语言或低级语言(如汇编语言)的程序。它首先会对源代码进行词法分析、语法分析、语义分析等,然后生成中间代码,并最终转换为机器代码。这个过程通常称为“编译”,生成的机器代码可以独立运行,无需解释器的支持。
前端编译
javac就是Java的前端编译器,是个编译工具,负责把程序员写的java文件,经过初步的分析,转换为class文件。 class文件就是字节码文件。二者可以一块理解。
Java的前端编译基本就是做了这个工作。它的编译流程不做赘述,相当于C++编译或者编译原理的打折版。什么词法,语法,语义分析。这个知识点探下去没完没了,是一个比较深的话题。略过。流程上图已经给出了可以翻上去看一些图片。
Java从编写代码到执行涉及到的编译器概览
我们重点注意的是下半部分,主要由JVM承担的,发生在运行时的后端编译工作!
Java严格的说是一个编译 + 解释混合的语言。这使得我们讨论Java编译到执行这个话题时,不止要讲静态的编译,还要深入了解Java是怎么执行代码的,比如,解释器一条条指令解释,那指令是哪儿来的?存哪里了?类加载环节是不是和这里有关呢?类加载究竟做了什么事?Class除了反射时用,JVM到底用它做什么了?为什么需要它?class文件里面好像有操作指令,这些指令是不是相当于C++可执行程序里的代码段呢?new一个对象JVM划分内存,那个垃圾回收机制是怎么管理这些对象的? 逐渐铺开发现相当于讲1/3JVM机制,一片文章根本讲不完。我会拆分下来一块一块讲。
JVM后端编译
后端编译流程图
对于Java解释字节码很粗略的流程,在上图已经画出来了。总共拆分为两个阶段。
- 类加载 and 二进制转换
- 解释器解释 和即时编译器优化同时进行。(为什么这样之后解释)
需求分析
在了解一个方案的解决办法前,有时候思考问题,要把事件重置到还未发生时考虑。有没有想过为什么JVM的后端编译器为什么要把流程搞成这样子?还什么类加载,还要解释器,那个JIT是啥?为什么要有这个?
我们不妨以一个实现者的视角来聊聊。假设你就是这个编译器的实现者
- 你们的这个大工程中,你负责开发后端编译器这块的组件。你组件的输入内容已经确定了,是前端编译机器的产出--字节码文件,即字节码文件。
- 你这个组件的产出结果是,生成机器码,能上机跑的那种。(我们排除掉一切更为复杂的需求,如内存回收等等机制)
那你会怎么解决呢?
我想是个人都得先分解输入内容和输出内容这两大关键点的样子吧。 于是乎
- 1 你必须要从java语言设计的思想,和所有语言通用思想方面来考虑java语言的组成结构。并能做到很好的拆分转换优化。另外,有些代码调用到中级逃不过C语言或C++,他们的接口入口地址,您要怎么找,怎么正确的连接。等等等等一堆的问题。
- 2 你必须要十分熟悉操作系统原理,尤其是一个程序要跑起来,计算机内部的状态究竟是什么样子。你需要将你输入的那些二进制代码,落实到运行时内存的数据里!
- 3 锦上添花的其余功能,例如垃圾回收机制。
CPU如何执行程序
粗略的讲。CPU本质上是一个没有主见的, 沿着代码块内存一条一条机械执行的工具, 我们对代码块做了很多的编程, 保证操作指令和被操作数是正确的, 这样cpu识别这些指令,也能一条接一条执行.基本没有错误.
您编译代码的目的就是,把代码变成最终CPU识别的样子。
JVM的思路
JVM的思路就是,模拟这种场景。只不过“代码块内存”里的指令是class文件中描述的字节码指令。JVM模拟CPU的操作流程,也是一条一条往下“读”,像极了CPU执行指令那般。在“读”的过程中,JVM会通过编译器或者解释器,将这些字节码指令转换为真正的机器码。这样就达成了与CPU协议的一致。
JVM的整体流程开始于类加载,我们知道了JVM宏观的思路后,再研究每个环节,下篇文章将先从类加载讲起, 类加载的输入是class文件(字节码文件),楼主将贯彻费曼学习法,知无不言,将他们写出来。