你好!我是郑晔。
通过前两讲的学习,相信你已经对程序设计语言有了全新的认识。我们知道了,在学习不同的程序设计语言时,可以相互借鉴优秀的设计。但是要借鉴,除了模型和接口,还应该有实现。所以,这一讲,我们就来谈谈程序设计语言的实现。
程序设计语言的实现就是支撑程序运行的部分:运行时,也有人称之为运行时系统,或运行时环境,它主要是为了实现程序设计语言的执行模型。
相比于语法和程序库,我们在学习语言的过程中,对运行时的关注较少。因为不理解语言的实现依然不影响我们写程序,那我们为什么还要学习运行时呢?
因为运行时,是我们做软件设计的地基。你可能会问,软件设计的地基不应该是程序设计语言本身吗?并不是,一些比较基础的设计,仅仅了解到语言这个层面是不够的。
我用个例子来进行说明,我曾经参与过一个开源项目:在JVM上运行Ruby。这种行为肯定不是 Java语言支持的,为了让Ruby能够运行在JVM上,我们将Ruby的代码编译成了Java的字节码,而字节码就属于运行时的一部分。
你看,做设计真正的地基,并不是程序设计语言,而是运行时,有了对于运行时的理解,我们甚至可以做出语言本身不支持的设计。而且理解了运行时,我们可以成为一个更好的程序员,真正做到对自己编写的代码了如指掌。
不过,运行时的知识很长一段时间内都不是显学,我初学编程时,这方面的资料并不多。不过,近些年来,这方面明显得到了改善,各种程序设计语言运行时的资料开始多了起来。尤其在Java社区,JVM相关的知识已经成为很多程序员面试的重要组成部分。没错,JVM就是一种运行时。
接下来,我们就以JVM为例,谈谈怎样了解运行时。
程序如何运行
首先,我们要澄清一点,对于大部分普通程序员来说,学习运行时并不是为了成为运行时的开发者,我们只是为了更好地理解自己写的程序是如何运行的。
运行时的相关知识很多,而“程序如何运行”本身就是一条主线,将各种知识贯穿起来。程序能够运行,前提条件是,它是一个可执行的文件,我们就从这里开始。
一般来说,可执行的程序都是有一个可执行文件的结构,对应到JVM上,就是类文件的结构。然后,可执行程序想要执行,需要有一个加载器将它加载到内存里,这就是JVM类加载器的工作。
加载是一个过程,那么加载的结果是什么呢?就是按照程序运行的需求,将加载过来的程序放到对应的位置上,这就需要了解JVM的内存布局,比如,程序动态申请的内存都在堆上,为了支持方法调用要有栈,还要有区域存放我们加载过来的程序,诸如方法区等等。
到这里,程序完成了加载,做好了运行的准备,但这只是静态的内容。接下来,你就需要了解程序在运行过程中的表现。一般来说,执行过程就是设置好程序计数器(Program Counter,PC),然后开始按照指令一条一条地开始执行。所以,重点是要知道这些指令到底做了什么。
在Java中,我们都知道,程序会编译成字节码,对于Java来说,字节码就相当于汇编,其中的指令就是Java程序执行的基础。所以,突破口就在于了解指令是如何执行的。
其实,大部分JVM指令理解起来都很简单,尤其在你了解内存布局之后。比如,加法指令就是在栈上取出两个数,相加之后,再放回栈里面。
我要提一个看上去很简单的指令,它是一根拴着牛的绳子,这就是new,没错,就是创建对象的指令。那头牛就是内存管理机制,也就是很多人熟悉的GC,这是一个很大的主题,如果展开来看的话,也是一个庞杂的知识体系。
有了对指令的理解,就对Java程序执行有了基本的理解。剩下的就可以根据自己的需要,打开一些被语法和程序库隐藏起来的细节。比如,synchronized是怎样实现的,顺着这条线,我们可以走到内存模型(Java Memory Model,JMM)。
当然,这里的内容并不是为了与你详细讨论JVM的实现,无论是哪个知识点真正展开后,实际上都还会有很多的细节。
这里只是以JVM为例进行讲解,学习其他语言的运行时也是类似的,带着“程序如何运行”这个问题去理解就好了。只不过,每种语言的执行模型是不同的,需要了解的内容也是有所差异的。比如,理解C的运行时,你需要知道更多计算机硬件本身的特性,而理解一些动态语言的运行时,则需要我们对语法树的结构有一定认识。
有了对运行时的理解,我们就可以把一些好的语言模型借鉴到自己的语言中,比如,使用C语言编程时,我们可以实现多态,做法就是自己实现一个虚拟表,这就是面向对象语言实现多态的一种方案。
运行时的编程接口
我们前面说过,做软件设计的地基是运行时,那怎样把我们的设计构建在运行时之上呢?这就要依赖于运行时给我们提供的接口了。所以,我们学习运行时,除了要理解运行时本身的机制之外,还要掌握运行时提供的编程接口。
在Java中,最简单的运行时接口就是运行时类型识别的能力,也就是很多人熟悉的getClass。通过这个接口,我们可以获取到类的信息,一些程序库的实现就会利用类自身声明的信息。比如,之前说过,有些程序库会利用Annotation进行声明式编程,这样的程序库往往会在运行的过程中,以getClass为入口进行一系列操作将Annotation取出来,然后做相应的处理。
当然,这样的接口还有很多,一部分是以标准库的方式提供的,比如,动态代理。通过阅读JDK的文档,我们很容易学会怎么去运用这些能力。还有一部分接口是以规范的方式提供的,需要你对JVM有着更好的理解才能运用自如,比如,字节码。
前面我们说了,通过了解指令的执行方式,可以帮助我们更好地理解运行时的机制。有了这些理解,再来看字节码,理解的门槛就大幅度地降低了。
如果站在字节码的角度思考问题,我们甚至可以创造出一些Java语言层面没有提供的能力,比如,有的程序库给Java语言扩展AOP(Aspect-oriented programming,面向切面编程)的能力。这样一来,你写程序的极限就不再是语言本身了,而是变成了字节码的能力极限。
给你举个例子,比如,Java 7发布的时候,字节码定义了InvokeDynamic这个新指令,当时语言层面上并没有提供任何的语法。如果你需要,就可以自己编写字节码使用这个新指令,像JRuby、Jython、Groovy 等一些基于JVM的语言,开发者就可以利用这个指令改善自己的运行时实现。当然InvokeDynamic的诞生,本身就是为了在JVM上更好地支持动态语言。
好消息是,操控字节码这件事的门槛也在逐渐降低。最开始,操作字节码是一件非常神秘的事情,在许多程序员看来,那是只有 SUN 工程师才能做的事情(那时候,Java还属于 SUN)。
后来,出现了一个叫ASM的程序库,把字节码拉入了凡间,越来越多的程序员开始拥有操作字节码的能力。不过,使用ASM,依然要对类文件的结构有所理解,用起来还是比较麻烦。后来又出现了各种基于ASM的改进,现在我个人用得比较多的是ByteBuddy。
有了对于字节码的了解,在Java这种静态的语言上,就可以做出动态语言的一些效果。比如,Java语言的一些Mock框架,为什么可以只声明接口就能够执行,因为背后常常是动态生成了一个类。
一些动态语言为了支持自己的动态特性,也提供了一些运行时的接口给开发者。比如,Ruby里面很著名的method_missing,很多框架用它实现了一些效果,即便你未定义方法也能够执行的。你也许想到了,我们提到过的Ruby on Rails中各种find_by方法就可以用它来实现。
method_missing其实就是一个回调方法,当运行时在进行方法查找时,如果找不到对应方法时就调用语言层面的这个方法。所以,你看出来了,这就是运行时和语言互相配合的产物。如果你能够对于方法查找的机制有更具体的了解,使用起来就可以更加地得心应手,就能实现出一些非常好的设计。
总结时刻
今天,我们讨论了程序设计语言的实现:运行时。对于运行时的理解,我们甚至可以做出语言本身不支持的设计。所以,做设计真正的地基,并不是程序设计语言,而是运行时。
理解运行时,可以将“程序如何运行”作为主线,将相关的知识贯穿起来。我们从了解可执行文件的结构开始,然后了解程序加载机制,知道加载器的运作和内存布局。然后,程序开始运行,我们要知道程序的运行机制,了解字节码,形成一个整体认识。最后,还可以根据需要展开各种细节,做深入的了解。
有一些语言的运行时还提供了一些语言层面的编程接口,程序员们可以与运行时进行交互,甚至拥有超过语言本身的能力。这些接口有的是以程序库的方式提供,有的则是以规范的方式提供。如果你是一个程序库的开发者,这些接口可以帮助你写出更优雅的程序。
关于程序设计语言的介绍,我用了三讲分别从模型、接口和实现等不同的角度给你做了介绍。目的无非就是一个,想做好设计,不仅仅要有设计的理念,更要有实际落地的方式。下一讲,我们来讲一个你可以在项目中自己设计的语言:DSL。
如果今天的内容你只能记住一件事,那请记住:把运行时当作自己设计的地基,不受限于语言。

