前言

性能优化专题共计四个部分,分别是:

本节是性能优化专题第一部分 —— Tomcat性能优化篇,共计两个小节,分别是:

上一节主要分析了Tomcat的源码以及手写Tomcat源码,接下来咱们就来聊聊tomcat的性能优化,那怎么进行优化?哪些方面需要进行优化?先有一个整体的认知。

Tomcat性能优化思路

优化思路过渡

其实还是要回归到问题的本质,一个客户端的连接请求响应的流程,看看这个过程经历了什么,哪些地方能够优化。

当然,我要补充的一点是,服务器的CPU、内存、硬盘等对性能有决定性的影响,硬件这块配置越高越好。

现在我们依然从Tomcat的官方文档入手,看一看Tomcat核心组件的介绍,不光为了回顾,也是探究一下有什么端倪在其中。

  • 发现客户端的连接请求会和Connector打交道,对于Connector可以进行选择,比如Http Connector,AJP Connector。

整体介绍 : Documentation/Tomcat8.0/User Guide/21)Connectors链接

详细介绍 :Documentation/Tomcat8.0/Reference/Configuration/Connectors链接

  • Executor

介绍 :Documentation/Tomcat8.0/Reference/Configuration/Executors链接

  • Context

介绍 :Documentation/Tomcat8.0/Reference/Configuration/Containers/Context链接

  • Context中加载web.xml文件时的源码处理一些过滤器,全局servlet,session等等这些有一个全局的web.xml文件,在conf目录下,源码中会将两者进行合并处理。

conclusion:要想改变上面这些内容,适当进行调整,咱们去修改tomcat源码显然不合适,那怎么修改呢?tomcat给我们提供了可以进行定制自己组建的相关配置文件,比如说conf目录下的server.xml和web.xml文件,也就是说我们可以站在修改配置文件的角度进行性能优化 .

继续思考tomcat性能优化思路

既然tomcat是Java写的,最终这些代码是会跑到jvm虚拟机中的,也就是说jvm的一些优化思路也可以在tomcat中进行落实。

配置优化

conf/server.xml核心组件

  • Server

官网描述 :Server interface which is rarely customized by users.
服务器界面,很少由用户定制。

  • Service

官网描述 :The Service element is rarely customized by users.
服务元素很少由用户定制。

  • Connector

官网描述 :Creating a customized connector is a significant effort.
创建定制的连接器是一项重要的工作。

  • Engine

官网描述 :The Engine interface may be implemented to supply custom Engines, though this is uncommon.
引擎接口可以实现为提供自定义引擎,尽管这种情况并不常见。

  • Host

官网描述 :Users rarely create custom Hosts because the StandardHost implementation provides significant additional functionality.
用户很少创建自定义主机,因为StandardHost实现提供了重要的附加功能。

  • Context

官网描述 :The Context interface may be implemented to create custom Contexts, but this is rarely the case because the StandardContext provides significant additional functionality.
可以实现Context接口来创建自定义Context,但这很少发生,因为StandardContext提供了重要的附加功能。

Context既然代表的是web应用,是和我们比较接近的,这块我们考虑对其适当的优化

我们上面从官网上拿出了 conf/server.xml核心组件进行总结,我们发现貌似只有Connector and Context 可以更多地进行优化。

conf/server.xml非核心组件

官网 :Documentation/Reference/Configuration/Nested Components/xxx

  • Listener
    Listener(即监听器)定义的组件,可以在特定事件发生时执行特定的操作;被监听的事件通常是Tomcat的启动和停止。
<!--监听内存溢出-->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /> 
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" /> 
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" /> 
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
  • Global Resources

GlobalNamingResources元素定义了全局资源,通过配置可以看出,该配置是通过读取$TOMCAT_HOME/ conf/tomcat-users.xml实现的。

The GlobalNamingResources element defines the global JNDI resources for the Server
GlobalNamingResources元素定义了[Server]的全局JNDI资源。

