<audio id="audio" title="答疑丨Java Web程序的运行时环境到底是怎样的？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2b/12/2bd27ec7ec5b4a0f88201199c0f4ee12.mp3"></audio>

今天是第一模块的最后一讲。在这一讲中，我们主要讲了软件的基础原理，今天，我将会针对这一模块中大家提出的普遍问题进行总结和答疑，让我们整理一下，再接着学习下一个模块的内容。

## 问题一

> 
<p>@小美<br>
既然一个JVM是一个进程，JVM上跑Tomcat，Tomcat上可以部署多个应用。这样的话，每个跑在Tomcat上的应用是一个线程吗？该怎么理解“如果一个应用crash了，其他应用也会crash”？</p>


理解程序运行时的执行环境，直观感受程序是如何运行的，对我们开发和维护软件很有意义。我们以小美同学提的这个场景为例，看下Java Web程序的运行时环境是什么样的，来重新梳理下进程、线程、应用、Web容器、Java虚拟机和操作系统之间的关系。

我们用Java开发Web应用，开发完成，编译打包以后得到的是一个war包，这个war包放入Tomcat的应用程序路径下，启动Tomcat就可以通过HTTP请求访问这个Web应用了。

在这个场景下，进程是哪个？线程有哪些？Web程序的war包是如何启动的？HTTP请求如何被处理？Tomcat在这里扮演的是什么角色？JVM又扮演什么角色？

首先，我们是通过执行Tomcat的Shell脚本启动Tomcat的，而在Shell脚本里，其实启动的是Java虚拟机，大概是这样一个Shell命令：

```
java org.apache.catalina.startup.Bootstrap &quot;$@&quot; start 

```

所以我们在Linux操作系统执行Tomcat的Shell启动脚本，Tomcat启动以后，其实在操作系统里看到的是一个**JVM虚拟机进程**。这个虚拟机进程启动以后，加载class进来执行，首先加载的就这个`org.apache.catalina.startup.Bootstrap`类，这个类里面有一个`main()`函数，是整个Tomcat的入口函数，JVM虚拟机会启动一个**主线程**从这个入口函数开始执行。

主线程从Bootstrap的main()函数开始执行，初始化Tomcat的运行环境，这时候就需要**创建一些线程**，比如负责监听80端口的线程，处理客户端连接请求的线程，以及执行用户请求的线程。创建这些线程的代码是Tomcat代码的一部分。

初始化运行环境之后，Tomcat就会扫描Web程序路径，扫描到开发的war包后，再加载war包里的类到JVM。因为Web应用是被Tomcat加载运行的，所以我们也称**Tomcat为Web容器**。

如果有外部请求发送到Tomcat，也就是外部程序通过80端口和Tomcat进行HTTP通信的时候，Tomcat会根据war包中的web.xml配置，决定这个请求URL应该由哪个Servlet处理，然后Tomcat就会**分配一个线程去处理这个请求**，实际上，就是**这个线程执行相应的Servlet代码**。

我们回到小美同学的问题，Tomcat启动的时候，启动的是JVM进程，这个进程首先是执行JVM的代码，而JVM会加载Tomcat的class执行，并分配一个主线程，这个主线程会从main函数开始执行。在主线程执行过程中，Tomcat的代码还会启动其他一些线程，包括处理HTTP请求的线程。

而我们开发的应用是一些class，被Tomcat加载到这个JVM里执行，所以，即使这里有多个应用被加载，也只是加载了一些class，我们的应用被加载进来以后，并没有增加JVM进程中的线程数，也就是web应用本身和线程是没有关系的。

而Tomcat会根据HTTP请求URL执行应用中的代码，这个时候，可以理解成每个请求分配一个线程，每个线程执行的都是我们开发的Web代码。如果Web代码中包含了创建新线程的代码，Tomcat的线程在执行代码时，就会**创建出新的线程**，这些线程也会被操作系统调度执行。

如果Tomcat的线程在执行代码时，代码抛出未处理的异常，那么当前线程就会结束执行，这时控制台看到的异常信息，其实就是线程堆栈信息，线程会把异常信息以及当前堆栈的方法都打印出来。事实上，这个异常最后还是会被Tomcat捕获，然后Tomcat会给客户端返回一个500错误。单个线程的异常不影响其他线程执行，也就是不影响其他请求的处理。

但是如果线程在执行代码的时候，抛出的是JVM错误，比如`OutOfMemoryError`，这个时候看起来是应用crash，事实上是整个进程都无法继续执行了，也就是进程crash了，进程内所有应用都不会被继续执行了。

