Tomcat 的线程池原理与 JVM 内存回收形象生动的介绍
Posted sp42a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tomcat 的线程池原理与 JVM 内存回收形象生动的介绍相关的知识,希望对你有一定的参考价值。
Tomcat 的线程池原理
原理
我们先来看一下 Tomcat 的线程池原理:
tomcat 原理如上图。Tomcat 线程池在工作的时候,实际情况是:以上述线程池为例,一开始就创建最小空闲数的线程在池里,20个,当同一时间请求数量大于最小空闲数20,比如来了50个并发请求,那么线程池还需要创建30个线程来处理请求。这时候当请求都处理完了,持续来的请求低于50个的时候,那么当时间过了60秒,并发数还是没有达到50,那么从第50个线程开始,线程池将按照,空闲时间达到60s的,开始逐个回收,49个,48个,47个,如此回收。如果并发请求小于20个,那么线程池会回收至20个的时候,停止回收,这就是最小空闲数的作用,即使一个请求都没有,那么线程池也得保证随时都有20个。所谓空闲回收是指:一个线程在60s的时间内,一直处于等待。那么就可以判定该线程是空闲。如果这个空闲线程是在最小空闲数以上,则会被回收。当请求并发高于500最大空闲数的时候,线程池是会继续创建线程的,来满足特大突发性并发。当并发请求数降下之后,线程池中有空闲,那么,无论线程空闲时间是否达到60s,线程池都会进行回收至500。500以类的线程也会根据空闲时间是否大于60s来判断是否需要进行回收。
如何处理并发
下面我们详细结合实际情况来阐述 tomcat 线程池在实际运用中,是如何工作的,如何处理并发的。
可以结合这个来看,最高的线程如果是繁忙的话,那么说明 tomcat 线程已经被打满了。
在短时间周期内,如果线程数忽然持续走高,说明有突发性请求已经打过来,且正在创建更多的线程去执行,这时候创建线程的过程中,请求处于被等待,越是最后的请求被等待的时间越长。而过一段时间,线程数降下去很多的话,说明突发性请求已经过去了,线程池里的线程空闲时间达到了最大空闲时间,比如60s,那么即将被回收。
我个人觉得,这种现象应该被归属于不健康状态。因为请求来了,如果等待的线程只有10个以下,那么等待时间不会太长。但是如果等待时间达到10个以上,等待时间就会呈几何方式上升的,且线程处理时间也会呈几何倍数上升,因为同一个线程池里的线程之间要相互竞争 CPU 资源,比如现在 tomcat 运行中有1000个线程,假如每个 CPU 的时间片为0.01秒,那么,轮到第1000个线程执行时,该线程实际已经等待了999个时间片0.01=1秒了,这还只是一次时间片执行,0.01秒相当于10ms,而咱们的一个请求正常情况下的时间是:30ms,那一个请求要成功就需要等待3次cpu时间片来执行,如果 tomcat 线程池一直有1000个在运行,那正常情况下一个请求的执行时间应该是:0.03+9990.013=3.03s,可想而知,执行一个请求只需要30ms,可是却要等待需要3s,这是相当不靠谱的且难以接受的。再假如,时间片是20ms,那么一个请求从进入等待到执行完毕的执行时间应该是多少?0.03+9990.02(0.03/0.02=取大于整数=2)+CPU 切换时间 a 999次=4.03s+999a,这已经很夸张了吧!
咱们还可以理想一点,加入线程池里只有500个活跃线程,那么上面的公式应该是:当 cpu 时间片为0.01s时,最后一个请求需要处理成功,需要用时:0.03+5000.013= 1.53s,当 cpu 时间片为0.02s时,最后一个请求需要处理成功,需要用时:0.03+5000.022= 2.03s,当 cpu 时间片为0.03s时,最后一个请求需要处理成功,需要用时:0.03+5000.031= 1.53s。
但是这只是cpu执行时间,还要加上网络传输,创建线程时间,cpu 切换线程所需时间,还有其他时间,总共加起来,就是一个很可怕的时间了。所以,即使 tomcat 有线程池,但最好不要总是超过最小空闲数,这是最优的情况,最次的情况就是 tomcat 线程池中线程数超过最大空闲数了,线程们总是繁忙地工作,宕机随时有可能。
而健康状态是:线程池保持最小空闲数才算是健康,如果你的 tomcat 线程数总是超过最小空闲数,那么你的程序随时都处于繁忙状态,这时候出错的几率已经大大增加。是时候需要被重点关注了。
我们重点看2个部分
线程池:
他们的指标含义总结如下:
- [当前线程数]代表当前线程池中 ready 好的数量,有的在运行状态,有的在等待状态
- [当前线程忙]| 代表活动状态的线程数量
- [最大线程]代表最大的线程数量
- [最大空闲线程]代表最大的空闲线程数量
- [最小空闲线程]代表最小的空闲线程数量
我们主要关注,活动状态的线程数量和[当前线程数]是否接近了最大的线程数量。如果是,则需要加大[最大线程]的数量。如果硬件支持不了更多的线程数量,就需要换更强大的硬件。如下图,我们可以看到线程的明细:
这里有几个状态解释一下:
- runnable:正在正常运行的线程
- waiting:等待中,是指线程拥有了某个锁之后,调用了 wait 方法,等待其他线程或者锁唤醒该线程继续下一步操作
- time_waiting:有限制的 waiting
- blocked:阻塞状态。代表线程繁忙正在执行中,可能会有资源等待的情况。比如我们常说的多个线程同步操作的场景,一个线程正在等待另外一个线程的同步块释放;我们需要关注长期的 blocked 状态线程。Dump 线程栈就可以找到程序,从而分析出在做什么操作,等待哪些资源,这个是分析问题,解决问题的常用方法。
- Blocked 和 waiting 的区别,Blocked 是临界点外边等待进入(等待获取锁占用资源),waiting 是在临界点里边等待唤醒。
- terminated:表示该线程已经执行完毕,进入死亡状态,如果线程被上时间持有,则有可能不会被回收。
通过Probe,我们还可以查看JVM的内存使用情况。
- 一个人(对象)出来(new 出来)后会在 Eden Space(伊甸园)无忧无虑的生活,直到 GC 到来打破了他们平静的生活。GC 会逐一问清楚每个对象的情况,有没有钱(此对象的引用)啊,因为 GC 想赚钱呀,有钱的才可以敲诈嘛。然后富人就会进入 Survivor Space(幸存者区),穷人的就直接kill掉。
- 并不是进入 Survivor Space(幸存者区)后就保证人身是安全的,但至少可以活段时间。GC 会定期(可以自定义)会对这些人进行敲诈,亿万富翁每次都给钱,GC 很满意,就让 其进入了 Genured Gen(养老区)。万元户经不住几次敲诈就没钱了,GC 看没有啥价值啦,就直接kill掉了。
- 进入到养老区的人基本就可以保证人身安全啦,但是亿万富豪有的也会挥霍成穷光蛋,只要钱没了,GC 还是 kill 掉。
分区的目的:新生区由于对象产生的比较多并且大都是朝生夕灭的,所以直接采用标记-清理算法。而养老区生命力很强,则采用复制算法,针对不同情况使用不同算法。
非 heap 区域中 Perm Gen 中放着类、方法的定义,JVM Stack 区域放着方法参数、局域变量等的引用,方法执行顺序按照栈的先入后出方式。
GC工作机制
SUN 的 JVM 内存池被划分为以下几个部分:
- Eden Space (heap) 内存最初从这个线程池分配给大部分对象。
- Survivor Space (heap) 用于保存在 eden space 内存池中经过垃圾回收后没有被回收的对象。
- Tenured Generation (heap) 用于保持已经在 survivor space 内存池中存在了一段时间的对象。
- Permanent Generation (non-heap) 保存虚拟机自己的静态(reflective)数据,例如类(class)和方法(method)对象。Java 虚拟机共享这些类数据。这个区域被分割为只读的和只写的。
- Code Cache (non-heap) HotSpot Java 虚拟机包括一个用于编译和保存本地代码(native code)的内存,叫做“代码缓存区”(code cache)。
简单来讲,JVM 的内存回收过程是这样的: 对象在 Eden Space 创建,当 Eden Space 满了的时候,gc 就把所有在 Eden Space中的对象扫描一次,把所有有效的对象复制到第一个 Survivor Space,同时把无效的对象所占用的空间释放。当 Eden Space再次变满了的时候,就启动移动程序把 Eden Space 中有效的对象复制到第二个 Survivor Space,同时,也将第一个 Survivor Space 中的有效对象复制到第二个 Survivor Space。如果填充到第二个 Survivor Space 中的有效对象被第一个 Survivor Space 或 Eden Space 中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到 Permanent Generation。
若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行 Full GC,此时 jvm gc 停止所有在堆中运行的线程并执行清除动作。
点击“OS 信息”可查看 OS 与 JVM 对比使用情况,如下图所示:
注:在进行性能测试时,可以通过内存使用和 OS 信息判断是否出现内存溢出等问题
Connectors 界面
切换到“Connectors”界面,查看连接器信息,如下图所示:
连接器主要是接收用户的请求,然后封装请求传递给容器处理,tomcat 中支持两种协议的连接器:HTTP/1.1 与 AJP/1.3,HTTP/1.1 协议负责建立 HTTP 连接,web 应用通过浏览器访问tomcat服务器用的就是这个连接器,默认监听的是8080端口;AJP/1.3 协议负责和其他 HTTP 服务器建立连接,监听的是 8009 端口,比如 tomcat 和 apache 或者 iis 集成时需要用到这个连接器。协议上有三种不同的实现方式:JIO、APR、NIO。
实战演示:
我们对服务形成持续的请求测试,看 JVM 的变化:
执行过程中在 Probe 界面的程序应用中,查看 jiance 程序相关信息,如下图所示:
我们这里可以查看请求数量,可以和 jmter 的聚合报告做对比:
然后我们查看一下内存使用情况,在 Probe 中跳转到系统信息界面,如下图所示:
在此界面主要关注 PS Eden Space、PS Survivor Space 和 PS Old Gen 即可,通过相关内存的变动判断是否出现性能瓶颈。
点击系统信息下的 OS 信息,可以查看 JVM CPU 等相关参数的历史图表,如下图所示:
通过查看下图,可以核查是否有内存溢出:
以上是关于Tomcat 的线程池原理与 JVM 内存回收形象生动的介绍的主要内容,如果未能解决你的问题,请参考以下文章