<GlobalNamingResources>
	<Resource name="UserDatabase" auth="Container"
	type="org.apache.catalina.UserDatabase"
	description="User database that can be updated and saved"
	factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
	pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
  • Valve

功能类似于过滤器Filter

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
				prefix="localhost_access_log" suffix=".txt"
				pattern="%h %l %u %t &quot;%r&quot; %s %b" />
  • Realm

A Realm element represents a “database” of usernames, passwords, and roles (similar to Unix groups) assigned to those users.
Realm元素代表分配给这些用户的用户名,密码和角色(类似于Unix组)的“数据库”。

Realm,可以把它理解成“域”;Realm提供了一种用户密码与web应用的映射关系,从而达到角色安全管理的作用。在本例中,Realm的配置使用name为UserDatabase的资源实现。而该资源在Server元素中使用GlobalNamingResources配置

<Realm className="org.apache.catalina.realm.LockOutRealm">
	<!-- This Realm uses the UserDatabase configured in the global JNDI resources under the key "UserDatabase". Any edits
that are performed against this UserDatabase are immediately available for use by the Realm. -->
	<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
	resourceName="UserDatabase"/>
</Realm>

conf/web.xml

全局的web.xml文件有些标签用不到的,可以删除掉,具体后面会说。

JVM优化

内存设置

为了防止内存不够用,显然可以设置一下内存的大小

GC算法

选择合适的GC算法,其实内存大小的设置也会影响GC

小结

减少相关配置->查看日志tomcat启动时间

项目方法 :Connector->BIO/NIO/APR->压测某个项目的方法观察Throughout JVM :jconsole,gceasy.io,jvisual

Tomcat优化案例

软件环境准备:

  • jdk1.8
  • maven
  • git
  • idea
  • tomcat8.0
  • Xshell
  • jmeter:用于本地压测观察
  • ftp/rzsz:用于本地和远端文件交互

测试代码准备:

  • 项目架构: springboot 框架,只需加入一个 index.html页面(比如加入hello ,world !!)即可。

  • 项目介绍: 其实就是一个静态页面,接下来就是对这个页面进行压测。

JVisualVM监控java进程

(1)命令行输入jvisualvm

(2)选择本地的java进程【本地无需任何设置,直接连接即可】

(3)监控远程tomcat

输入远程ip地址,修改远端的catalina.sh文件,添加如下内容


JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=39.98.168.189 
-Dcom.sun.management.jmxremote.port=8998
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=true 
-Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password 
-Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access"

然后在jvisualvm中添加远程连接,JMX类型

采坑指南

查看hostname -i,修改/etc/hosts文件公网ip地址指向查询到的地址

lsof -i tcp:8998 得到PID

然后netstat -antup | grep PID 得到几个端口号,在阿里云安全组中添加相应端口

上述修改之后要重启远端的tomcat

添加JMX Connection,注意端口写8999

防火墙要记得添加对应的策略

在conf文件中添加两个文件jmxremote.access和jmxremote.password,内容分别为

guest readonly
manager readwrite
guest guest
manager manager

授予文件相应权限: chmod 600 jmxremot

tomcat-manager/probe : 如果不想用jvisualvm来监控tomcat线程内存的信息,也可以选择tomcat自带的tomcat-manager或者probe来监控,只是有些功能没有那么完善。

Tomcat性能优化

写的不错的一篇文章链接 :资料

配置优化

减少web.xml/server.xml中标签

最终观察tomcat启动日志[时间/内容],线程开销,内存大小,GC等

  • DefaultServlet

官网导航 :User Guide->Default Servlet

The default servlet is the servlet which serves static resources as well as serves the directory listings (if directory listings are enabled).
缺省的Servlet是既提供静态资源又提供目录列表的Servlet(如果启用了目录列表)。

<servlet>
	<servlet-name>default</servlet-name>
	<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
	<init-param>
		<param-name>debug</param-name>
		<param-value>0</param-value>
	</init-param>
	<init-param>
		<param-name>listings</param-name>
		<param-value>false</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
	<servlet-name>default</servlet-name>
	<url-pattern>/</url-pattern>
