ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Tomcat启动报错BindException:1099端口被占?Windows端口排查与解决

Tomcat启动报错BindException:1099端口被占?Windows端口排查与解决

昨天群里有人甩了一张Tomcat启动日志截图,红色报错写着BindException: Address already in use: JVM_Bind:1099。他说三个常规手段全试了:netstat查端口,没有结果;重启机器再启动,依旧报错;把Tomcat端口改成8085,还是报1099。整个下午耗在上面,最后人麻了。这种场景我在Windows环境里见过很多次,1099这个端口名本身也不是Tomcat默认会用的端口。今天就把我自己的处理链路、排查思路和最终能落地的方案完整写一遍,给同样被这个端口恶心过的人省点时间。这篇内容主要面向在Windows上部署Tomcat的开发者、运维和做中间件维护的朋友,里面用到的命令在Windows Server 2012到2022、Win10/Win11上基本都通用。

1. 1099端口在Tomcat体系里的真实身份

1.1 什么时候Tomcat才会用到1099

Tomcat默认情况下只监听8080端口做HTTP服务,如果启用了AJP协议会再多一个8009端口。1099这个端口在默认配置里根本不存在,它其实是Java RMI Registry的约定端口。当我们需要对Tomcat做JMX远程监控时,JVM会启动一个RMI注册服务,默认绑定1099,然后通过这个注册服务暴露JMX相关的管理接口。

常见的启动参数长这样,通常写在setenv.bat或者启动脚本里:

set "JAVA_OPTS=%JAVA_OPTS% -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"

这里面的jmxremote.port=1099就是让RMI注册服务绑定1099的原因。为什么大家默认都喜欢用1099?因为它是RMI Registry的历史默认端口,早期很多Java监控教程和内部技术规范都直接照抄这个值,久而久之1099就成了Java中间件“事实上的默认端口”。很多团队的Tomcat里都埋着一行这样的参数,连自己都忘了是什么时候加进去的。

所以处理这个问题前,心里要有一本账:1099不是Tomcat的HTTP端口,它背后是JMX远程监控和RMI注册服务。如果报错的是1099,那多半是JMX参数里写了这个端口,而不是Tomcat的Connector配置有问题。

1.2 报错日志长什么样

1099端口被占时,Tomcat启动日志里会出现类似这样的内容:

SEVERE [main] org.apache.catalina.startup.Catalina.start Cannot start server java.net.BindException: Address already in use: JVM_Bind:1099

JVM_Bind:1099的意思非常直白:JVM尝试把本地端口1099绑定到自己名下,结果系统告诉它这个地址已经被别人“拿走”了。看到这个报错,第一反应肯定是查端口占用,然后杀掉占用进程,再重启Tomcat。

但有个容易忽略的现场情况:如果Tomcat是通过startup.bat启动的,Windows控制台可能一闪而过,很多人根本没看到堆栈日志。这时候需要直接去看logs/catalina.<日期>.log,例如catalina.2025-01-13.log,里面才有完整异常栈。排查端口占用之前,先确认报错文本到底是不是JVM_Bind:1099,别把别的端口报错也当成1099处理。

2. 常规排查为什么失效:重启无效、查不到进程

2.1 先走一遍标准动作

正常排查端口占用的流程大家都很熟,无非下面几步:

netstat -ano | findstr ":1099" tasklist | findstr "进程PID" taskkill /F /PID 12345

如果netstat能查到PID,后面两步就能顺藤摸瓜。但1099这个场景最诡异的地方在于:netstat返回结果空空如也,根本没有PID可以杀。既然查不到进程,重启Tomcat自然还是失败,因为端口依然被某个看不见的东西占着。重启整个系统也未必有用,这一点后面会详细解释。

我见过有人在这时候就开始怀疑Tomcat本身坏了,把安装包重新解压、JDK换版本、防火墙关了,折腾一圈还是原样。其实问题根本不在这层。