从JVM的角度看，Tomcat和我们的Web应用是一样的，都是一些Java代码，但是Tomcat却可以加载执行Web代码，而我们的代码又不依赖Tomcat，这也是一个很有意思的话题。Tomcat是如何设计的，我将会在下个模块讲述。

## 问题二

> 
<p>@黄海峰<br>
有点难以想象，“Hash表的时间复杂度为什么是O(1)”这个问题居然有阿里大厂的面试官觉得难。</p>


这不是一个疑问，但其实是一个有意思的话题，我们花一点时间讨论下，也许会对你的职业规划有所启发。

文中这个故事大概发生在2009年，整整十年前，那个时候互联网还不像今天这样炙手可热，提供的薪水也不像今天这样有竞争力，也没有BAT这样的专有名词指代所谓的互联网巨头。那个时候，计算机专业优秀的毕业生向往的是微软、Oracle、IBM这样的外资IT巨头，退而求其次，国内好的IT公司是联想、用友这些企业。

事实上，那个时候在技术研发能力上，互联网公司的技术能力也是落后传统企业的，阿里巴巴最核心的数据存储依赖的是IBM、Oracle、EMC的解决方案，即所谓的IOE。

所以在十年前的人才市场上，国内互联网公司的形象一般是：技术落后、薪水一般、加班严重、没有名气。可以说在人才市场的竞争中，相比国内外的IT巨头是落于下风的。

我个人感觉，互联网公司的崛起大概是在七八年前，移动互联网开始出现，互联网的渗透率得到加速，BAT逐渐开始成为家喻户晓的名字，名气大涨。其次，经过前面时间的积累，互联网企业主导的各种分布式技术、大数据技术、移动互联网技术、云计算技术的风头超过传统IT巨头，阿里巴巴开始去IOE，打造自己的云计算平台，成为先进技术的代表者；最主要的还是互联网企业盈利能力大幅增加，能够提供市场上更有竞争力的薪水和股票。

于是互联网企业在人才市场上开始变得灼手可热，BAT这些企业开始被人称为“大厂”。我们今天感觉这些互联网巨头高高在上，人们纷纷向往。事实上，这个现象出现的时间非常短。今天这些企业有足够的名气和资源将自己营造得高高在上，可以在众多优秀的候选人中间挑来选去，仅仅在十年前，还不是这样的。

但是事情真正的吊诡之处还不在这里，当今这些互联网大厂的核心技术和业务模式在十几年前就已经奠定了，经过几年的摸索，大概在七八年前开始稳定成熟。也就是说，互联网企业的技术实力和商业能力是在这些企业还默默无闻的时候就发展起来的，而在这些企业成为明星之后，并没有什么突破性的进展。想想这些所谓的互联网大厂，最近几年，并没有什么值得称道的商业模式创新和技术创新。

也就是说，十多年前，可能是一些并不优秀的技术人员加入一个并不出名的公司，然后这些人开创出了一个杰出的事业。用马云的话说，就是“二流的人做一流的事”。然后公司开始挑选一流的人，但结果似乎只是在维持这个事业，并没有开创出更加杰出的事业。今天的BAT似乎成为当年的IBM，历史好像进入了某种循环。

如果这就是事情的真相，我想你或许可以从其中得到某些启发，重新考虑下未来的职业规划。也许你会发现，你可能不需要追逐当前所谓的热门技术，而应该好好想想需要为自己的未来准备些什么。

最后，在第一模块中，我在每一篇文章的下面都留了几道思考题，各位同学在评论区都有很好的答案。但只有[第五篇文章](https://time.geekbang.org/column/article/169533)，我似乎没有看到比较准确的答案，我在这里回答一下。

RAID5中，校验位之所以螺旋式地落在所有硬盘上，主要原因是因为如果将校验位记录在同一块硬盘上，那么对于其他多块数据盘，任何一块硬盘修改数据，都需要修改这个校验盘上的校验数据，也就是说，对于有8块硬盘的RAID5阵列，校验盘的数据写入压力是其他数据盘的7倍。而硬盘的频繁写入会导致硬盘寿命缩短，校验盘会频繁损坏，存储的整体可用性和维护性都会变差。

所以，作为软件架构师，当你在进行软件设计的时候，你不光需要考虑软件本身，你还需要了解软件的各种约束，硬盘的特性约束是一种，当然还有其他一些约束，我会在专栏的后面模块中继续讲解如何在各种约束下，设计出符合期望的软件系统。