</servlet-mapping>
  • JspServlet
<servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        <init-param>
            <param-name>fork</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
            <param-name>xpoweredBy</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>3</load-on-startup>
</servlet>

<servlet-mapping>
       <servlet-name>default</servlet-name>
       <url-pattern>/</url-pattern>
</servlet-mapping>

 <!-- The mappings for the JSP servlet -->
<servlet-mapping>
       <servlet-name>jsp</servlet-name>
       <url-pattern>*.jsp</url-pattern>
       <url-pattern>*.jspx</url-pattern>
</servlet-mapping>
  • welcome-list-file
<welcome-file-list>
        <welcome-file>index.html</welcome-file>
        <welcome-file>index.htm</welcome-file>
        <welcome-file>index.jsp</welcome-file>
</welcome-file-list>
  • mime-mapping

移除响应的内容,支持的下载打开类型

<mime-mapping>
        <extension>123</extension>
        <mime-type>application/vnd.lotus-1-2-3</mime-type>
</mime-mapping>
<mime-mapping>
        <extension>3dml</extension>
        <mime-type>text/vnd.in3d.3dml</mime-type>
</mime-mapping>
  • session-config

默认jsp页面有session,就是在于这个配置

<session-config>
        <session-timeout>30</session-timeout>
</session-config>

调整优化server.xml中标签

Connector标签
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

对于protocol=“HTTP/1.1”,查看源码,Connector构造函数:

public Connector(String protocol) {
	setProtocol(protocol);
}

setProtocol(protocol)因为配置文件中传入的是HTTP/1.1并且这里没有使用APR,一会我们会演示APR

性能优化专题 02 - Tomcat性能优化之调优_性能优化

发现这里调用的是Http11NioProtocol,也就是说明tomcat8.0.x中默认使用的是NIO

使用同样的方式看tomcat7和tomcat8.5,你会发现tomcat7默认使用的是BIO,tomcat8.5默认使用的是NIO

针对BIO和NIO的方式进行压测

(1)BIO

来到tomcat官网Configuration/HTTP/protocol

org.apache.coyote.http11.Http11Protocol - blocking Java connector org.apache.coyote.http11.Http11NioProtocol - non blocking Java NIO connector org.apache.coyote.http11.Http11Nio2Protocol - non blocking Java NIO2 connector org.apache.coyote.http11.Http11AprProtocol - the APR/native connector.

使用tomcat7取巧一下,默认是BIO,端口改成7070,将tomcat-optimize复制到tomcat7中

(2)NIO

tomcat8.0中默认使用的是NIO

针对上述BIO和NIO的方式,进行压测,调整并发数,看吞吐量

(3)APR
【具体查看操作手册/APR安装方式】

下载并配置以下:

  • apr
  • apr-iconv
  • apr-util

配置bin/catalina.sh

LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/apr/lib export LD_LIBRARY_PATH

server.xml中的AprListener需要关闭SSL的方式,值设置为off,重新启动tomcat,看日志apr的方式,也可以压测看一下效果。

  • executor属性

最佳线程数公式 ????(线程等待时间+线程cpu时间)/线程cpu时间) * cpu数量

The Executor represents a thread pool that can be shared between components in Tomcat. Historically there has been a thread pool per connector created but this allows you to share a thread pool, between (primarily) connector but also other components when those get configured to support executors
执行器表示可以在Tomcat中的组件之间共享的线程池。从历史上看,每个连接器都会创建一个线程池,但是当配置为支持执行器时,您可以在(主要)连接器之间以及其他组件之间共享线程池

设置一些属性,这里参考官网

(1)acceptCount:达到最大连接数之后,等待队列中还能放多少连接,超过即拒绝,配置太大也没有意义

The maximum queue length for incoming connection requests when all possible request processing threads are in use. Any requests received when the queue is full will be refused. The default value is 100.
使用所有可能的请求处理线程时,传入连接请求的最大队列长度。队列已满时收到的任何请求都将被拒绝。默认值为100

(2)maxConnections