2.2 查不到进程的五个隐藏原因

netstat查不到东西,不等于端口真的空闲。根据我处理过的案例,查不到进程通常有五个原因:

  1. 端口落在Windows保留端口段里。系统启动时预留了一段TCP端口给Hyper-V、容器网络等组件使用,这段端口没有任何进程监听,但任何程序试图bind都会失败。netstat当然显示不出“进程”,因为没有进程。

  2. 占用进程以Windows服务方式运行。服务进程的权限和管理方式跟普通进程不一样,普通命令行下结束它,服务管理器会立刻把它拉起来,表现出“怎么杀都杀不死”的现象。

  3. 端口绑在IPv6栈上。Java在双栈环境下可能监听[::]:1099,用netstat -ano配合某些过滤写法时容易让人看漏。

  4. WMI服务异常或权限不足。PowerShell的Get-NetTCPConnection依赖WMI,如果系统WMI服务有问题,或当前命令行没有管理员权限,查询结果可能不完整,造成“端口明明被占却查不到”的假象。

  5. 安全软件或内核态组件做了端口拦截。有些安全软件会监听特定端口做流量审计,netstat未必把它们显示成普通TCP连接。

搞清楚这五个原因,基本就能解释“重启无效、查不到进程、改端口无效”这三大诡异现象。下面逐一拆解怎么处理。

3. 大头来了:Windows端口保留段(最常见根因)

3.1 什么是端口保留段

用大白话讲,Windows启动时会从TCP动态端口范围里划走一段“不对外出租”的端口,专门留给Hyper-V虚拟交换机、Windows容器网络、NAT等组件使用。这个过程不需要任何进程监听,就像酒店前台提前把某几间房设定为“工程保留房”,系统层面直接拒绝其他程序入住。

这个机制的关键影响有两个。第一,程序bind时,操作系统会直接返回“端口已被占用”,但netstat里根本没有对应监听记录。第二,这种保留是系统启动阶段就建立的,重启机器不会让它消失,因为重启后系统又会重新保留同样的区间。这就是“重启无效”的根本原因。

更坑的是,保留的是一整个区间,比如1094到1114。你辛辛苦苦把JMX端口从1099改成1105,结果1105还落在这个区间里,照样报错。这也是“改换端口无效”最常见的解释之一。注意,动态端口默认范围是49152到65535,但Hyper-V和winnat有时会从低段“借用”端口,导致1000开头的端口也被划进保留段。尤其Windows 10/11和Windows Server上装了Docker、启用了虚拟机平台后,这个问题特别容易碰到。

3.2 怎么确认是不是这个问题

一条命令就能看清楚系统到底保留了多少段端口:

netsh interface ipv4 show excludedportrange protocol=tcp

输出大概长这样:

协议 tcp 端口排除范围 开始端口 结束端口 ---------- -------- 1000 1390 1396 1430 5000 5090

其他内容可以先不看,重点判断1099是否落在某个“开始端口”和“结束端口”之间。只要1099落进任何一段,不管它看起来空闲与否,任何程序都绑定不上。上面示例里1000到1390直接覆盖了1099,这就是报错来源。

如果嫌netsh输出不好看,也可以配合下面几条命令一起看:

netstat -ano -p tcp | findstr "1099" netstat -ano -p tcpv6 | findstr "1099"

两边都查不到,而netsh里1099又确实落在某段区间内,那基本可以实锤是保留段问题。有些机器上还可以用PowerShell的Test-NetConnection -Port 1099验证,返回结果可能显示端口未监听却又连接失败,这种矛盾现象也指向保留段。

3.3 彻底解决办法

针对保留段,有两条路:让系统让出这段端口,或者让Tomcat绕开这段端口。我建议按下面的顺序操作。

先试重启winnat服务,管理员身份打开CMD,执行:

net stop winnat net start winnat

