机器语言到Java语言的技术背景

1.概述

客官,这里有故事,而且故事可能会很长,请带上花生米和酒。本篇博客内容可能会涵盖计算机的历史,编程语言的发展,Java的发展轨迹这三方面的内容,而文章的落笔方式以我个人的思考方式为主,旨在为小白和有一定编程基础的人建立一个立体模型和进阶路线,当然大神请勿喷。而写该博客的动因是技术进阶前对个人知识面的一个回顾,在这里希望能给自己和热爱技术的人一个新的起点。个人学习Java有三年,写过hello world,捣鼓过C语言,翻过技术贴,看过技术书,写过Java项目,一路上也碰到过很多未解的问题,有些想明白了,有些消失在繁忙之中,迷茫过也思考过,总的来说前进方向是正确的。

2.计算机体系

作为一个科班出身的程序员,入门与其他小白一样,对hello world的程序打印充满好奇和不解,这就是编程?why so easy?why我们的程序和日常看到的软件不一样?why我们平时看到的QQ页面是酷炫的,功能是强大的,我的hello world程序只有一个黑色的输出窗口?要讲清楚上面所述的内容,得从最古老得电路板说起:
1)最原始的CPU(电路模型)
CPU也称处理器,在通电状态下可以处理二进制的机器指令,而处理过程也很有机器特性:cpu下有16根引脚线,引脚可以传输0和1的电信号,输入设备可以通过其向cpu输入16位的二进制机器指令,处理完成之后,cpu会再一次通过输出引脚输出结果到其他设备。最开始具备计算机特征的工具是电路板,电路板具备重复计算的功能,例如计算器就是电路板运用的典型产品,通过输入数字和计算规则,然后输出结果。但有一点是重要的,通电的电路板只能记录0和1信号,所以电路板只能输入二进制数字(包括将要计算的数字和计算规则)和输出二进制数字结果。
2)冯诺伊曼模型
因为电路板具备重复计算的快捷功能,很快得到了市场的大面积应用,早期特别是应用在工业领域和数学等领域,并且在应用实践中,人们得出了一个冯诺伊曼模型:cpu(电路板),输入设备(键盘,鼠标,硬盘),输出设备(显示器,硬盘),存储器(内存条),why该模型是实用的,因为在重复计算过程中,人们发现使用计算机处理问题的方式都是三个要点,输入数字和处理规则,电路板执行规则语义,输出结果,所以输入设备,cpu,输出设备在该模型中是毫无疑问,但是存储器为何在其中,这是因为在计算过程中cpu无法保存计算的中间状态,例如3X3+4算术中,3X3=9这个状态需要保存下来以便cpu完成下一次9+4的计算。
3)古老的计算机
最初符合冯诺伊曼模型的计算机是挺简单的,一张有二进制数字程序的纸条,经过专门的硬件翻译录入到电路板上(电路板上自带存储器-寄存器),经过电路板计算,然后把二进制结果刻在纸条上完成输出。这就是早期模型的计算机,程序是由二进制数字(包括要处理的数据和指令)组成,没有高级语言,也没有操作系统,程序员编写的程序需要显式指明计算过程中产生的中间态数字需要保存在那个寄存器上面。例如将要参与计算的数字加载到那两个寄存器上,那两个寄存器之间的数字执行怎样的计算规则。所以从这里看出,程序员用二进制数字指令(机器语言)编程需要自己清楚硬件资源的使用情况。
4) 计算的演变
计算模型在发展中,在应用层面上不断扩展,能处理的问题不再单指重复计算问题,已经演变成处理重复问题,计算规则从加减乘除演变成具有特定的指令规则(机器指令),整个流程演变成编写二进制程序,运行指令,cpu读指并执行指令,输出结果,例如a+b=c,程序员先编写一段机器程序,cpu读取程序指令,然后执行指令,把数字a加到寄存器1上,把数字b加到寄存器2上,然后cpu把寄存器1和2的数字相加得到c并把c存到寄存器3中,最后把寄存器3中的数字打印到输出设备。
5)语言的演变
谈起编程语言,有很多,但是cpu只会处理二进制机器指令,其他编程语言的指令,cpu都不能处理,所以所谓的其他编程语言都只不过是机器语言的一层装饰,而why需要这层装饰,下面会提及。在这之前先说一个结论:编程语言演变的内因是人在编程过程中出现问题而一直在推进(机器语言-二进制数字,汇编语言,C语言,Java和C++,区块链正在酝酿新兴语言)。
(1)从机器语言到汇编语言,汇编语言的指令是一个简单明了的单词(例如add,remove),人的眼睛看到的不再是毫无人性的二进制数字指令,指令有了些许温度,指令本身可以见名知义,程序员学习和使用汇编语言变得简单和容易,并且用其编写的程序变得容易维护,因为指令本身是容易读的。那机器指令和汇编指令具备什么关系?机器指令和汇编指令具备一一对应的关系,所以使用汇编语言进行编程保持了机器语言的高效性。那cpu只能识别机器指令,怎么执行汇编指令?其实运行汇编写的程序之前,其需要经过专门的翻译工具,把汇编指令一一转换成机器指令,这个过程也叫编译,当编译完成,程序指令本身就可以被cpu识别和执行了。
(2)从汇编语言到C语言,已经有汇编语言了,为什么还需要C语言?因为汇编语言没有从根本上去掉机器语言的特性,只是机器语言的一层简单的粉饰,那机器语言的特性指的是什么?一个是用机器语言编写的程序难以维护(下面操作系统的论述有介绍),一个是指令本身需要显式指定硬件资源去进行操作,编程人员手动分配资源是很容易导致资源分配冲突的现象(下面操作系统的论述有介绍)。所以汇编语言也被称为低级语言,优缺点也显得很明显,优点是保持了机器语言的高效性以及是可读的,缺点是汇编指令仍然像机器指令一样需要显式分配硬件资源。在这样的背景下,C语言因为完全去掉机器语言的特性而诞生,C语言本质上也是机器语言的一层粉饰,只是C语言的指令是由几条机器指令的组合而成,其C语言的指令设计原则是为了实现简单功能,并屏蔽硬件资源的显式分配,所以C语言也被称为高级语言。当然C语言编写的程序也需要经过编译成机器程序才能运行。
(3)从面向过程走向面向对象编程,从机器语言,到汇编语言,再到C语言,都是面向过程的语言,时至今日仍然在某些领域大放光彩,例如汇编语言编写的硬件驱动程序是高效的,C语言利用库函数编写的程序保持较高效的同时屏蔽了计算机硬件资源的操作,不过C语言也可以理解为高级语言中的低级语言,在某些领域中在硬件编程方面也有应用。但是面向过程的语言面对像QQ,淘宝这类应用,C语言尽管是可读的也屏蔽了硬件的直接操作,但是因为程序的体积过于庞大,函数式编程不适用于模块分类以及应用因为需求变化的快速迭代,所以面向对象的Java领域和C++这类语言诞生并得到快速发展。
(4)随着互联网的普及,用户体量越来越大,产生的数据越来越多以及新的区块链领域的发展,单点计算机已经不能实时的处理海量数据,市场上催生出许多分布式方案,利用多台计算机去共同处理这些海量数据,不过这种计算机协同的方式去处理海量数据的行为会产生很多分布式的状态,靠编程的方式去维护分布式状态是可行的,但是弊端很明显,靠程序员去维护这些中间状态是十分繁琐的以及容易出错的,目前区块链领域诞生了一些新的语言概念,依靠编程语言本身去维护这些中间状态,程序员只需要配置那几台计算机协同工作就好。
6)操作系统
(1)计算的演变和语言的演变都是渐进的,需要进行演变的内因是机器解决的重复问题越来越复杂,二进制指令组成的程序也变得越来越复杂,依靠专业的编程人员用机器语言(二进制指令)编写程序已经变得不可靠,why不可靠,原因就好像是在问:计算机上面why需要一层操作系统,用户(指使用计算机的人)直接用机器语言操作硬件机器不就好了吗?
(2)我假设一种场景,没有操作系统,用户(早期指程序员)直接在硬件上提供的机器指令进行编写程序,然后编写出来的程序是由一堆二进制的指令组成,最后交给计算机硬件去执行。当然这段程序编写下来如果没有bug还好,但是万一存在因为bug返工修改的情况,这个问题就可大可小了,毕竟修改程序之前,程序员需要先弄懂这段程序如何通过机器指令实现功能的,如果这段程序只是处理简单A+B的功能,那么组成这段程序的机器指令很短,修改也问题不大,可是如果这程序像QQ应用一样大,那么你想象一下组成这段程序的机器指令有多恐怖,人肉眼看到的程序是由密密麻麻的二进制数字指令组成,单纯依靠人的脑容量去分析这段程序(二进制组成的程序)的逻辑完全是不现实的。所以,完全用机器语言写出来的大型程序是不可维护的。
(3)同时,用机器语言编写大型程序也是很容易出bug的。想象一个场景,程序是调用计算机的硬件资源完成计算,如果程序员用机器语言编写大型程序,那么程序的机器指令在硬件上执行的时候是很容易发生资源冲突的(硬件冲突是指,程序在执行的时候,某个硬件资源被前面的某条指令占用了,但是又被当前指令分配占用的现象),因为计算机的硬件资源,完全依靠程序员自主分配给机器指令,举个例子:某段内存在执行过程中,前面的某条指令用这段内存保存了数据,但是因为程序员忘了这个事情,又把这段内存分配给新的指令去保存数据,那么老的数据会被新的数据给覆盖掉,老的数据就会丢失。上述例子就是资源冲突的典型,这些资源冲突的现象在早期原始编程里面是很容易发生的现象,原因是程序员自己手动分配给硬件资源给某条指令使用。
(4)操作系统的概念基于以上两点引出,也是为了解决以上两个问题。那么是解决以上两个问题尼?第一,引出新的可读的高级编程语言(例如C语言),程序员通过高级语言编写程序,而编写的程序经过编译,会从C语言转变成机器语言,再交给计算机去执行,当出现问题时,程序员维护用可读高级语言编写的程序即可,经过重新编译再交给计算机去执行。第二,用户程序不能再直接去分配和调用硬件资源,若想要资源需要向操作系统申请,操作系统会返回一个可用的计算机资源的编号,若想要调用计算机硬件资源,只能通过操作系统提供的系统调用函数和资源编号完成调用,原则上操作系统本身去维护硬件资源的可用状态,用户程序只能通过系统调用函数间接调用。
(5)那么操作系统是什么以及如何实现的呢?操作系统是也是一段程序,程序是由C程序编写,经过编译成二进制机器指令运行在硬件上,它随着电脑开机而启动运行,它向下管理着计算机上的硬件资源,向上暴露出一组系统调用函数给用户去调用。所以操作系统具备计算机硬件的最高权限,其对本身维护着计算机硬件的所有资源,用户程序若想使用计算机的硬件资源,必须通过操作系统提供的系统接口进行调用。这里得先介绍用户程序(例如QQ)和内核程序(调动硬件的驱动),不管用户程序还是内核程序经过编译之后都是一组二进制指令集,其运行都需要操作系统提供上下文支持,不过在执行用户程序和内核程序指令的时候,操作系统的上下文是不一样的,因为用户程序是运行在操作系统的用户态上下文中,内核程序是运行在操作系统的内核态上下文中,只有内核态的上下文才有硬件最具体的信息,并且cpu运行系统调用(内核程序)必须处在操作系统的内核态上下文中,,所以用户程序调用系统函数会使操作系统陷入内核态,导致传说中的上下文切换。
7) 进程与线程
进程概念的提出,是针对日益强大的cpu处理能力而言,随着上世纪cpu的快速发展,cpu的处理能力快速增强,cpu只处理单个应用程序,严重浪费计算机资源,因为对于输入-cpu处理-输出这个模型来说,输入数据的速度远慢于cpu的处理速度,导致了cpu需要空闲等待程序录入数据完成,cpu才能进行往下处理输入数据的情况发生。所以进程概念被提出,一个进程意味着单个应用程序(例如QQ就是一个应用程序),人们为了充分利用cpu的计算能力,一般计算机都是同时在操作系统上运行多个进程,而我们现代计算机基本都是同时运行N个进程。那有了进程,why还需要有线程这个概念,内因是cpu同时处理着多个进程任务,虽然说同时,但是实质是cpu来回切换执行进程,所以cpu切换进程执行之前得把正在执行的进程中间态(进程上下文)保存下来,以便该进程下次可以恢复执行到原来状态。所以进程可拆成进程上下文+线程,线程一般指执行的单元,可以理解为可执行的指令。
8)现代计算机
再回头看看现代的计算机,因为网络连接的能力,计算机能与计算机进行通信,计算机的应用已经可以联合完成功能(例如QQ)来满足用户的需求,尽管当代计算机功能强大,但是还是离不开冯诺依曼模型(输入设备,输出设备,存储器,CPU),今天我们看到的输入设备最多是键盘,输出设备最多是显示器,cpu是intel系列,存储器是内存条,而运行在硬件之上的操作系统是Win10。除此之外,多了两个重量极硬件,显卡和网卡,显卡根据色粒渲染出绚丽的显示页面,网卡可以使计算机在网络中传输数据,自此,现代计算机介绍完毕。
9)回归hello world程序
hello world程序和QQ大型应用相同之处都涉及到C语言实现,区别在于hello world程序是一个入门级程序,经过编译成机器语言,机器语言的语义就是往控制台输出hello world字符串,而QQ是一个支撑起几亿人使用的大型程序,程序功能包括聊天窗口的构建,聊天内容传输等功能,QQ经过编译出来的机器语言十分庞大,更充分利用了现代计算机硬件的资源去实现功能。