达到这个值之后,将继续接受连接,但是不处理,能继续接受多少根据acceptCount的值

BIO:maxThreads

NIO/NIO2:10000 ——— AbstractEndpoint.maxConnections

APR:8192

The maximum number of connections that the server will accept and process at any given time. When this number has been reached, the server will accept, but not process, one further connection. This additional connection be blocked until the number of connections being processed falls below maxConnections at which point the server will start accepting and processing new connections again. Note that once the limit has been reached, the operating system may still accept connections based on the acceptCount setting. The default value varies by connector type. For BIO the default is the value of maxThreads unless an Executor is used in which case the default will be the value of maxThreads from the executor. For NIO and NIO2 the default is 10000. For APR/native, the default is 8192.

服务器在任何给定时间将接受和处理的最大连接数。达到此数目后,服务器将接受但不处理另一个连接。在处理的连接数降至maxConnections以下之前,该附加连接将被阻止,此时服务器将开始重新开始接受并处理新的连接。请注意,一旦达到限制,操作系统仍然可以基于acceptCount设置接受连接。默认值因连接器类型而异。对于BIO,除非使用执行程序,否则默认值为maxThreads的值,在这种情况下,默认值为执行程序的maxThreads的值。对于NIO和NIO2,默认值为10000。对于APR / native,默认值为8192。

Note that for APR/native on Windows, the configured value will be reduced to the highest multiple of 1024 that is less than or equal to maxConnections. This is done for performance reasons.
If set to a value of -1, the maxConnections feature is disabled and connections are not counted.

请注意,对于Windows上的APR /本机,配置的值将减小为1024的最大倍,该最大值小于或等于maxConnections。这样做是出于性能原因。
如果设置为-1,将禁用maxConnections功能,并且不计算连接数。

(3)maxThreads:最大工作线程数,也就是用来处理request请求的,默认是200,如果自己配了executor,并且和Connector有关联了,则之前默认的200就会被忽略,取决于CPU的配置。监控中就可以看到所有的工作线程是什么状态,通过监控就能知道开启多少个线程合适

The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as -1 to make clear that it is not used.

此连接器将创建的请求处理线程的最大数量,因此,它确定可以处理的同时请求的最大数量。如果未指定,则此属性设置为200。如果执行程序与此连接器相关联,则此属性将被忽略,因为连接器将使用执行程序而不是内部线程池执行任务。请注意,如果配置了执行程序,则将正确记录为此属性设置的任何值,但会将其报告为(例如,通过JMX)为-1,以表明未使用该值。

(4)minSpareThreads

最小空闲线程数

The minimum number of threads always kept running. This includes both active and idle threads. If not specified, the default of 10 is used. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as -1 to make clear that it is not used.

始终保持运行状态的最小线程数。这包括活动线程和空闲线程。如果未指定,则使用默认值10。如果执行程序与此连接器相关联,则此属性将被忽略,因为连接器将使用执行程序而不是内部线程池执行任务。请注意,如果配置了执行程序,则将正确记录为此属性设置的任何值,但会将其报告为(例如,通过JMX)为-1,以表明未使用该值。

可以实践一下,Connector配合自定义的线程池

<Connector executor="tomcatThreadPool"
	port="8080" protocol="HTTP/1.1"
	connectionTimeout="20000"
	redirectPort="8443" />

<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
	maxThreads="150" minSpareThreads="4"/>

其实这块最好的方式是结合BIO来看,因为BIO是一个request对应一个线程

值太低,并发请求多了之后,多余的则进入等待状态。

值太高,启动Tomcat将花费更多的时间。

比如可以改成250。

  • enableLookups : 设置为false

  • 删掉AJP的Connector

Host标签

autoDeploy :Tomcat运行时,要用一个线程拿出来进行检查,生产环境之下一定要改成false

This flag value indicates if Tomcat should check periodically for new or updated web applications while Tomcat is running. If true, Tomcat periodically checks the appBase and xmlBase directories and deploys any new web applications or context XML descriptors found. Updated web applications or context XML descriptors will trigger a reload of the web application. The flag’s value defaults to true. See Automatic Application Deployment for more information.