winnat服务负责Windows NAT和容器网络,重启它会重新计算端口保留区间,有一定概率把1099释放出来。注意,这个操作会短暂影响依赖该服务的网络功能,生产环境尽量放到维护窗口里做,别在业务高峰期顺手敲。

如果重启winnat没用,就调整TCP动态端口范围,把整个动态端口池挪到高位:

netsh int ipv4 set dynamic tcp start=40000 num=10000 netsh int ipv6 set dynamic tcp start=40000 num=10000

改完需要重启操作系统。这个方案的本质是把系统用于动态分配的端口全部推到40000以上,高位区间重新规划后,低位的1099就不再会被“借用”为保留段。根据我的实测,很多服务器上动态端口默认会动态占用大量低段端口,手动固定到高位后能极大降低跟业务端口冲突的概率。

如果不想动系统网络参数,也可以直接把JMX端口改成不落在任何保留段区间的高位端口,比如16001。改之前先重新执行一次netsh interface ipv4 show excludedportrange protocol=tcp,确认16001没被包进去。同样的逻辑也适用于其他端口的修改。

4. 其他真凶:残留进程、系统服务与IPv6

4.1 服务化部署的Tomcat进程

另一个容易造成“查不到进程、杀不掉”的情况是Tomcat被装成了Windows服务。用工具把Tomcat做成服务后,Java进程由Windows服务控制器统一管理,普通命令行结束它,服务管理器会在几秒内自动拉起新进程。你杀掉一次,它又原地复活,表现跟“重启无效”一模一样。

先查服务状态:

sc queryex tomcat9

如果服务名不是默认的tomcat9,可以直接看所有带Java的服务进程:

tasklist /fi "imagename eq java.exe"

查到PID后,还要看启动命令行确认是不是当前这个Tomcat实例:

wmic process where processid=1234 get commandline

wmic在某些新系统里可能不可用,可以用PowerShell替代:

Get-CimInstance Win32_Process -Filter "ProcessId = 1234" | Select-Object CommandLine

确认之后,正规做法是停服务、再确认端口释放、最后重新启动:

net stop tomcat9 netstat -ano | findstr "1099" net start tomcat9

如果打算彻底换端口重来,甚至可以考虑删除旧服务重新注册,但删除服务是一个比较重的操作,建议先确保Tomcat完全停止,注册表和服务配置不会被搞乱。

4.2 WMI异常或权限不足的典型表现

有相当一部分“查不到进程”的真实原因是查询工具本身没查全。比如普通用户权限下运行PowerShell的Get-NetTCPConnection,有些监听状态不会显示出来;再比如系统里WMI服务停了,很多基于WMI的管理命令直接失败。这些都是环境层面的坑,跟Tomcat本身没有关系。

我的习惯是,排查端口占用一律先把netstat -ano当作第一信息来源,它不依赖WMI,而且默认显示所有协议族的监听情况。用PowerShell辅助可以,但别只信PowerShell的结果。执行敏感命令时也尽量右键“以管理员身份运行”打开CMD,避免权限问题造成查询结果不完整。

4.3 IPv4/IPv6双栈绑定问题

Java在双栈系统上默认可能监听所有地址,包括IPv6通配地址::。如果Tomcat的JMX端口绑定在IPv6栈上,netstat输出里会显示类似[::]:1099这样的监听记录。有些人在cmd里过滤写法不严谨,比如用findstr ":1099 "带了个空格,就可能漏掉[::]:1099。

更隐蔽的情况是:IPv4的1099是空闲的,IPv6的1099被别的进程占了。Java尝试绑定所有网络栈时,只要其中一个栈冲突,整个启动就失败。排查的时候最好把IPv4和IPv6分开看:

netstat -ano -p tcp | findstr "1099" netstat -ano -p tcpv6 | findstr "1099"

如果确认是IPv6绑定问题,可以强制Java走IPv4栈,在JAVA_OPTS里加上:

-Djava.net.preferIPv4Stack=true