3.Java发展轨迹

Java是在1995年出生的,创世主是高司令,原本初衷是想在家电的编程领域大展拳脚,提出一处编译到处运行的口号,这句话的意思是:编写一份程序代码,程序经过编译后可以运行在各个智能家电设备上。这个口号听起来很nice,但是实现起来会有很多问题,原因是每个智能家电的硬件设备环境有所区别,每个设备提供的编程指令集(机器指令)大致相同但又有微小区别,所以这个难题在于:高级语言指令经过编译之后,编译出来的机器指令如何保证兼容所有的硬件设备并且能在硬件设备上正常运行。上述问题不止Java面临,几乎所有的高级编程语言都要面对,而C语言采取的是编译器兼容方案:同一份代码,采用不同的编译器来编译,编译出不同的机器指令来兼容不同的硬件设备指令集。与C语言不同,Java不走寻常路线:同一份代码,相同的编译器编译出相同的指令,不过这个指令不是机器指令,而是Java规范自定义的字节码指令,这个字节码指令cpu是不能直接运行的,得借助一个叫JVM虚拟机的程序翻译成机器指令去执行,而不同的硬件设备需要运行不同的虚拟机,所以Java语言走得是虚拟机兼容方案。
1)硬件编程领域的语言角斗场
尽管两种方案都是可行的,然而在实践中,汇编语言和C语言在硬件领域编程大放光彩,Java在这场角斗中黯然离场。Java在这场竞争中败下阵来的原因,我个人分析主要有两:第一,对于硬件层面来说,硬件编程其实指的就是编写驱动程序,由于一般硬件提供的功能逻辑都不复杂,所以驱动程序一般不会太复杂,程序体量都会很轻,使用汇编语言和C语言已经足以编写可维护的驱动程序,如果使用Java编程还得给每个硬件设备编写一套JVM虚拟机的环境,Java编译后的字节码指令才能运行,这反而给编写驱动程序带来更大的工作量;第二,使用汇编语言和C语言编写驱动程序性能比较高,因为使用Java语言编写的程序依赖于JVM虚拟机环境翻译执行,而JVM是基于C语言实现的,所以Java执行效率是比C语言低的。
2)应用编程领域的语言角斗场
尽管Java在家电设备的硬件领域没有取得一定成就,但是其提出面向对象编程概念,引领了PC端应用编程的潮流,一举成为世界上使用量最多的编程语言。Java在应用编程领域的成功我个人归功于两点:第一,Java有前沿的概念创新,其包名概念和类概念可以为大型应用实现清晰的模块拆分思路,并且涵盖的面向对象编程方式使程序易于理解和维护,特别是大型程序需要这样的概念支撑,才能实现现代化应用程序的快速迭代。第二,Java易于维护的另一个支撑是技术创新,Java编程领域抛弃了管理逻辑内存(下方会有描述)的概念,程序指令只有对象引用,保存数据时依靠JVM虚拟机做到了内存的自动分配和回收,Java正是因为这个不再像C语言一样因为逻辑地址导致生涩感,所以使用Java编写的程序可以做到简单明了。当然,Java为了实现这些创新的概念,创造出的JVM虚拟机概念导致程序的运行上下文变得臃肿,运行性能会比不上C语言,但是应用市场的发展选择了Java和C++语言,说明了应用编程领域的可维护性是高于性能追求。
3)逻辑内存和物理内存地址
回到上述的逻辑内存,那这个逻辑内存指得是什么呢?谈起这个得从物理内存说起,物理内存就是冯诺依曼模型的存储器,早期的机器语言编程都需要机器指令显式指明数据保存到那一段物理内存地址当中,但是随着指令显式分配内存带来资源冲突的弊端,操作系统的概念随之被提出,从此,高级编程语言指令不再有权限直接操作物理内存,高级语言的指令若想要内存保存数据,只能通过系统调用向操作系统申请,操作系统会返回一个逻辑内存地址给高级语言的指令,指令再使用这个逻辑内存地址保存数据,这个逻辑内存地址经过操作系统映射,可能指向一段连续的物理内存,也可能指向一段由操作系统组织起来的分散物理内存。所以可以得出一个结论:物理内存只有操作系统的程序指令具备操作权限,其他高级语言指令若想要使用内存保存数据,只能向操作系统申请逻辑内存,并只能操作逻辑内存地址保存数据,逻辑内存地址经过操作系统的地址映射,数据最终会保存到物理内存地址当中。
4)Java的内存处理方式
前文提到Java程序可以做到内存自动分配和回收,已经抛弃了管理逻辑内存的概念,那它的运行时产生的中间数据状态又是怎么保存到物理内存当中的呢?在描述Java程序内存处理方案之前,先看C程序的内存方案:声明一个指针变量,然后C程序指令调用系统函数并传递变量参数到函数中,向操作系统申请内存,当系统调用完成指针变量就指向逻辑内存地址,程序就可以使用逻辑内存地址来保存数据了。从上述用例中可以看到,C语言是通过操作逻辑内存地址保存数据的,而其他编程语言能绕过逻辑内存地址保存数据吗?事实上是不能的,因为不管什么语言的程序,只要要保存运行过程中产生的数据,都只能向操作系统申请逻辑内存地址,然后才能保存数据。
(1)那Java提到的内存自动分配和回收又是怎么回事呢?这与Java里面的字节码规范和JVM虚拟机有关,在Java的编程指令中,不会看到指针,也不会看到申请内存的系统调用,这是因为这部分代码的逻辑封装在JVM中,JVM内部会维护着向操作系统申请到的一大段逻辑内存地址,这个内存申请操作其实是像普通C程序一样进行系统调用得到的,而Java中提到的内存自动分配和回收就是对这一大段逻辑内存地址的二次分配和回收。这里值得注意一点是:现代计算机若安装了JVM,则JVM会随着操作系统开机而启动运行,启动运行时会执行JVM的环境初始化,初始化的时候就完成了逻辑内存地址的申请和分配,关机时会随着JVM进程结束而回收。
(2)那JVM又是怎么在Java程序运行时给产生的数据分配合适的逻辑内存地址呢?描述清楚这个概念,先介绍三种指令,编程上的源码指令,编译后的字节码指令,机器运行时的机器指令。
编程上的源码指令: 这个阶段上的指令是易读的和可维护的,肉眼根据单词语义能分析出指令的执行逻辑,这也是高级语言的魅力所在,正因为如此,高级语言能让大型的软件程序变得可维护;
编译后的字节码指令: 这个阶段的指令是一个软件层面上面的中间态指令,运行在虚拟机上的指令,这类指令的规范是由虚拟机定义,运行时虚拟机会根据规范(预先定义好的指令映射关系)将字节码指令翻译成对应的机器指令,当然不同的硬件平台,相同的字节码指令被翻译成不同的机器指令;
机器上的机器指令: 这个阶段的指令是机器可以执行的;
接下来回到正题,前文提到的JVM自动为Java程序分配和回收内存空间,严格意义上是帮Java编译过后的字节码指令分配和回收内存空间,因为源码指令层面完全没有内存分配和回收的概念,只有数据类型(int,byte等基本类型和对象类型)的概念,源码指令只有经过编译器编译成字节码指令之后,才有内存的概念(例如字节码文件规定了每一个字段变量需要占用的内存长度),回顾这个编译过程:编译器会根据源码指令计算出每一个数据类型需要占用的内存空间,并生成一份JVM可执行的特定字节码指令文件。其中提到的字节码文件由字节码指令组成,并且其中的指令规定了内存的申请和分配逻辑,是JVM为Java程序分配和回收内存的依据。最后分析一下Java程序的运行过程:
启动JVM: 由于Java指令是运行在JVM基础上,由C语言实现的JVM解释执行,所以首先先启动JVM;
Java源码编译: Java源码是给程序员看的,源码文件只有编译成字节码文件才能运行在JVM之上;
java命令: java命令是JVM上的一个脚本命令,运行命令,就是让JVM执行Java程序;
Java程序运行过程:
执行java命令就是让JVM扫描当前目录下的字节码文件,并加载到程序的内存当中,当加载完成,JVM程序会分析和解构字节码文件,把字节码的逻辑含义变成C语言定义好的,方便管理的数据结构。当转换完成,JVM开始在转换的数据结构中扫描出main方法,并读取main方法中的字节码指令,然后把字节码指令转换成机器指令,最后JVM调用系统接口使机器指令在CPU上直接执行;
运行时的内存分配:
JVM启动时,通过系统调用向操作系统申请一大段虚拟内存,其中内存主要分为三部分,一个是栈空间(Java方法运行时需要的内存空间),一个是永久区(保存着数据结构:加载字节码文件时生成的数据结构),一个是堆空间(保存new 出来的对象)。当运行java脚本命令时,JVM会解析字节码文件并在永久区生成对应的数据结构;当JVM运行main方法的指令时,JVM会为执行方法分配一块栈空间,保存着方法的上下文(传递进来的参数,新键的临时变量,返回结果,以及方法间的调用关系);当执行方法遇到通过new指令创建对象时,JVM虚拟机还会在堆空间分配一块内存来保存新创建的对象。
5)Java杂谈
文章的篇幅写到这里已经很长,写到这里作者已经把自己构建的计算机体系背景知识交代完全,希望读者能从其中吸收养分,从另一个视角去审视编程语言,去审视Java编程语言。作为一个Java程序员,我们站在编程界的顶峰,使用其简单易懂的语法构建出支撑起亿万用户的程序,但是我们却看不见JVM如何去运行我们的Java程序,却看不见JVM进程如何运行在操作系统之上,却看不见操作系统如何控制计算机硬件资源完成指令调用,这3个问题的答案也是我希望给你们的。

你可能感兴趣的:(经验,经验分享,其他)