此标志值指示在Tomcat运行时,Tomcat是否应定期检查新的或更新的Web应用程序。如果为true,则Tomcat会定期检查appBase和xmlBase目录,并部署找到的任何新的Web应用程序或上下文XML描述符。更新的Web应用程序或上下文XML描述符将触发Web应用程序的重新加载。标志的值默认为true。有关更多信息,请参见自动应用程序部署。

Context标签

reloadable:false

reloadable:如果这个属性设为true,tomcat服务器在运行状态下会监视在WEB-INF/classes和WEB-INF/lib目录下class文件的改动,如果监测到有class文件被更新的,服务器会自动重新加载Web应用。

在开发阶段将reloadable属性设为true,有助于调试servlet和其它的class文件,但这样用加重服务器运行负荷,建议在Web应用的发存阶段将reloadable设为false。

Set to true if you want Catalina to monitor classes in /WEB-INF/classes/ and /WEB-INF/lib for changes, and automatically reload the web application if a change is detected. This feature is very useful during application development, but it requires significant runtime overhead and is not recommended for use on deployed production applications. That’s why the default setting for this attribute is false. You can use the Manager web application, however, to trigger reloads of deployed applications on demand.

如果您希望Catalina监视/ WEB-INF / classes /和/ WEB-INF / lib中的类以进行更改,则设置为true,并在检测到更改时自动重新加载Web应用程序。此功能在应用程序开发期间非常有用,但是它需要大量的运行时开销,因此不建议在已部署的生产应用程序上使用。这就是为什么此属性的默认设置为false的原因。但是,您可以使用Manager Web应用程序来触发按需重新加载已部署的应用程序。

JVM优化

JVM优化过渡

为什么会有JVM这块的优化?因为tomcat是java语言写的,那么对于jvm这块的优化在tomcat中就是适用的。比如修改一些参数,调整内存大小,选择合适的垃圾回收算法等等。

现在有个问题,修改JVM参数在哪里修改会对tomcat生效?还是在bin文件夹之下,有一个catalina.sh,找到JAVA_OPTS即可,当然不建议对此文件进行直接修改,一般是在外面新建一个文件,然后引入进来,我们就不这样做了,直接修改 bin/catalina.sh 文件。

运行时数据区和内存结构

既然要对内存的大小做调整设置,你得认知一下jvm这块的内容,这里之前James老师的公开课和VIP课中讲过,当然你没听过也没关系,可以回头听一下,而且后面大白老师也会和大家讲这块的内容。

结论 :接下来我也站在我的角度和大家做一个简单的分享,这有利于接下来我们tomcat的jvm调优。

运行时数据区是一个规范,内存结构是一个实际的实现.性能优化专题 02 - Tomcat性能优化之调优_tomcat_02

  • 运行时数据区

官网 :官网

(1)程序计数器The pc Register

JVM支持多线程同时执行,每一个线程都有自己的pc register,线程正在执行的方法叫做当前方法。如果是java代码,pc register中存放的就是当前正在执行的指令的地址,如果是c代码,则为空。

(2)Java虚拟机栈Java Virtual Machine Stacks

Java虚拟机栈是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

(3)堆Heap

Java堆是Java虚拟机所管理的内存中最大的一块。对是被所有线程共享的一块内存区域,在虚拟机启动时创建。次内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java对可以处于物理上不连续的内存空间中,只要逻辑上市连续的即可。

(4)方法区Method Area

方法区和Java堆一样,是各个线程共享的内存区域,也是在虚拟机启动时创建。它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的是与Java堆区分开来。

jdk1.8中就是metaspace,jdk1.6或者1.7中就是perm space

运行时常量池Runtime Constant Pool是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译时期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

(5)本地方法栈Native Method Stacks

本地方法栈和虚拟机栈锁发挥的作用是非常相似的,它们之间的区别不过是虚拟机栈执行Java方法服务,而本地方法栈则为虚拟机使用到的native方法服务。

  • 内存结构

