jdk8和Tomcat8.5是JDK和Tomcat的史诗级提升,直接把单车变跑车,所以如果你还是使用的jdk7和Tomcat8.5以后的版本,那可以考虑去线上换一下,但是不知道到时是架构师打死你还是你打死架构师
Tomcat的四种运行模式:
- BIO:同步阻塞模式,Tomcat7及之前默认的模式,性能最差,Tomcat8.5后已抛弃
- NIO:同步非阻塞模式,Tomcat8及之后默认的模式,性能高,现在的主流模式
- AIO:异步非阻塞模式,由于纯异步,性能最高
- APR:需要额外安装依赖库,从操作系统级别解决异步IO,大幅度的提高服务器的处理和响应性能,也是Tomcat运行高并发应用的首选模式,但是使用困难,依赖系统的底层网络包,团队没有C++大神和熟悉Linux底层的大神还是别拿出来秀了
主流是NIO模式,我们主要学习NIO模式的
配置优化
server status
通过配置可以看到Tomcat管理页server status,利用server status帮我们获取Tomcat的信息
我们需要修改一下配置文件,conf/tomcat-users.xml和webapps/manager/META-INF/context.xml。
找到conf/tomcat-users.xml
<role rolename="manager-gui"/>
<role rolename="admin-gui"/>
<role rolename="manager-script"/>
<role rolename="manager-jmx"/>
<role rolename="manager-status"/>
<user username="tzb" password="123456" roles="manager-gui,admin-gui,manager-script,manager-jmx,manager-status"/>
找到webapps/manager/META-INF/context.xml,将以下内容注释掉,我已经注释掉了,这样就能远程访问web manager了:禁用ajp协议
ajp协议是基于TCP协议的,Tomct使用ajp协议主要是为了连接http apache服务器的,http apache这个服务器已经被Ngnix完爆了,如果没有什么特殊癖好的话应该没人会用它了,但是这个配置会在程序中默认跑着十个线程,所以为了性能把这个没用的功能去除,如果不是2020年2月以后下载的Tomcat的话,是没有默认禁用的,所以需要手动禁掉
我的Tomcat是默认禁止的,我在配置中开启了ajp,我们看到这里跑了十个线程,跑了线程又用不到,浪费CUP的性能自定义Tomcat线程池
Tomcat需要给每一个请求创建线程,Tomcat默认的线程池最大线程数是200,核心线程数是10,如何并发高的场景,Tomcat就得不断的创建和销毁线程,所以就得自定义线程池提高核心线程数,这样可以帮助我们提高性能。但是如果不是是连接请求特别多的场景,最后别乱改,核心线程是需要占用内存的
先看看我们没修改前的状态,我使用Jvisualvm工具监控Tomcat的线程状态,我们看到这里默认是十个线程,由于我用的是Tomcat8.5所以运行模式默认是nio模式,线程的前缀是exec,记住这个名字后面用到的
修改server.xml文件,打开Executor的注释,我配置的参数是:最大线程数150,核心线程数15,线程池名称,每个线程的前缀。
为了区别,我把线程的前缀改为tzb-nb-,接着把默认的连接器配置注释掉,打开下面的连接器,让自定义的连接线程池生效
Tomcat线程模型
我们主要讲Tomcat8后的NIO,因为NIO才是主流,Tomcat的线程有很多,主线程叫做main是负责启动和关闭看监控工具可以看到Tomcat的主要线程
-
Acceptor
使用NIO的原生ServerSocketChannel的accept()方法监听客户的连接请求,但是这个ServerSocketChannel是设置阻塞的,所以当没有连接请求来时,线程会阻塞在accept方法上。ServerSocketChannel设置的TCP连接队列大小默认是100,TCP连接队列就是把当前连接信息存放到全连接队列中,队列中的连接信息等待ServerSocket.accpt()处理,Acceptor队列由acceptCount控制,但是Tomcat保持的通道数默认是10000,也就是ServerSocket.accpt()进去的通道Tomcat能保持10000个,由maxConnections控制。然后将ServerSocket.accpt()监听获取到的客户端通道SocketChanel设置为非阻塞,同时将SocketChanel和注册事件封装成一个PollerEvent对象扔进队列SynchronizedStack中,SynchronizedStack内部是数组,然后队列里的PollerEvent等待Poller线程来注册处理,Poller线程内部就是使用了NIO中的Selector多路复用器,PollerEvent对象主要是装载了SocketChanel和注册事件OP_REGISTER在Poller内部其实就是给Selector注册OP_READ,PollerEvent其实本身也是个线程。Poller线程是队列的消费者,Acceptor是生产者。这个Acceptor线程默认只有1个,但是可以在Tomcat中配置数量
public void bind() throws Exception {
if (!getUseInheritedChannel()) {
//Acceptor的ServerSocketChannel
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = (getAddress()!=null?
new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
serverSock.socket().bind(addr,getAcceptCount());//getAcceptCount()默认是100
} else {
//............................省略
//设置为阻塞的连接监听通道
serverSock.configureBlocking(true);
//............................省略
protected final void startAcceptorThreads() {
//Acceptor的线程个数,这个是可以通过配置文件配置的
int count = getAcceptorThreadCount();
//创建Acceptor线程
acceptors = new Acceptor[count];
for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
//启动Acceptor线程
t.start();
}
}
protected class Acceptor extends AbstractEndpoint.Acceptor {
//............................省略
@Override
public void run() {
//............................省略
while (running) {
//计数+1,达到最大值则等待,tomcat设定个最大连接数是10000,达到这个阈值后,就会拒绝连接请求,进行阻塞,这个是用AQS阻塞队列实现的
countUpOrAwaitConnection();
SocketChannel socket = null;
try {
//ServerSocketChannel开始监听连接
socket = serverSock.accept();
//............................省略
if (running && !paused) {
if (!setSocketOptions(socket)) {//setSocketOptions设置socket的方法
closeSocket(socket);
}
//............................省略
private void closeSocket(SocketChannel socket) {
//断开连接计数器将会减1
countDownConnection();
protected boolean setSocketOptions(SocketChannel socket) {
try {
//将监听到的SocketChannel 设置为非阻塞
socket.configureBlocking(false);
Socket sock = socket.socket();
//............................省略
//注册到ClientPoller的方法
getPoller0().register(channel);
//............................省略
}
public void register(final NioChannel socket) {
//............................省略
if ( r==null)
//封装成PollerEvent,与OP_REGISTER注册事件绑定
r = new PollerEvent(socket,ka,OP_REGISTER);
else r.reset(socket,ka,OP_REGISTER);
//添加到队列
addEvent(r);
}
public static class PollerEvent implements Runnable {
//............................省略
@Override
public void run() {
//注册事件,我们看到注册事件其实就是OP_READ读事件
if (interestOps == OP_REGISTER) {
try {
//将事件和通道注册到ClientPoller中的多路复用器中
socket.getIOChannel().register(
socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper);
-
ClientPoller
Tomcat8以后的NIO模式比Tomcat7以前强悍就是因为这个ClientPoller,ClientPoller是实现了Runnable,ClientPoller线程内部有一个jdk原生的Selector对象,也就是NIO的多路复用器。ClientPoller线程从队列SynchronizedQueue中取出PollerEvent逐一启动,启动后PollerEvent将SocketChanel和对应的事件逐一注册注册到ClientPoller的Selector,Selector找出SocketChanel中就绪的SelectionKey,将SelectionKey和事件类型交给工作线程Exec进行处理。整个过程就是典型的NIO实现。Tomcat默认启动Math.min(2,Runtime.getRuntime().availableProcessors())个ClientPoller线程,绝大部分的情况是2个ClientPoller,也就是两个Selector需要处理成千上万的事件,非常繁忙
//processKey源码大致逻辑,省略详细代码
protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
//............................省略
if ( close ) {
//............................省略
} else if ( sk.isValid() && attachment != null ) {
if (sk.isReadable() || sk.isWritable() ) {
// 写事件
if ( attachment.getSendfileData() != null ) {
processSendfile(sk,attachment, false);
} else {
if (sk.isReadable()) {
//读事件
if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
if (!closeSocket && sk.isWritable()) {
// 写事件
if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
closeSocket = true;
}
}
if (closeSocket) {
............................
// 关闭通道
}
..................................
}
-
NioBlockingSelector.BlockPoller
NioBlockingSelector主要处理socketChanel的写操作,也就是servlet的回写过程,NioBlockingSelector有两个对象BlockPoller和Selector叫做sharedSelector。BlockPoller中也有一个Selector对象,这个Selector主要是在blockingSelector.write时如果出现写失败就把socketChanel注册到BlockPoller的Selector不在占用ClientPoller的时间片,然后就利用CountDownLatch进行阻塞这是Tomcat默认的写操作过程,在BlockPoller的Selector注册的socketChanel以后的读写操作都是由这个Selector阻塞轮询。同时NioBlockingSelector是NioSelectorPool中一个成员对象,NioSelectorPool中有两个成员对象NioBlockingSelector和一个Selector叫做SHARED_SELECTOR,SHARED_SELECTOR主要在非默认的写过程中处理由ClientPoller轮询的socketChanel出现写失败时,为了节省ClientPoller宝贵的时间片socketChanel写事件会注册到SHARED_SELECTOR上,这个SHARED_SELECTOR是叫做辅Selector,值得一提的是NioSelectorPool的SHARED_SELECTOR、NioBlockingSelector的sharedSelector、BlockPoller的Selector都是同一对象,这个辅Selector轮询出现写事件失败的socketChanel,由辅Selector负责这些socketChanel以后的读写事件,这样减少线程间的切换,同时还可减轻主Selector的负担。
public class NioSelectorPool {
//SHARED 默认是true
protected static final boolean SHARED =
Boolean.parseBoolean(System.getProperty("org.apache.tomcat.util.net.NioSelectorShared", "true"));
protected NioBlockingSelector blockingSelector;
protected volatile Selector SHARED_SELECTOR;
//........................省略
public int write(ByteBuffer buf, NioChannel socket, Selector selector,
long writeTimeout, boolean block) throws IOException {
if ( SHARED && block ) {//block 和SHARED 是默认true,所以写事件默认走这里
//阻塞写操作
return blockingSelector.write(buf,socket,writeTimeout);
}
//........................省略
if ( keycount > 0 ) { //only write if we were registered for a write
//写操做,cnt -1为失败
cnt = socket.write(buf); //write the data
if (cnt > 0) {
continue;
}
//非阻塞写
if (cnt==0 && (!block)) break; //don't block
}
if ( selector != null ) {
//非阻塞写失败后重新注册到SHARED_SELECTOR,不占用ClientPoller的时间片
if (key==null) key = socket.getIOChannel().register(selector, SelectionKey.OP_WRITE);
else key.interestOps(SelectionKey.OP_WRITE);
} else if (writeTimeout<0) {
//SHARED_SELECTOR轮询
keycount = selector.select();
} else {
keycount = selector.select(writeTimeout);
}
}
//.....................................................................省略
ublic class NioBlockingSelector {
protected Selector sharedSelector;
protected BlockPoller poller;
//阻塞写
public int write(ByteBuffer buf, NioChannel socket, long writeTimeout)
throws IOException {
SelectionKey key = socket.getIOChannel().keyFor(socket.getPoller().getSelector());
try {
while ( (!timedout) && buf.hasRemaining()) {
if (keycount > 0) { //only write if we were registered for a write
int cnt = socket.write(buf); //write the data
written += cnt;
//写操做,cnt -1为失败
if (cnt > 0) {
time = System.currentTimeMillis(); //reset our timeout timer
continue; //we successfully wrote, try again without a selector
}
}
try {
if ( att.getWriteLatch()==null || att.getWriteLatch().getCount()==0) att.startWriteLatch(1);
//失败则重新注册写事件到BlockPoller 中
poller.add(att,SelectionKey.OP_WRITE,reference);
//利用CountDownLatch进行阻塞
if (writeTimeout < 0) {
att.awaitWriteLatch(Long.MAX_VALUE,TimeUnit.MILLISECONDS);
} else {
att.awaitWriteLatch(writeTimeout,TimeUnit.MILLISECONDS);
}
}
-
Exec
Exec是一个线程池很多博客叫它为Work,默认的线程名称是和server.xml中的一致,核心线程默认是10,最大线程数是200,可以通过我们上面的方法配置server.xml文件设置线程的数量,它主要是获取ClientPoller轮询出来的socketChanel后,做相应的逻辑处理:先进行三次握手,然后调用Http11Processor的service方法解析HTTP协议,将http协议解析成键值对放入request,最终调用CoyoteAdapter的service方法将request和response传入Tomcat的各种容器中执行servlet的业务逻辑。
//工作线程执行run处理socketChanel
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
//将Socket封装到这个对象,然后扔给工作线程执行
SocketProcessorBase<S> sc = processorCache.pop();
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
//工作线程,我们在配置文件中配置的Executor,不配默认是10个。最大数200
Executor executor = getExecutor();
//工作线程执行工作
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
//工作线程执行的东西,主要是三次握手后开始解析HTTP,然后关闭socket,省略部分代码
protected class SocketProcessor extends SocketProcessorBase<NioChannel> {
public SocketProcessor(SocketWrapperBase<NioChannel> socketWrapper, SocketEvent event) {
super(socketWrapper, event);
}
@Override
protected void doRun() {
if (handshake == 0) {//三次握手已完成
SocketState state = SocketState.OPEN;
// Process the request from this socket
if (event == null) {
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
} else {
state = getHandler().process(socketWrapper, event);
}
if (state == SocketState.CLOSED) {
close(socket, key);
}
..........................
//逻辑处理主要是调用了这给ConnectionHandler处理socket的数据包
protected static class ConnectionHandler<S> implements AbstractEndpoint.Handler<S> {
//省略 ...............................................
//处理socket数据包,将HTTP其解析成request 对象,传给Tomcat容器
@Override
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
//省略 ...............................................
do {
//省略 ...............................................
state = processor.process(wrapper, status);//真正继续数据包的方法,这个processor是AbstractProcessorLight 类型的,但是process方法主要是调用子类Http11Processor类实现的service进行处理
//省略 ...............................................
}
}
}
点进process方法,里面调用了service
public abstract class AbstractProcessorLight implements Processor {
@Override
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status)
//省略 ...............................................
throws IOException {
} else if (status == SocketEvent.OPEN_READ) {
//这里是调用Http11Processor 的service方法
state = service(socketWrapper);
}
//省略 ...............................................
Http11Processor 的service方法进行系列的解析各种
public class Http11Processor extends AbstractProcessor {
//省略 ...............................................
@Override
public SocketState service(SocketWrapperBase<?> socketWrapper)
throws IOException {
//省略 ...............................................
//经过一系列的解析和设置后,将http的各种信息都存放到request中调用CoyoteAdapter的service方法将request, response传递到Tomcat容器
getAdapter().service(request, response);
//省略 ...............................................
}
}
NioEndpoint组件
NioEndpoint是Tomcat的NIO关键的类,要理解tomcat的nio最主要就是对NioEndpoint的理解这个我是摘抄别人的,出处https://www.jianshu.com/p/76ff17bc6dea
,有空得自己去看看《Tomcat的内核设计剖析》
NioEndpoint它一共包含LimitLatch、Acceptor、Poller、SocketProcessor、Excutor5个部分。LimitLatch是连接控制器,它负责维护连接数的计算,nio模式下默认是10000,达到这个阈值后,就会拒绝连接请求。Acceptor负责接收连接,默认是1个线程来执行,将请求的事件注册到事件列表。有Poller来负责轮询,Poller线程数量是cpu的核数Math.min(2,Runtime.getRuntime().availableProcessors())。由Poller将就绪的事件生成SocketProcessor同时交给Excutor去执行。Excutor线程池的大小就是我们在Connector节点配置的maxThreads的值。在Excutor的线程中,会完成从socket中读取http request,解析成HttpServletRequest对象,分派到相应的servlet并完成逻辑,然后将response通过socket发回client。在从socket中读数据和往socket中写数据的过程,并没有像典型的非阻塞的NIO的那样,注册OP_READ或OP_WRITE事件到主Selector,而是直接通过socket完成读写,这时是阻塞完成的,但是在timeout控制上,使用了NIO的Selector机制,但是这个Selector并不是Poller线程维护的主Selector,而是BlockPoller线程中维护的Selector,称之为辅Selector
Tomcat文件传输
sendfile零拷贝:
在普通的输入输出流进行读写时,实际上是进行了多次上下文切换,应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存,在用户态对数据进行加工,然后再从用户态切换回内核态,将用户缓存数据拷贝到内核缓存中,然后读到socket中再到网卡
sendfile实质是linux系统中一项优化技术,用以发送文件和网络通信时,减少用户态空间与磁盘倒换数据,而直接在内核级做数据拷贝,在Tomcat中是可以使用sendfile对一些静态数据如:图片、文件等进行传输,这个sendfile是在Tomcat是默认打开的,可以有效的提高Tomcat的传输性能compression文件压缩:
compression可以对传输文件进行压缩有效解决文件传输的带宽问题,在Tomcat不支持图片压缩,因为要进行上层的用户态数据加工所以与sendfile互斥,Tomcat默认关闭,开启的话Tomcat会调用Filter利用jdk解压缩流进行解压缩
//源码中compression支持的压缩格式中没有图片
private String compressibleMimeType = "text/html,text/xml,text/plain,text/css," +
"text/javascript,application/javascript,application/json,application/xml";
Accept-Encoding 和Content-Encoding
Accept-Encoding 和Content-Encoding是HTTP中用来对采用哪种编码格式传输正文进行协定的一对头部字段。浏览器发送请求时,会在Request-heads携带Accept-Encoding会说明自己支持的编码列表,服务端在接收到请求后,从中挑选出一种用来对响应信息进行编码,并通过Response-heads携带Content-Encoding来说明服务端选择的编码信息,浏览器在拿到响应正文后,依据Content-Encoding进行解压。绝大部分是gzip格式
Tomcat重要配置参数
server.xml
自定义线程池我们上面讲过了,现在有了一定的基础更好介绍了
Executor标签是专门配置Exec线程池的,专门用于处理多路复用器传递进来的socketChanel,我们上面介绍过了
<Executor name="tomcatThreadPool"
namePrefix="tzb-nb-"
maxThreads="1000"
minSpareThreads="30"
maxIdleTime="60000"
maxQueueSize="1000000"
className="org.apache.catalina.core.StandardThreadExecutor"/>
name:线程池名称,用于 Connector中指定。
namePrefix:所创建的每个线程的名称前缀。
maxThreads:池中最大线程数。
minSpareThreads:核心池线程数。
maxIdleTime:线程空闲时间,超过该时间后,非核心线程会被销毁,默认值为6000(1分钟),单位毫秒。
maxQueueSize:在被执行前最大线程排队数目,任务数超出会执行拒绝策略,默认为Int的最大值
className:线程池实现类,未指定情况下,默认实现类为org.apache.catalina.core.StandardThreadExecutor。
如果想使用自定义线程池首先需要实现 org.apache.catalina.Executor接口。
Connector用于配置Tomcat运行模式、Acceptor、CilentPoller、sendfile、是否开启文件压缩
<Connector port="8080"
protocol="HTTP/1.1"
maxThreads="1000"
minSpareThreads="100"
acceptorThreadCount="2"
acceptCount="1000"
maxConnections="1000"
connectionTimeout="20000"
maxHttpHeaderSize="8192"
compression="on"
compressionMinSize="2048"
redirectPort="8443"
URIEncoding="UTF-8" />
参数很多参考这里吧:https://blog.csdn.net/lijunwyf/article/details/84244209
我只列出几个有意思的
port:代表Tomcat监听端口,也就是网站的访问端口,默认为8080,可以根据需要改成其他。
protocol:运行模式,可选类型有四种,分别为BIO,NIO,AIO和APR。
protocol="org.apache.coyote.http11.Http11NioProtocol" //NIO
protocol="org.apache.coyote.http11.Http11Nio2Protocol" //AIO
protocol="org.apache.coyote.http11.Http11AprProtocol" //ARP
acceptCount:就是Acceptor线程工作时通道的长度也就是前面说的SynchronizedStack,当请求PollerEvent超出队列时请求就会被拒绝,默认是100。
一般是设置的跟 maxThreads一样或一半,此值设置的过大会导致排队的请求超时而未被处理。所以这个值应该是主要根据应用的访问峰值与平均值来权衡配置。
acceptorThreadCount:Acceptor线程的数量,默认是1,如果在多核CPU架构下,此值可以设置为2,官方不建议设定超过2个的值。
maxConnections:tomcat最大连接数也就是上面说的LimitLatch。NIO默认是10000,超出则Acceptor线程就被阻塞了,即不再队列中获取已经建立的连接。
但是它并不阻止新的连接的建立,新的连接的建立过程不是Acceptor控制的,Acceptor仅仅是从队列中获取新建立的连接。
所以当连接数已经超过maxConnections后,仍然是可以建立新的连接的,存放在上述acceptCount大小的队列中,这个队列里面的连接没有被Acceptor获取,
就处于连接建立了但是不被处理的状态。当连接数低于maxConnections之后,Acceptor线程就不再阻塞
connectionTimeout:当请求已经被接受,但未被处理,也就是等待中的超时时间。单位为毫秒,默认值为60000。
executor:就是把executor标签的内容引过来,不要它的话可以把executor的内容复制到这里
URIEncoding:URL编码字符集。
pollerThreadCount:ClientPoller的线程数,默认为2。官方不建议设置大于2的值,因为锁的竞争会导致性能下降,事实上一个线程也足够快速。
useSendfile:是否开启sendfile特性,默认为true。对于web应用而言,通常project中还会包含一定数量的静态资源,比如图片、CSS、js、html等,sendfile在一定程度上可以提高性能。
compression:是否开启压缩,这个属性与useSendfile互斥,useSendfile开启了,这个认关闭,compression是默认关闭的
compressionMinSize:如果compression="on",则启用此项。被压缩前数据的最小值,也就是超过这个值后才被压缩。如果没有指定,这个属性默认为“2048”(2K),单位为byte。
实战:
我们看到Acceptor线程线程是3个、Client线程是5个,exec线程是15个和我上图的配置一致
catalina.sh
使用JAVA_OPTS配置JVM参数
//例如:
JAVA_OPTS="-Dfile.encoding=UTF-8 -server -Xms2048m -Xmx2048m
-XX:NewSize=512m -XX:MaxNewSize=1024m
-XX:PermSize=256m -XX:MaxPermSize=256m -XX:MaxTenuringThreshold=10 -XX:NewRatio=2 -XX:+DisableExplicitGC"
下面是参考这里的https://blog.csdn.net/fly910905/article/details/78518599
一般说来,您应该使用物理内存的 80% 作为堆大小。说明:以上两个参数关系到tomcat承受的访问性能,但也要根据服务器实际内存情况设定。有人建议Xms和Xmx的值取成一样比较好,说是可以加快内存回收速度。
Xms和Xmx这两个值的大小一般根据需要进行配置。初始化堆的大小执行了虚拟机在启动时向系统申请的内存的大小。一般而言,这个参数不重要。但是有的应用程序在大负载的情况下会急剧地占用更多的内存,此时这个参数就是显得很重要,假如虚拟机启动时配置使用的内存比较小而在这种情况下有许多对象进行初始化,虚拟机就必须重复地增加内存来满足使用。由于这种原因,我们一般把-Xms和-Xmx设为相同大,而堆的最大值受限于系统使用的物理内存。一般使用数据量较大的应用程序会使用持久对象,内存使用有可能迅速地增长。当应用程序需要的内存超出堆的最大值时虚拟机就会提示内存溢出,并且导致应用服务崩溃。因此一般建议堆的最大值配置为可用内存的最大值的80%。
另外需要考虑的是Java提供的垃圾回收机制。虚拟机的堆大小决定了虚拟机花费在收集垃圾上的时间和频度。收集垃圾能够接受的速度和应用有关,应该通过分析实际的垃圾收集的时间和频率来调整。假如堆的大小很大,那么完全垃圾收集就会很慢,但是频度会降低。假如您把堆的大小和内存的需要一致,完全收集就很快,但是会更加频繁。调整堆大小的的目的是最小化垃圾收集的时间,以在特定的时间内最大化处理客户的请求。在基准测试的时候,为确保最好的性能,要把堆的大小设大,确保垃圾收集不在整个基准测试的过程中出现。
假如系统花费很多的时间收集垃圾,请减小堆大小。一次完全的垃圾收集应该不超过 3-5 秒。假如垃圾收集成为瓶颈,那么需要指定代的大小,检查垃圾收集的周详输出,研究 垃圾收集参数对性能的影响。一般说来,您应该使用物理内存的 80% 作为堆大小。当增加处理器时,记得增加内存,因为分配能够并行进行,而垃圾收集不是并行的。
Tomcat压力测试
使用jmeter可以进行对程序进行压力测试,根据用户的并发,调整Tomcat的线程数量
我使用一千个线程,一次并发一千,循环十次
主要看吞吐量,根据吞吐量慢慢调试Tomcat的线程设置