或者给JMX显式指定监听地址,比如只监听内网IP:

-Dcom.sun.management.jmxremote.host=127.0.0.1

双栈问题在Windows Server上不算罕见,尤其装了多个网卡或虚拟交换机的机器,值得专门留意。

5. 改端口无效?多半是没改全

5.1 检查Tomcat所有端口配置点

“改了Tomcat端口,怎么还报1099”这句话里藏着一个典型误区:Tomcat有好几个端口维度,HTTP端口、AJP端口、JMX端口各自独立。只改了HTTP Connector端口,JMX参数里还是1099,那1099必然继续被占用。改端口前先明确自己改的是哪个端口,以及目标端口究竟服务于哪个功能。

JMX相关端口最常出现在这几个位置:

  • setenv.bat或setenv.sh里的JAVA_OPTS
  • catalina.bat或catalina.sh里覆盖的JAVA_OPTS
  • Windows服务注册时写入的启动参数
  • 系统环境变量里的JAVA_OPTS

以Windows为例,搜索整个Tomcat目录下的1099:

findstr /s /i "1099" *.bat server.xml conf\*

Linux环境就换成grep:

grep -rn "1099" --include="*.bat" --include="*.sh" --include="*.xml" .

如果搜出来的结果里还有1099,那就是没改全。还有一种情况是配置文件改了,但Tomcat服务启动时读的是注册表里保存的旧参数,或者容器镜像里打包了旧配置。改完之后,重启服务前最好再用任务管理器确认Java进程的启动参数里已经没有1099。

5.2 被其他应用抢占的1099

1099不是Tomcat独占端口,很多Java中间件、监控agent、开发框架的自带RMI服务也喜欢用它。如果netstat能查到PID,但PID对应的不是你的Tomcat,那就是“别的程序占了地盘”,这时候要处理的是那个程序,而不是继续折腾Tomcat配置。

通过PID确认进程身份的方式前面已经写过:wmic或PowerShell查CommandLine,看看它属于哪个中间件。如果是监控agent或者另一个实例,要么停掉它,要么让它改用其他端口。如果查不到PID但bind又报错,优先怀疑保留段,别在这台机器上瞎改Tomcat配置,方向不对会浪费大量时间。

6. 一套完整的处理流程(从头到尾照着做)

6.1 快速诊断清单

下面这张表是我排查同类问题的顺序,实践下来命中率很高:

现象优先怀疑方向检查命令
netstat查不到进程,bind报1099Windows保留端口段netsh interface ipv4 show excludedportrange protocol=tcp
查到java进程,结束又自动恢复Tomcat被注册为Windows服务sc queryex tomcat9
改了HTTP端口仍然报1099JMX端口没改全findstr /s /i "1099" *.bat server.xml
netstat只有IPv6记录,IPv4没有IPv6双栈绑定netstat -ano -p tcpv6
端口被其他java进程占用其他中间件/监控agentwmic process where processid=1234 get commandline

这张表建议截图保存。以后遇到任意端口占用问题,先按行对号入座,基本不会跑偏。

6.2 分步实操

完整执行一遍以下步骤,能从“报错”一路走到“验证通过”。

第一步,确认监听状态:

netstat -ano -p tcp | findstr "1099" netstat -ano -p tcpv6 | findstr "1099"

第二步,检查端口保留段:

netsh interface ipv4 show excludedportrange protocol=tcp

第三步,根据结果分两种情况处理。如果1099落在保留段里,按第3章的winnat重启或动态端口调整方案处理。如果netstat能查到PID,先看这个进程是谁,再决定kill还是停止服务。

第四步,修改或确认Tomcat配置。把JMX端口从1099改成其他高位端口,确保新端口不在保留段内,确保setenv.bat、catalina.bat、服务参数里没有旧值残留。

第五步,重启Tomcat并验证:

net stop tomcat9 net start tomcat9 netstat -ano | findstr "1099" netstat -ano | findstr "16001" curl http://localhost:8080/