上面对运行时数据区描述了很多,其实重点存储数据的是堆和方法区(非堆),所以我们内存结构的设计也是着重从这两方面展开的。

一块是非堆区,一块是堆区。堆区分为两大块,一个是Old区,一个是Young区。Young区分为两大块,一个是Survival区(S0+S1),一块是Eden区。 Eden:S0:S1=8:1:1S0和S1一样大,也可以叫From和To。在同一个时间点上,S0和S1只能有一个区有数据,另外一个是空的。

垃圾回收算法

Q: 为什么需要学习垃圾回收算法?

A: Java是做自动内存管理的,自动垃圾回收。

Q: 如何确定一个对象是否是垃圾,从而确定是否需要回收?

(1)引用计数

对于某个对象而言,只要应用程序中持有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,它就是垃圾。

弊端 :AB相互持有引用,导致永远不能被回收。

(2)枚举根节点做可达性分析

能作为根节点的 :类加载器、Thread、虚拟机栈的本地变量表、static成员、常量引用、本地方法栈的变量等。

常量的垃圾回收算法

能够确定一个对象是垃圾之后,怎么回收?得要有对应的算法

(1)标记清除

先标记所有需要回收的对象,然后统一回收。

缺点 :效率不高,标记和清除两个过程的效率都不高,容易产生碎片,碎片太多会导致提前GC。

(2)复制

将内存按容量划分为大小相等的两块(S0和S1),每次只使用其中一块。

当这块使用完了,就讲还存活的对象复制到另一块上,然后再把已经使用过的内存空间一次性清除掉【Young区此采用的是复制算法】

优缺点 :实现简单,运行高效,但是空间利用率低。

(3)标记整理

标记需要回收的对象,然后让所有存活的对象移动到另外一端,直接清理掉端边界意外的内存。

JVM中采用的是分代垃圾回收,换句话说,堆中的Old区和Young区采用的垃圾回收算法是不一样的。

  • Young区:复制算法

  • Old区:标记清除或标记整理

对象在被分配之后,可能声明周期比较短,Young区复制效率比较高。

Old区对象存活时间比较长,复制来复制去没必要,不如做个标记。

对象分配方式

  • 对象优先分配在Eden区

  • 大对象直接进入老年代,多大的对象称为大对象?可以通过JVM参数指定 -XX:PretenureSizeThreshold 长期存活对象进入老年代

垃圾收集器
  • 串行收集器Serial:Serial、Serial Old

一个线程跑,停止,启动垃圾回收线程,回收完成,继续执行刚才暂停的线程。适用于内存比较小的嵌入式设备中。

  • 并行收集器Parallel:Parallel Scavenge、Parallel Old,吞吐量优先

多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态,适合科学计算、后台处理等弱交互场景

  • 并发收集器Concurrent:CMS、G1,停顿时间优先

用户线程和垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾收集线程在执行的时候不会停顿用户程序的运行。适合于对相应时间有要求的场景,比如Web。

  • 吞吐量和停顿时间解释
  1. 吞吐量:花在垃圾收集的时间和花在应用程序时间的占比
  2. 停顿时间:垃圾收集器做垃圾回收终端应用执行的时间

小结: 评价一个垃圾回收器的好坏,其实调优的时候就是在观察者两个变量

  • 开启垃圾收集器

性能优化专题 02 - Tomcat性能优化之调优_tomcat_03

  • Young区和Old区适用的垃圾回收器
    jdk1.8中比较推荐使用G1垃圾回收器,性能比较高。

  • 常用的G1 Collector

jdk1.7开始使用,jdk1.8非常成熟,jdk1.9默认的垃圾收集器

要求 :>=6GB,停顿时间小于0.5秒,适用于新老生代

是否需要用G1的判断依据

(1)50%以上的堆被存活对象占用

(2)对象分配和晋升的速度变化非常大

(3)垃圾回收时间比较长

  • 如何选择合适的垃圾回收器

(1)优先调整堆的大小让服务器自己来选择

