Usamos cookies essenciais para login e armazenamento local para suas preferências. Para métricas de uso, usamos o Google Analytics 4 — você pode recusar sem perder nada do produto. Saber mais

ThreadMineThreadMine
Voltar

Guia técnico

Thread dump no Tomcat: como capturar e analisar

Onde o PID do Tomcat se esconde, por que o kill -3 cai no catalina.out, o que os pools http-nio exec significam e como flagrar um maxThreads esgotado antes dos seus usuários.

Testar grátis

Travamento de Tomcat tem assinatura: a porta continua aceitando conexões, o processo está vivo, a CPU parece calma — e cada request fica lá parada até o cliente desistir. A ferramenta que transforma esse mistério em diagnóstico é um thread dump. Mas o Tomcat adiciona as próprias pegadinhas ao procedimento padrão: o processo da JVM se chama Bootstrap, não tomcat; o clássico kill -3 escreve o dump onde ninguém espera; e o próprio dump é dominado por pools de connector com nomes como http-nio-8080-exec-42 que você precisa saber ler. Este guia percorre o caminho específico do Tomcat de ponta a ponta.

Encontrando o PID do Tomcat

A JVM do Tomcat roda a classe org.apache.catalina.startup.Bootstrap, então um ps aux | grep tomcat ingênuo pode não achá-lo (ou achar o próprio grep). As duas opções confiáveis:

# jps vem com o JDK e lista as JVMs pela main class
$ jps -l
41317 org.apache.catalina.startup.Bootstrap
41551 jdk.jcmd/sun.tools.jps.Jps

# ou case pelas system properties catalina que todo Tomcat carrega
$ pgrep -f catalina
41317

Se várias instâncias de Tomcat rodam no mesmo host, distinga-as pelo -Dcatalina.base na linha de comando completa (ps -fp 41317) — cada instância aponta para o próprio diretório base.

Capturando com jstack ou jcmd

Com o PID em mãos, a captura é o procedimento padrão do JDK — com um detalhe específico do Tomcat: o mecanismo de attach exige rodar como o mesmo usuário da JVM alvo. Tomcat de produção costuma rodar como usuário de serviço (tomcat), então prefixe de acordo:

# como o usuario tomcat (attach exige o mesmo usuario da JVM)
$ sudo -u tomcat jstack -l 41317 > /tmp/tomcat-dump-1.txt

# jcmd e o equivalente moderno
$ sudo -u tomcat jcmd 41317 Thread.print -l > /tmp/tomcat-dump-1.txt

Tire três dumps com alguns segundos de intervalo — diagnóstico de travamento é sobre o que não se move entre os snapshots. Se você precisa do cardápio completo de métodos de captura (serviço Windows, Spring Boot Actuator, hosts sem JDK), o guia geral de como capturar um thread dump cobre cada um; tudo de lá vale para o Tomcat também.

kill -3: o dump cai no catalina.out

O truque mais antigo continua funcionando em qualquer lugar, inclusive em hosts sem JDK: kill -3 (SIGQUIT) pede à JVM que imprima um thread dump. A própria JVM trata o sinal — o Tomcat não para, não reinicia e não derruba nenhuma request. A pegadinha é onde o dump vai parar: no stdout da JVM, que os scripts de inicialização do Tomcat redirecionam para CATALINA_BASE/logs/catalina.out.

$ kill -3 41317          # nada aparece no seu terminal - e o esperado
$ tail -n 400 $CATALINA_BASE/logs/catalina.out
Full thread dump OpenJDK 64-Bit Server VM (21.0.3+7 mixed mode):
"http-nio-8080-exec-1" #52 daemon prio=5 ...
  • Nada apareceu no catalina.out? Em instalações systemd que não usam o redirect do catalina.sh, o stdout vai para o journal: journalctl -u tomcat -n 400.
  • Repita. Cada kill -3 anexa mais um dump completo ao mesmo arquivo — mande o sinal duas ou três vezes, com segundos de intervalo, e você tem a série de snapshots num lugar só.
  • Cuidado com a rotação. O catalina.out costuma ser rotacionado ou truncado pelo logrotate; extraia o trecho do dump antes que ele suma.

