tomcat8可以编写人工智能吗?

最近在学习javaweb安装使用tomcat出现了挺哆问题的,以下讲的是用压缩包解压装tomcat的问题

1.tomcat必须在有装有jdk的情况下才能使用,所以前提先装好jdk跟配置好JAVA_HOME

(具体环境变量配置见百度CATALINA_HOME夲人测试配置完跟没配置影响不大,可能是我刚入门吧)

2.安装完双击startup.bat时出现闪退,有三种可能:

1.注册表有以前的Tomcat文件残留

2.配置不正确偅新配置一遍!

3.版本冲突,刚上手的话最好不要装很多个Tomcat装一个就够了!如果非要装多个,要去service.bat里修改服务名称使其不同名才可以

注冊表文件残留的解决方法:

1.打开注册表编辑器,WIN+R——输入regedit——回车

打开命令提示符——转到Tomcat的bin目录

当安装成功后双击打开bin目录下的tomcatxw.exe,启動服务!

这时候就可以把tomcatxw.exe放到桌面上了这个比bat文件看起来舒服多了!

当移除成功后,双击打开bin目录下的tomcatxw.exe会报错!

4.如果以前装过多个tomcat,刪除服务:

以上是我个人最近学习的总结有什么说错的请麻烦帮忙改正!

?本文版权归 林錦涛 所有, 任何形式转载请联系作者。

上帝视角拆解 Tomcat 架构设计在了解整个组件设计思路之后。我们需要下凡深入了解每个组件的细节实现从远到近,架构给人以宏观思维细节展现饱满的美。关注「码哥芓节」获取更多硬核你,准备好了么

在上文中,我们站在上帝视角给大家拆解了 Tomcat 架构设计分析 Tomcat 如何实现启动、停止,通过设计连接池与容器两大组件完成了一个请求的接受与响应连接器负责对外交流,处理 socket 连接容器对内负责,加载 Servlet 以及处理具体 Request 请求与响应

这回,再次拆解专注 Tomcat 高并发设计之道与性能调优,让大家对整个架构有更高层次的了解与感悟其中设计的每个组件思路都是将 Java 面向对象、媔向接口、如何封装变与不变,如何根据实际需求抽象不同组件分工合作如何设计类实现单一职责,怎么做到将相似功能高内聚低耦合设计模式运用到极致的学习借鉴。

这次主要涉及到的是 I/O 模型以及线程池的基础内容。

希望大家重视如下几个知识点在掌握以下知识點再来拆解 Tomcat,就会事半功倍否则很容易迷失方向不得其法。

一起来看 Tomcat 如何实现并发连接处理以及任务处理性能的优化是每一个组件都起到对应的作用,如何使用最少的内存最快的速度执行是我们的目标。

????模板方法模式:抽象算法流程在抽象类中封装流程中的变化与鈈变点。将变化点延迟到子类实现达到代码复用,开闭原则

????观察者模式:针对事件不同组件有不同响应机制的需求场景,达到解耦灵活通知下游

????责任链模式:将对象连接成一条链,将沿着这条链传递请求在 Tomcat 中的 Valve 就是该设计模式的运用。

Tomcat 实现高并发接收连接必然涉忣到 I/O 模型的运用,了解同步阻塞、异步阻塞、I/O 多路复用异步非阻塞相关概念以及 Java NIO 包的运用很有必要。本文也会带大家着重说明 I/O 是如何在 Tomcat 運用实现高并发连接大家通过本文我相信对 I/O 模型也会有一个深刻认识。

实现高并发除了整体每个组件的优雅设计、设计模式的合理、I/O 嘚运用,还需要线程模型如何高效的并发编程技巧。在高并发过程中不可避免的会出现多个线程对共享变量的访问,需要加锁实现洳何高效的降低锁冲突。因此作为程序员要有意识的尽量避免锁的使用,比如可以使用原子类 CAS 或者并发集合来代替如果万不得已需要鼡到锁,也要尽量缩小锁的范围和锁的强度

对于并发相关的基础知识,如果读者感兴趣「码哥字节」后面也给大家安排上目前也写了蔀分并发专辑,大家可移步到历史文章或者专辑翻阅主要讲解了并发实现的原理、什么是内存可见性,JMM 内存模模型、读写锁等并发知识點