思考题
最后,我想请你分享一下,你知道哪些程序库的哪些特性是利用运行时交互的接口实现的?欢迎在留言区分享你的想法。
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
精选留言
2020-06-15 10:18:53
2.spring基本是java后端开发的实时标准,如果以spring为运行时环境(ioc容器+依赖注入以及其他功能),那么其以约定和编码库提供的运行时编程的接口就实在挺多了。
3.按本篇自己的理解,运行时不限于语言级别。固有以上推断。
2020-06-15 00:31:50
2020-07-11 11:54:37
设计一门语言,以运行时为地基。
按照时间顺序的发展是:高手把运行时抽象为简化的模型,供小白入门。小白想进阶的话,还是要深入到运行时的实现细节中去。
2020-06-15 07:48:04
理解运行时可以深入理解程序语言是如何加载,执行的,这能让我们对语言添加一些原本不支持的功能
对于JAVA语言就是要了解jvm执行的机制,
它包括加载字节码,解析,内存管理,线程调度等等方面
这些跟操作系统的很多知识其实是相通的,因此需要把各种知识在头脑建立连接,达到知识的迁移的效果,提高学习的效率
2020-11-20 07:48:10
2020-07-08 14:09:00
2023-10-09 06:44:57
【R】运行时环境 = 程序设计语言的执行模型 = 软件设计的地基。
【.I.】起手写个热身代码,就来个“Hello XYZ”,一写、一编、一跑,再瞄一眼,输出符合预期,可以。这么流畅的过程,会忘记去关注,写的时候敲入的文本,编的时候转换成的二进制,跑的时候所在的系统环境,这之间哪些是显性的编程语言特征、哪些又是隐性的部分。而编程语言的运行时,就是容易被忽视的关键隐者。以至于刚开始做嵌入式,愣是不明白交叉编译、目标运行的原理到底是什么,只是觉得特别神奇,神奇到难以解释的略显神秘感,实际上却是编程语言运行时能够解释这一切神奇性。
当解开这种运行时环境的神奇性之后,就不会再尴尬去面对,在编写“Hello XYZ”时,如果“X=C”、“Y=JAVA”、“Z=Python”时,都是在控制台输出这么问好话语,居然会拖泥带水引入如此之多的七大姑八大姨还有十六兄三十二妹。
运行时的典型特点,就是它在帮助我的时候,我浑然不知,就像公司里用电用网自然而然,但是当断网断水的时候,我除了叽里咕噜抱怨几句,却拿不出啥招式来解决遇到的问题。运行时已经让我享受到很多恩惠,却没有来想我索要感谢的透明又极其重要的存在。如果说编程语言的语法像公司的各种规章制度,那么编程语言的运行时,就是公司里让这些制度实施的人和物。
【Q】如何设计一个C语言运行时环境的课程,同时达到讲得明白、上手操练、深入理解、拓展开去的效果?
---- by 术子米德@2023年10月9日
2022-03-10 18:52:30
2021-04-23 16:49:37
2020-10-20 20:49:49
2020-10-19 23:22:02
2023-09-05 08:28:08
2022-11-16 16:46:06
2022-05-20 11:58:07
我的思考:
java运行时想象成一个模型,这个模型的接口通过字节码的方式提供出来,语言本身是通过使用的这些接口来实现功能。我们怎么使用运行时接口呢?有些运行时接口是就直接通过语言本身(语法,程序库)传递出来,比如`getClass`,或者我们直接使用字节码。 从某种层面上说,开发者直接使用字节码的话,是不是就增加了耦合。
2022-05-11 12:31:41
2021-09-28 16:58:11
模型-接口-实现,其实就是顶层设计到具体落地措施,从抽象到具体,不断从上往下细化和拆解的过程。
这个过程怎么去具体实施,就考验功力和经验。
实际上我们在实践中就是在积累和沉淀这个拆解落地的经验和能力,最终实现高层抽象和具体实现之间的衔接。
缺乏这个过程的实操,就容易变成纸上谈兵或盲人摸象。
2020-06-20 19:21:04