Tomcat em containers

Em Docker/Kubernetes existem os mesmos dois caminhos, escolhidos pelo que a imagem traz:

# imagem JDK (jcmd disponivel): o Tomcat costuma ser o PID 1
$ docker exec meu-tomcat jcmd 1 Thread.print -l > tomcat-dump.txt

# imagem so-JRE (sem jcmd/jstack): o handler de sinais da JVM segue valendo
$ docker exec meu-tomcat kill -3 1
$ docker logs --tail 400 meu-tomcat   # stdout do PID 1 = docker logs

A rota do kill -3 é o motivo de uma imagem só-JRE não ser um beco sem saída: o dump vai para o stdout do container, que é exatamente o que o docker logs (ou kubectl logs) mostra. No Kubernetes, kubectl exec <pod> -- kill -3 1 seguido de kubectl logs <pod> é o equivalente.

Lendo o dump: os pools de threads do Tomcat

Um dump do Tomcat é dominado por algumas famílias de threads, e os nomes codificam connector, porta e papel:

  • http-nio-8080-exec-N — o pool de workers do connector HTTP na porta 8080. Essas threads rodam seus servlets, filters e controllers; é nelas que os problemas da sua aplicação aparecem. A contagem é limitada por maxThreads (default 200 por connector). Um connector TLS aparece como https-jsse-nio-8443-exec-N, AJP como ajp-nio-8009-exec-N, e se você configurou um <Executor> compartilhado, o nome é o do executor (tipicamente catalina-exec-N).
  • http-nio-8080-Acceptor — aceita conexões TCP novas e as entrega ao poller. Uma por connector; quase sempre RUNNABLE em accept. Benigna.
  • http-nio-8080-Poller — o loop de selector NIO vigiando sockets com dados para ler. RUNNABLE em select é o estado ocioso dela. Benigna.
  • catalina-utility-N — a manutenção interna do Tomcat (expiração de sessão, varredura de deploy). Normalmente WAITING; raramente o problema.

A regra de leitura: ignore o encanamento (Acceptor, Poller, utility) e leia o pool exec. Um worker exec ocioso estaciona em WAITING na fila do pool — saudável. Um ocupado está RUNNABLE dentro do código da sua aplicação ou bloqueado em algo. O que os estados significam com precisão — e por que RUNNABLE num socket read não é "CPU ocupada" — está no guia de campo dos estados de thread.

Sintomas clássicos: maxThreads esgotado, requests enfileirando

Todas as threads exec ocupadas no mesmo downstream

"http-nio-8080-exec-17" #71 daemon prio=5 ... runnable
   java.lang.Thread.State: RUNNABLE
	at sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:191)
	at java.net.Socket$SocketInputStream.read(Socket.java:1099)
	at com.mysql.cj.protocol.ReadAheadInputStream.fill(...)
	at com.example.OrderRepository.findPending(OrderRepository.java:44)

Duzentas threads exec com essa cara significam o pool inteiro estacionado dentro de socket reads contra o banco (ou outro downstream). A JVM reporta RUNNABLE porque a espera é nativa, mas ninguém está trabalhando — e com o pool no maxThreads, requests novas não conseguem worker. Elas esperam na fila de accept do connector (limitada por acceptCount, default 100) até algo liberar ou o cliente estourar o timeout. Visto de fora: "o Tomcat está no ar mas nada responde". Aumentar o maxThreads não resolve — só adiciona mais threads para estacionar no mesmo downstream lento.

Threads exec BLOCKED num mesmo monitor

"http-nio-8080-exec-9" #63 daemon prio=5 ... waiting for monitor entry
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.example.LegacyCache.get(LegacyCache.java:71)
	- waiting to lock <0x000000076ab61c58> (a com.example.LegacyCache)