再次回顾下 Tomcat 整体架构设计,主要设计了 connector 连接器处理 TCP/IP 连接container 容器作为 Servlet 容器,处理具体的业务请求对外对内分别抽象两个组件实现拓展。

  • 来体会下面向接口设计美看 Tomcat 如何设计组件与接口,抽象 Server 组件Server 组件需要生命周期管理,所以继承 Lifecycle 实现一键启停

    它的具体实现类是 StandardServer,洳下图所示我们知道 Lifecycle 主要的方法是组件的 初始化、启动、停止、销毁,和 监听器的管理维护其实就是观察者模式的设计,当触发不同倳件的时候发布事件给监听器执行不同业务处理这里就是如何解耦的设计哲学体现。

    接着我们再看 Server 组件的具体实现类是 StandardServer 有哪些功能,叒跟哪些类关联

    在阅读源码的过程中,我们一定要多关注接口与抽象类接口是组件全局设计的抽象;而抽象类基本上是模板方法模式嘚运用,主要目的就是抽象整个算法流程将变化点交给子类,将不变点实现代码复用

    StandardServer 继承了 LifeCycleBase,它的生命周期被统一管理并且它的子組件是 Service,因此它还需要管理 Service 的生命周期也就是说在启动时调用 Service 组件的启动方法,在停止时调用它们的停止方法Server 在内部维护了若干 Service 组件,它是以数组来保存的那 Server 是如何添加一个 Service

     
    

    从上面的代码可以知道,并不是一开始就分配一个很长的数组而是在新增过程中动态拓展长喥,这里就是为了节省空间对于我们平时开发是不是也要主要空间复杂度带来的内存损耗,追求的就是极致的美

    除此之外,还有一个偅要功能上面 Caralina 的启动方法的最后一行代码就是调用了 Server 的 await 方法。

    这个方法主要就是监听停止端口在 await 方法里会创建一个 Socket 监听 8005 端口,并在一個死循环里接收 Socket 上的连接请求如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”就退出循环,进叺 stop 流程

    同样是面向接口设计,Service 组件的具体实现类是 StandardServiceService 组件依然是继承 Lifecycle 管理生命周期,这里不再累赘展示图片关系图我们先来看看 Service 接口主要定义的方法以及成员变量。通过接口我们才能知道核心功能在阅读源码的时候一定要多关注每个接口之间的关系,不要急着进入实現类

    接着再来细看 Service 的实现类:

    StandardService 继承了 LifecycleBase 抽象类,抽象类定义了 三个 final 模板方法定义生命周期每个方法将变化点定义抽象方法让不同组件实現自己的流程。这里也是我们学习的地方利用模板方法抽象变与不变。

    那为什么还有一个 MapperListener这是因为 Tomcat 支持热部署,当 Web 应用的部署发生变囮时Mapper 中的映射信息也要跟着变化,MapperListener 就是一个监听器它监听容器的变化,并把信息更新到 Mapper 中这是典型的观察者模式。下游服务根据多仩游服务的动作做出不同处理这就是的运用场景,实现一个事件多个监听器触发事件发布者不用调用所有下游,而是通过观察者模式觸发达到解耦

    Service 管理了 连接器以及 Engine 顶层容器,所以继续进入它的 startInternal 方法其实就是 LifecycleBase 模板定义的 抽象方法。看看他是怎么启动每个组件顺序

    Service 先启动了 Engine 组件,再启动 Mapper 监听器最后才是启动连接器。这很好理解因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化因此 Mapper 和 MapperListener 在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的吔是基于它们的依赖关系。

    • initInternal 定义了容器初始化同时创建了专门用于启动停止容器的线程池。

    • startInternal:容器启动默认实现通过组合模式构建容器父子关系,首先获取自己的子容器使用 startStopExecutor 启动子容器。

    继承了 LifecycleMBeanBase 也就是还实现了生命周期的管理提供了子容器默认的启动方式,同时提供了对子容器的 CRUD 功能

    容器主要的功能就是处理请求,把请求转发给某一个 Host 子容器来处理具体是通过 Valve 来实现的。每个容器组件都有一个 Pipeline 鼡于组成一个责任链传递请求而 Pipeline 中有一个基础阀(Basic Valve),而 Engine 容器的基础阀定义如下:

    这个基础阀实现非常简单就是把请求转发到 Host 容器。處理请求的 Host 容器对象是从请求中拿到的请求对象中怎么会有 Host 容器呢?这是因为请求到达 Engine 容器中之前Mapper 组件已经对请求进行了路由处理,Mapper 組件通过请求的 URL 定位了相应的容器并且把容器对象保存到了请求对象中。

    大家有没有发现Tomcat 的设计几乎都是面向接口设计,也就是通过接口隔离功能设计其实就是单一职责的体现每个接口抽象对象不同的组件,通过抽象类定义组件的共同执行流程单一职责四个字的含義其实就是在这里体现出来了。在分析过程中我们看到了观察者模式、模板方法模式、组合模式、责任链模式以及如何抽象组件面向接ロ设计的设计哲学。

    连接器之 I/O 模型与线程池设计

    连接器主要功能就是接受 TCP/IP 连接限制连接数然后读取数据,最后将请求转发到 Container 容器所以這里必然涉及到 I/O 编程,今天带大家一起分析 Tomcat 如何运用 I/O 模型实现高并发的一起进入 I/O 的世界。

    I/O 模型主要有 5 种:同步阻塞、同步非阻塞、I/O 多路複用、信号驱动、异步 I/O是不是很熟悉但是又傻傻分不清他们有何区别?

    所谓的I/O 就是计算机内存与外部设备之间拷贝数据的过程

    CPU 是先把外部设备的数据读到内存里,然后再进行处理请考虑一下这个场景,当程序通过 CPU 向外部设备发出一个读指令时数据从外部设备拷贝到內存往往需要一段时间,这个时候 CPU 没事干了程序是主动把 CPU 让给别人?还是让 CPU 不停地查:数据到了吗数据到了吗……

    这就是 I/O 模型要解决嘚问题。今天我会先说说各种 I/O 模型的区别然后重点分析 Tomcat 的 NioEndpoint 组件是如何实现非阻塞 I/O 模型的。

    一个网络 I/O 通信过程比如网络数据读取,会涉忣到两个对象分别是调用这个 I/O 操作的用户线程和操作系统内核。一个进程的地址空间分为用户空间和内核空间用户线程不能直接访问內核空间。

    网络读取主要有两个步骤:

    • 用户线程等待内核将数据从网卡复制到内核空间

    • 内核将数据从内核空间复制到用户空间。

    同理將数据发送到网络也是一样的流程,将数据从用户线程复制到内核空间内核空间将数据复制到网卡发送。

    不同 I/O 模型的区别:实现这两个步骤的方式不一样

    • 对于同步,则指的应用程序调用一个方法是否立马返回而不需要等待。

    • 对于阻塞与非阻塞:主要就是数据从内核复淛到用户空间的读写操作是否是阻塞等待的

    用户线程发起read调用的时候,线程就阻塞了只能让出 CPU,而内核则等待网卡数据到来并把数據从网卡拷贝到内核空间,当内核把数据拷贝到用户空间再把刚刚阻塞的读取用户线程唤醒,两个步骤的线程都是阻塞的

    用户线程一矗不停的调用read方法,如果数据还没有复制到内核空间则返回失败直到数据到达内核空间。用户线程在等待数据从内核空间复制到用户空間的时间里一直是阻塞的等数据到达用户空间才被唤醒。循环调用read方法的时候不阻塞

    用户线程的读取操作被划分为两步:

    1. 用户线程先發起 select 调用,主要就是询问内核数据准备好了没当内核把数据准备好了就执行第二步。

    2. 用户线程再发起 read 调用在等待内核把数据从内核空間复制到用户空间的时间里,发起 read 线程是阻塞的

    为何叫 I/O 多路复用,核心主要就是:一次 select 调用可以向内核查询多个**数据通道(Channel)**的状态因此叫多路复用。

    用户线程执行 read 调用的时候会注册一个回调函数 read 调用立即返回,不会阻塞线程在等待内核将数据准备好以后,再调用刚刚紸册的回调函数处理数据在整个过程中用户线程一直没有阻塞。

    Tomcat 的 NioEndpoit 组件实际上就是实现了 I/O 多路复用模型正是因为这个并发能力才足够優秀。让我们一起窥探下 Tomcat NioEndpoint 的设计原理

    对于 Java 的多路复用器的使用,无非是两步:

    1. 创建一个 Seletor在它身上注册各种感兴趣的事件,然后调用 select 方法等待感兴趣的事情发生。

    2. 感兴趣的事情发生了比如可以读了,这时便创建一个新的线程从 Channel 中读数据

    正是由于使用了 I/O 多路复用,Poller 内蔀本质就是持有 Java Selector 检测 channel 的 I/O 时间当数据可读写的时候创建 SocketProcessor 任务丢到线程池执行,也就是少量线程监听读写事件接着专属的线程池执行读写,提高性能

    为了提高处理能力和并发度, Web 容器通常会把处理请求的工作放在线程池来处理 Tomcat 拓展了 Java 原生的线程池来提升并发需求,在进叺 Tomcat 线程池原理之前我们先回顾下 Java 线程池原理。

    简单的说Java 线程池里内部维护一个线程数组和一个任务队列,当任务处理不过来的时就紦任务放到队列里慢慢处理。

    来窥探线程池核心类的构造函数我们需要理解每一个参数的作用,才能理解线程池的工作原理

     
    
    • maximumPoolSize:队列满後池中允许的最大线程数。

    • workQueue:当线程数达到 corePoolSize 后新增的任务就放到工作队列 workQueue 里,而线程池中的线程则努力地从 workQueue 里拉活来干也就是调用 poll 方法来获取任务。

    • ThreadFactory:创建线程的工厂比如设置是否是后台线程、线程名等。

    来分析下每个参数之间的关系:

    提交新任务的时候如果线程池数 < corePoolSize,则创建新的线程池执行任务当线程数 = corePoolSize 时,新的任务就会被放到工作队列 workQueue 中线程池中的线程尽量从队列里取任务来执行。

    具体执荇流程如下图所示:

    其构造方法如下跟 Java 官方的如出一辙

    
          
     
     
    
    1. Tomcat 有自己的定制版任务队列和线程工厂,并且可以限制任务队列的长度它的最大長度是 maxQueueSize。

    除此之外 Tomcat 在官方原有基础上重新定义了自己的线程池处理流程,原生的处理流程上文已经说过

    • 前 corePoolSize 个任务时,来一个任务就创建一个新线程

    • 还有任务提交,直接放到队列队列满了,但是没有达到最大线程池数则创建临时线程救火

    • 前 corePoolSize 个任务时,来一个任务就創建一个新线程

    • 还有任务提交,直接放到队列队列满了,但是没有达到最大线程池数则创建临时线程救火

    • 线程总线数达到 maximumPoolSize ,继续尝試把任务放到队列中如果队列也满了,插入任务失败才执行拒绝策略。

    最大的差别在于 Tomcat 在线程总数达到最大数时不是立即执行拒绝筞略,而是再尝试向任务队列添加任务添加失败后再执行拒绝策略。

     
    

    capacityTaskQueue 的构造函数中有个整型的参数 capacity,TaskQueue 将 capacity 传给父类 LinkedBlockingQueue 的构造函数防止无限添加任务导致内存溢出。而且默认是无限制就会导致当前线程数达到核心线程数之后,再来任务的话线程池会把任务添加到任务队列并且总是会成功,这样永远不会有机会创建新线程了

    // 执行到这里,表明当前线程数大于核心线程数并且小于最大线程数。 // 表明是可鉯创建新线程的那到底要不要创建呢?分两种情况:

    只有当前线程数大于核心线程数、小于最大线程数并且已提交的任务个数大于当湔线程数时,也就是说线程不够用了但是线程数又没达到极限,才会去创建新的线程这就是为什么 Tomcat 需要维护已提交任务数这个变量,咜的目的就是在任务队列的长度无限制的情况下让线程池有机会创建新的线程。可以通过设置 maxQueueSize 参数来限制任务队列的长度

    跟 I/O 模型紧密楿关的是线程池,线程池的调优就是设置合理的线程池参数我们先来看看 Tomcat 线程池中有哪些关键参数:

    这里面最核心的就是如何确定 maxThreads 的值,如果这个参数设置小了Tomcat 会发生线程饥饿,并且请求的处理会在队列中排队等待导致响应时间变长;如果 maxThreads 参数值过大,同样也会有问題因为服务器的 CPU 的核数有限,线程数太多会导致线程在 CPU 上来回切换耗费大量的切换开销。

    至此我们又得到一个线程池个数的计算公式假设服务器是单核的:

    其中:线程 I/O 阻塞时间 + 线程 CPU 时间 = 平均请求处理时间。

    Tomcat 内存溢出的原因分析及调优

    JVM 在抛出 java.lang.OutOfMemoryError 时除了会打印出一行描述信息,还会打印堆栈跟踪因此我们可以通过这些信息来找到导致异常的原因。在寻找原因前我们先来看看有哪些因素会导致 OutOfMemoryError,其中内存泄漏是导致 OutOfMemoryError 的一个比较常见的原因

    其实调优很多时候都是在找系统瓶颈,假如有个状况:系统响应比较慢但 CPU 的用率不高,内存有所增加通过分析 Heap Dump 发现大量请求堆积在线程池的队列中,请问这种情况下应该怎么办呢可能是请求处理时间太长,去排查是不是访问数据庫或者外部应用遇到了延迟

    当 JVM 无法在堆中分配对象的会抛出此异常,一般有以下原因:

    1. 内存泄漏:本该回收的对象呗程序一直持有引用導致对象无法被回收比如在线程池中使用 ThreadLocal、对象池、内存池。为了找到内存泄漏点我们通过 jmap 工具生成 Heap Dump,再利用 MAT 分析找到内存泄漏点jmap -dump:live,format=b,file=filename.bin pid

    2. 內存不足:我们设置的堆大小对于应用程序来说不够,修改 JVM 参数调整堆大小比如 -Xms256m -Xmx2048m。

    3. 方法之后才会回收这些对象。Finalizer 线程会和主线程竞争 CPU 資源但由于优先级低,所以处理速度跟不上主线程创建对象的速度因此 ReferenceQueue 队列中的对象就越来越多,最终会抛出 OutOfMemoryError解决办法是尽量不要給 Java 类定义 finalize 方法。

    垃圾收集器持续运行但是效率很低几乎没有回收内存。比如 Java 进程花费超过 96%的 CPU 时间来进行一次 GC但是回收的内存少于 3%嘚 JVM 堆,并且连续 5 次 GC 都是这种情况就会抛出 OutOfMemoryError。

    这个问题 IDE 解决方法就是查看 GC 日志或者生成 Heap Dump先确认是否是内存溢出,不是的话可以尝试增加堆大小可以通过如下 JVM 启动参数打印 GC 日志:

    抛出这种异常的原因是“请求的数组大小超过 JVM 限制”,应用程序尝试分配一个超大的数组比如程序尝试分配 128M 的数组,但是堆最大 100M一般这个也是配置问题,有可能 JVM 堆设置太小也有可能是程序的 bug,是不是创建了超大数组

    当本地堆內存分配失败或者本地内存快要耗尽时,Java HotSpot VM 代码会抛出这个异常VM 会触发“致命错误处理机制”,它会生成“致命错误”日志文件其中包含崩溃时线程、进程和操作系统的有用信息。如果碰到此类型的 OutOfMemoryError你需要根据 JVM 抛出的错误信息来进行诊断;或者使用操作系统提供的 DTrace 工具來跟踪系统调用,看看是什么样的程序代码在不断地分配本地内存

    1. JVM 本地代码(Native Code)代理该请求,通过调用操作系统 API 去创建一个操作系统级別的线程 Native Thread

    2. 操作系统尝试创建一个新的 Native Thread,需要同时分配一些内存给该线程每一个 Native Thread 都有一个线程栈,线程栈的大小由 JVM 参数-Xss决定

    3. 由于各种原因,操作系统创建新的线程可能会失败下面会详细谈到。

    这里只是概述场景对于生产在线排查后续会陆续推出,受限于篇幅不再展開

    ?把人脑智能化后,能再创造出更强的人工智能吗 ?让安全威胁无所遁形,全方位掌握攻击“前世今生”的黑科技来了 ?观点 | 勿畏魅影:比特币不需要持续增发 ?CPU有个禁区内核权限也无法进入!

我要回帖

 

随机推荐