这一步的目的是确认旧端口不再监听、新端口正常监听、HTTP服务能正常返回。三步都通过才能算真正解决。

7. 常见问题速查表

7.1 排查类问题

问:1099被占,但netstat完全查不到任何记录,最可能是什么?

答:最优先怀疑Windows保留端口段。用netsh interface ipv4 show excludedportrange protocol=tcp看1099是否落在某个排除区间里。这一步可以解释80%以上的“查不到进程”场景。

问:重启机器为什么没用?

答:如果问题是保留端口段,重启后系统会再次保留同样的区间,所以端口依旧被“虚拟占用”。如果是Windows服务自动拉起进程,重启后服务一样会自动启动,同样是“重启无效”。解决方向要对症,单纯重启解决不了结构性问题。

问:为什么有时候能看到[::]:1099而不是0.0.0.0:1099?

答:说明Java进程监听的是IPv6通配地址,即双栈环境下的所有地址。此时要关注IPv6栈上1099是否被占,并考虑是否加-Djava.net.preferIPv4Stack=true强制走IPv4简化问题。

7.2 操作类问题

问:改端口应该改哪里?

答:如果是JMX导致1099,改JAVA_OPTS里的com.sun.management.jmxremote.port;如果是HTTP端口,改server.xml里的<Connector port="8080">;如果是AJP,改<Connector port="8009">。先想清楚当前报的是哪个端口的错。

问:改完端口还是报1099,怎么排查?

答:全局搜索所有配置文件里的1099,确认没有旧值残留。Windows上可以用findstr /s /i "1099" *.bat server.xml,Linux上用grep。同时检查服务注册参数和系统环境变量,确保没有任何隐藏入口还在用旧端口。

问:生产环境怎么避免以后再遇到?

答:把JMX端口这类监控端口统一规划到高位区间,比如16000以上,并提前用netsh确认新端口不在保留段内。同时把setenv.bat里的配置纳入版本管理,避免有人手工加上五花八门的端口参数。

8. 我的处理习惯与几条避坑心得

8.1 先拉三张表

遇到端口相关的问题,我的习惯是先拉三张表再动手:第一张是端口监听表,用netstat看实际情况;第二张是端口保留表,用netsh查系统保留段;第三张是服务列表,用sc queryex看有没有中间件被注册成Windows服务。这三张表坐下来,问题基本就定位到某个方向了,剩下的只是执行层面的问题。

不要一上来就杀进程、改配置,那样容易陷入“杀不掉、改不对、重启无效”的循环。很多Tomcat 1099的折腾,根源都是在一开始就少看了保留段这张表。

8.2 几条避坑心得

关于winnat服务,重启它确实能解决一部分保留段冲突,但不保证每次都成功。我遇到过有些机器因为启用了Hyper-V,重启winnat后保留区间只是换个起点,1099照样被圈进去。这时候最稳的还是调整动态端口范围,或者直接把端口挪到安全区间。

关于配置搜索,我见过一个案例,同事改了setenv.bat里的JMX端口,但Tomcat安装目录下还有另一个旧版catalina.bat,启动脚本里用追加方式覆盖了JAVA_OPTS,结果改了半天还是1099。所以搜索范围要覆盖整个Tomcat目录,别只看某一个文件。

关于端口选择,我个人觉得16000到20000这个区间在多数服务器上还是比较安全的,既避开了常见的开发端口,也大概率不会撞进保留段。选中新端口后,务必查一遍保留段再写进配置。

最后说一个细节:JMX如果开启了远程监控,除了1099这个RMI注册端口,JVM还会再开一个随机的RMI数据端口。有时候你解决了1099,过一会儿报错变成了另一个端口,不要慌,这通常是同一个JMX服务的两个端口都在被系统保留或占用,按同样的思路排查即可。端口问题只要思路对了,剩下的都是时间问题。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进