(2)如果内存小于100M,使用串行收集器

(3)如果是单核,并且没有停顿时间要求,使用串行或JVM自己选

(4)如果允许停顿时间超过1秒,选择并行或JVM自己选

(5)如果响应时间最重要,并且不能超过1秒,使用并发收集器

两款GC日志分析工具

评价一个垃圾回收器的好坏:吞吐量和停顿时间

要想分析,得把GC日志打印出来才行,可以在tomcat中catalina.sh JAVA_OPTS配置相关参数

XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps - Xloggc:$CATALINA_HOME/logs/gc.log

然后重启tomcat,下载下来看看内容

上述日志直接看比较费力,不妨借助工具,把gc.log下载到本地,然后上传到gceasy.io可以比较不同的垃圾回收器的日志情况

  • GCViewer
内存模型和GC联系

Minor GC:新生代

Major GC:老年代

Full GC:新生代+老年代

一个对象的一辈子-概要

一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

一个对象的一辈子-理论

在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。

Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。

一个对象的一辈子-案例

我是一个普通的Java对象,我出生在Eden区,在Eden区我还看到和我长的很像的小兄弟,我们在Eden区中玩了挺长时间。有一天Eden区中的人实在是太多了,我就被迫去了Survivor区的“From”区,自从去了Survivor区,我就开始漂了,有时候在Survivor的“From”区,有时候在Survivor的“To”区,居无定所。直到我18岁的时候,爸爸说我成人了,该去社会上闯闯了。于是我就去了年老代那边,年老代里,人很多,并且年龄都挺大的,我在这里也认识了很多人。在年老代里,我生活了20年(每次GC加一岁),然后被回收。

为什么会有Survival区

如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC(因为 Major GC一般伴随着Minor GC,也可以看做触发了Full GC)。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多。你也许会问,执行时间长有什么坏处?频发的Full GC消耗的时间是非常可观的,这一点会影响大型程序的执行和响应速度,更不要说某些连接会因为超时发生连接错误了。

增加老年代空间 更多存活对象才能填满老年代。降低Full GC频率 随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长

减少老年代空间 Full GC所需时间减少 老年代很快被存活对象填满,Full GC频率增加

Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次 Minor GC还能在新生代中存活的对象,才会被送到老年代。

为什么会有两个Survival区

设置两个Survivor区最大的好处就是解决了碎片化,下面我们来分析一下。

为什么一个Survivor区不行?第一部分中,我们知道了必须设置Survivor区。假设现在只有一个survivor区,我们来模拟一下流程:

刚刚新建的对象在Eden中,一旦Eden满了,触发一次Minor GC,Eden中的存活对象就会被移动到Survivor区。这样继续循环下去,下一次Eden满了的时候,问题来了,此时进行Minor GC,Eden和Survivor各有一些存活对象,如果此时把Eden区的存活对象硬放到Survivor区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化。永远有一个survivor space是空的,另一个非空的survivor space无碎片。

JVM常见参数

无论是设置内存大小还是选用不同的GC Collector都可以通过JVM参数的形式,所以我们有必要了解一下JVM参数相关的内容。

  • 标准参数

-help

-server -client

-version -showversion

-cp -classpath

  • X参数

非标准参数,也就是在jvm各个版本中可能会变

-Xint 解释执行

-Xcomp 第一次使用就编译成本地代码

-Xmixed 混合模式,JVM自己来决定是否编译成本地代码

  • XX参数

平时用的最多的参数类型

非标准化参数,相对不稳定,主要用于JVM调优和Debug

a.Boolean类型

格式:-XX:[±] 表示启用或者禁用name属性

比如:-XX:+UseConcMarkSweepGC 表示启用CMS类型的垃圾回收器

-XX:+UseG1GC 表示启用CMS类型的垃圾回收器

b.非Boolean类型

格式:-XX=表示name属性的值是value

比如:-XX:MaxGCPauseMillis=500

  • 特殊参数

-Xmx -Xms 设置最大最小内存的

不是X参数,而是XX参数