Dezenas de threads exec em waiting to lock no mesmo endereço é um ponto quente de contenção: uma seção synchronized serializando o pool inteiro do connector. Ache a thread que reporta locked <aquele endereço> — o que quer que ela esteja fazendo (muitas vezes IO lento dentro da seção crítica) é o gargalo de verdade. Se quem detém o lock está, ele próprio, esperando por uma das threads bloqueadas, você tem um deadlock, e a JVM o imprime explicitamente no fim do dump.

A linha de base saudável

Para contraste: num servidor tranquilo, a maioria das threads exec fica em WAITING na fila do pool (TaskQueue / parkNanos), um punhado está RUNNABLE de fato servindo requests, e a contagem do pool fica bem abaixo do maxThreads. Essa foto é a cara do "nada errado" — conhecê-la é o que permite reconhecer os dois padrões acima de relance.

Análise automática de um dump do Tomcat

Tudo acima dá para fazer na mão — às 3h da manhã, com 200 threads exec e o canal do incidente apitando, também é fácil errar. Essa leitura é o que o ThreadMine automatiza: cole o trecho do catalina.out ou a saída do jstack no analisador de thread dump online gratuito e ele parseia os pools pelo nome, separa workers ociosos dos ocupados e roda um conjunto de detectores sobre o resultado — pool exhaustion (contagem exec cravada no teto, com a evidência), pontos quentes de contenção resolvidos até quem detém o lock, deadlocks e thread leaks entre snapshots.

A saída são achados ranqueados com severidade e um health score A–F em vez de uma parede de stack traces — a diferença entre "aqui estão suas 200 threads" e "o pool http-nio-8080 está esgotado e 187 dos workers estão estacionados dentro da mesma chamada JDBC". Envie dois ou três dumps do mesmo incidente e a timeline mostra quando o pool começou a saturar, não só que saturou.

Conclusão

Thread dump no Tomcat é o procedimento padrão da JVM mais conhecimento local: o PID se esconde atrás do Bootstrap, o kill -3 cai no catalina.out (ou no docker logs), e a história quase sempre está no pool http-nio-*-exec — esgotado contra um downstream lento, serializado num lock quente, ou saudável e ocioso. Capture três dumps com segundos de intervalo, leia o pool exec contra a linha de base saudável, e deixe uma ferramenta fazer o agrupamento quando a contagem de threads estiver nas centenas.

Perguntas frequentes

O kill -3 para ou reinicia o Tomcat?

Não. O SIGQUIT é interceptado pelo handler de sinais da JVM, que imprime o thread dump no stdout e segue rodando. O Tomcat não para, não reinicia e não derruba nenhuma request — o kill -3 é seguro contra uma instância de produção, e é exatamente por isso que ele é o método clássico de captura no Tomcat.

Para onde vai o thread dump quando uso kill -3 no Tomcat?

Para o stdout do Tomcat, que os scripts de inicialização padrão redirecionam para CATALINA_BASE/logs/catalina.out — não para um arquivo separado nem para o seu terminal. Num serviço systemd sem esse redirect, olhe journalctl -u tomcat; num container, ele aparece no docker logs.

Quantos thread dumps devo tirar para diagnosticar um travamento do Tomcat?

Pelo menos três, com alguns segundos de intervalo. Um dump mostra um instantâneo; a comparação mostra o movimento. Threads exec que trocam de pilha entre dumps estão lentas mas vivas; threads congeladas no mesmo frame e lock em todos os dumps são as realmente travadas, e um pool cravado no maxThreads nos três está esgotado, não momentaneamente ocupado.

O ThreadMine é gratuito para analisar um thread dump do Tomcat?

O primeiro dump não pede nem cadastro: cole ou envie o trecho do catalina.out ou a saída do jstack e receba o diagnóstico completo. O plano Free dá análises diárias com histórico; os planos pagos começam em US$ 9/mês (Pro).

Quer ver tudo isso aplicado ao seu dump?

Faça upload e tenha health score, problemas detectados e sugestões em segundos. Sem cadastro para o primeiro dump.