-Xms等价于-XX:InitialHeapSize

-Xmx等价于-XX:MaxHeapSize

-Xss等价于-XX:ThreadStackSize

  • 查看JVM运行时参数

得先知道当前的值是什么,然后才能设置调优

=表示默认值

:=表示被用户或JVM修改后的值

查看PID: jps -l,专门用来查看java进程的

jinfo 查看已经运行的jvm里面的参数值

jinfo -flag MaxHeapSize PID 查看最大内存

jinfo -flag UseG1GC PID 查看垃圾回收器

jinfo -flags PID 查看曾经赋过值的一些参数

  • jstat查看JVM统计信息

(1)类装载

jstat -class PID 1000 10

PID进程ID,1000每个一秒钟,10输出10次

(2)垃圾收集

jstat -gc PID 1000 10

性能优化专题 02 - Tomcat性能优化之调优_jvm_04

内存溢出和优化

内存不够用主要分为两个方面:堆和非堆

所以这时候就要去手动设置堆或者非堆的大小,然后程序中不停使用相对应的区域,等待内存溢出。

关键是内存溢出之后,怎么得到溢出信息进行分析,有两种做法

  • 参数设置自动

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=./

  • jmap手动

查看当前进程id PID

jmap -dump:format=b,file=heap.hprof PID

jmap -heap PID 打印出堆内存相关的信息

当内存信息打印出来之后,发现看不懂,怎么办呢?得要有工具帮助我们看这块的信息,比如MAT

小结:这块可以适当增加内存的大小,这样防止内存溢出,减少垃圾回收的频率

GC调优

(1)查看目前JVM使用的垃圾回收器

[root@pretty ~]# jinfo -flag UseParallelGC 6925

-XX:+UseParallelGC —>发现使用了ParallelGC

[root@pretty ~]# jinfo -flag UseG1GC 6925

-XX:-UseG1GC —>发现没有使用G1GC

(2)将垃圾回收器修改为G1

-XX:+UseG1GC

[root@pretty ~]# jinfo -flag UseG1GC 7158

-XX:+UseG1GC

(3)打印出日志详情信息和日志输出目录文件

PrintGCDetails:打印日志详情信息

PrintGCTimeStamps:输出GC的时间戳(以基准时间的形式)

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps - Xloggc:$CATALINA_HOME/logs/g1gc.log

(4)将日志用工具来分析,看相应的参数

JVM调优小结

内存大小设置——>dump出日志 使用MAT工具分析

垃圾收集器选择———>dump出GC日志 gceasy或者GCViewer

其他优化

我们说既然Tomcat是Web服务器,在整个互联网架构模式中,往前推就是处理静态资源的过程。

  • Nginx 优化 :开启浏览器缓存,nginx静态资源部署

往后推,在Web服务器之后即是数据库层面。

  • 数据库优化 : 减少对数据库访问等待的时间,可以从数据库的层面进行优化,或者加缓存等等各种方案。
  • Connector:配置压缩属性compression=“500”,文件大于500bytes才会压缩

嵌入式Tomcat主类寻找

如果是在Spring Boot (Maven项目)里引入了Tomcat的依赖,我们该如何去依据之前的源码逻辑进行分析?

<dependency>
	<groupId>org.apache.tomcat.maven</groupId>
	<artifactId>tomcat7-maven-plugin</artifactId>
	<version>2.0</version>
</dependency>

寻找:Tomcat7RunnerCli类,寻找main函数

org.springframework.boot.context.embedded.tomcat.EmbeddedServletContainerCustomizer
// 相当于 new TomcatContextCustomizer(){}
factory.addContextCustomizers((context) -> { // Lambda 
	if (context instanceof StandardContext) {
		StandardContext standardContext = (StandardContext) context; 
 		standardContext.setDefaultWebXml(); // 设置
	}
});

写在最后

Tomcat 8.0.11 源码地址:

https://github.com/harrypottry/apache-tomcat-8.0.11-src

更多架构知识,欢迎关注本套系列文章Java架构师成长之路