尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java服务启动卡死排查全攻略:从现象到根因
先交代一下背景。最近连续接了两个线上反馈都是同一个症状新版本构建出来的jar包扔到测试环境秒起一上生产就“挂”在启动阶段日志刷到一半直接停住进程还活着可端口就是不开等多久都没反应。一开始我也习惯性走“重启大法”但连续几次之后发现同一个包反复重启卡住的位置一模一样根本不是随机抖动。最后拆下来根因居然在网络层和初始化顺序上跟代码逻辑本身没什么关系。这个过程让我觉得很有必要把jar启动卡死这一类问题从头到尾理一遍现象怎么分、链路怎么拆、用什么命令抓证据、怎么定位到具体的等待点。这篇内容主要面向Java后端开发、运维和负责发布上线的人尤其是那些遇到过“启动慢、启动卡住、日志不动但进程没死”这类现象却一时不知道从哪下手的同行。我会尽量把排查路径写得能直接照着做而不是只讲原理。1. 先分清属于哪种“卡死”四类现场与初步判断很多人排查启动卡死第一步就错了。原因很简单大家口中的“卡死”其实不是同一种东西。进程状态、日志行为、CPU占用不一样背后的排查方向完全相反。如果一上来就用jstack抓线程很可能抓了个寂寞因为问题根本不在线程执行上。我按实际见过的现场把“jar启动卡死”大致分成四类日志彻底停住型启动日志刷到某一行不再更新新输出为零进程CPU占用趋近于0。这种大概率是主线程在等待某个永远不会回来的响应常见于网络IO、注册中心连接、数据库连接池初始化、DNS解析这类外部依赖调用。日志反复重试型日志不停刷“重试”“连接失败”“等待xx秒后重新连接”但始终走不到“Started”关键字。这种严格说不是卡死是启动流程被外部依赖拖住了最常见于数据库连不上、Redis连不上、注册中心地址配错、权限不对等场景。CPU高但卡住型进程CPU占用非常高甚至把一个核跑满但日志没有任何进展。这种情况要优先怀疑初始化阶段执行了重逻辑比如启动时同步加载大量数据、正则表达式灾难性回溯、死循环扫描classpath、或者GC频繁导致应用线程完全被暂停。假启动成功型日志已经打到了“Started Application”端口也监听了但请求进来就超时或者健康检查永远不通过。这种其实不在“启动卡死”的范畴里但它经常被误报成启动卡死所以我建议排查前先把它排除掉。我后来给自己定了一个规矩遇到上报“启动卡死”先花三分钟回答三个问题。第一最后一行日志停在哪第二进程是活着还是已经退出第三CPU是0、正常、还是跑满。这三个问题的答案决定了后面用哪一套工具组合。用一个表格来总结这四类现场方便对号入座现象类型进程状态CPU表现最优先怀疑方向首选工具日志彻底停住存活趋近0网络等待、锁等待、IO阻塞jstack、ss、lsof日志反复重试存活正常或略高外部依赖连接异常、配置错误看日志、telnet/curl测试CPU高但卡住存活跑满重逻辑、GC问题、classpath扫描jstat、jstack、Arthas假启动成功存活且监听端口正常启动后业务自检不通过健康检查接口日志记住一个原则先看见象再猜原因不要反过来用原因去套现象。2. JVM从进程启动到服务就绪链条上的每个阻塞位要定位卡死就得把“jar启动”这个黑盒拆成一段一段的链路。我个人习惯把它分成五个阶段进程创建与JVM自身初始化、类加载与静态资源准备、Spring容器初始化如果项目用了Spring系列框架、端口绑定与应用启动完成、启动后置动作。每个阶段能看到的日志不一样能卡住的原因也不一样。不要指望一个命令从头查到尾。2.1 进程创建到JVM自身初始化看不见日志的阶段这一阶段从执行java -jar开始到类加载机制真正加载Main类为止。操作系统要加载JVM动态库、JVM要解析启动参数、分配堆内存、初始化GC、初始化JIT、初始化各种内部子系统。这个阶段的关键特征是应用日志没有任何输出。因为你的代码还没跑起来。如果卡在这一段现象就是进程存在但app.log文件是空的或者只有一句“Picked up JAVA_TOOL_OPTIONS”这种系统级输出。这个阶段比较常见的坑有两个。一个是JVM参数配置异常比如-Xmx和容器内存限制冲突导致JVM在分配内存时被系统反复换页另一个是系统熵池不足卡在安全随机数的初始化上。第二个问题在后面会单独讲因为它是被忽略次数最多的一个点。2.2 类加载与静态资源准备隐藏的扫描重灾区进入这一阶段Main类被加载字符常量池、类加载器体系、ServiceLoader机制开始工作。Spring Boot这类框架在这个阶段会做大量classpath扫描还会通过SPI机制加载各个jar包里的META-INF/spring.factories文件。如果依赖里有很多体积很大的jar包或者某个被扫描的jar包损坏、读取缓慢启动就会在这一段磨蹭很久。现象是日志长时间停留在“Starting Application on xxx”之前偶尔能看到一些类加载相关日志但不多。这里有一个很多文档不会提的经验如果卡在类加载阶段先检查classpath里有没有超大体积的依赖包以及有没有被反编译或手工修改过的jar。我遇到过一次某个依赖包里有几十张几百MB的静态资源图每次启动都要加载扫描一遍导致启动时间长达七八分钟看起来就像卡死。2.3 应用初始化连接外部依赖的高危段日志开始打出数据库、Redis、MQ、注册中心相关信息时说明已经进入应用初始化阶段。这一阶段是jar启动卡死的最高发区域。Spring容器要创建Bean数据源要初始化连接池Redis客户端要建立连接消息队列消费者要启动注册中心要上报实例。任何一个外部依赖超时、连不上、地址配错、被防火墙拦截都可能让主线程或初始化线程长时间阻塞。如果项目用了数据库连接池比如HikariCP默认的初始化逻辑会尝试获取一个初始连接。此时数据库地址不可达、账号密码错误、连接数打满初始化就会卡住并不断重试。如果连接池的初始化超时时间设置得很大那用户看到的就是“日志停住进程活着CPU很低”。判断这一阶段的技巧很简单看日志最后一行停留在哪个组件的启动描述附近。停在“Connecting to database”就优先查数据库停在某个自定义监听器的初始化日志上就优先查这个监听器内部的调用。2.4 端口绑定与启动完成最后的“假卡点”Spring Boot打出“Tomcat started on port xxxx”之后理论上端口已经监听但应用还没正式完成启动。因为后面还有ApplicationRunner、CommandLineRunner、各种PostConstruct、SmartLifecycle回调要执行。这一段如果卡住现象特别迷惑端口明明能连上但请求进来没有正常响应或者Spring上下文一直没发“Started”事件。原因往往是某个Runner里做了远程调用、批量数据初始化、预热缓存等耗时操作而且没有设置超时。这提醒我们只看“端口开了没有”是不够的还要看“Started Application”日志有没有打出来。两者之间的间隙恰恰是问题高发区。3. 五类高发根因从“看环境”到“抓线程”的排查顺序有了链路概念之后再看根因就清楚了。我把常见的启动卡死根因归成五类按照“从外到内、从简单到复杂”的顺序排查能少走很多弯路。3.1 网络层DNS解析、半开连接与连接超时这是最让我长教训的一类。某次生产环境jar启动卡在初始化外部服务客户端的地方应用日志没有任何报错jstack抓出来的主线程栈停在SocketInputStream.read上。用ss -antp查看进程的网络连接发现到某个服务IP有一条ESTABLISHED状态的连接但连接上没有任何数据传输。后来查清楚这个外部服务的网关变更过白名单导致旧IP被静默丢弃但TCP连接没有被强制断开客户端这边还以为连接活着同步等待响应永远等不到。在排查网络层时有一个反常识的坑ESTABLISHED不等于“连接可用”半开连接一样显示为ESTABLISHED。DNS解析卡顿也属于这一类。Java发起DNS解析时如果走系统的/etc/resolv.conf而配置的DNS服务器响应极慢可能在解析主机名时卡几十秒甚至更久。遇到启动卡在网络调用段可以用getent hosts 目标主机名和curl -v telnet://IP:端口这类命令反向验证一下网络层是不是正常。3.2 熵池阻塞随机数初始化那个经典坑这个坑非常隐蔽但我建议每个做JVM服务的人都提前了解。JVM启动过程中很多安全相关的类会触发SecureRandom的初始化而SecureRandom在老版本Linux上默认从/dev/random读取熵。如果系统熵池不足读取操作会阻塞线程就停在“等待更多熵输入”的状态CPU占用极低日志几乎没有输出。这个坑最讨厌的地方在于代码层面没有任何问题数据库、Redis、网络全通但它就是起不来。而且测试环境一般不会复现因为测试机器上的熵池往往比较充足或者压力不够大。生产环境的物理机或虚拟机一旦熵池被耗尽问题就冒出来了。快速判断方法jstack里看到主线程栈中包含Random、SecureRandom、NativeRead相关调用同时系统cat /proc/sys/kernel/random/entropy_avail的数值极低基本可以确认。临时规避办法是在启动参数里加-Djava.security.egdfile:/dev/./urandom但长期看还是要在操作系统层面补充熵源。3.3 初始化锁等待从死锁到活锁这个属于JVM内部问题。多个初始化线程在启动时互相等待对方持有的锁就会形成死锁。现象是应用日志卡住、线程dump里能看到一堆BLOCKED状态线程并且等待的锁对象互相咬合。比死锁更难抓的是活锁。活锁里线程没有阻塞状态一直是RUNNABLE但都在互相让路、重复重试、或者各自执行一段永远结束不了的任务整体进度为零。活锁在启动阶段最典型的表现就是初始化线程反复重试同一个外部调用日志里看不到报错进程却在空转。排查锁等待的核心技巧是多取样、对比变化而不是只看一次dump。只看一次dump你只能知道此刻线程卡在哪无法知道它是“卡了一会儿即将继续”还是“永续卡死”。两次dump间隔十秒左右状态完全没变化基本可以判定异常。3.4 内存、GC与容器限制JVM堆设置过大但容器或物理机的可用内存不足会导致JVM在启动分配堆内存时触发大量磁盘换页整个启动过程拖得非常慢看起来就像卡死。这种现象在容器环境里特别常见-Xmx设了4G但cgroup限制只有2GJVM不会直接报错而是不断在物理内存和交换分区之间来回倒腾。排查方法很简单先看free -g和容器的cgroup限制再看jstat -gc启动阶段的GC频率。如果Minor GC和Full GC间隔极短加上系统si/so值很高基本可以断定是内存不足导致的假死。另外JDK版本不同容器内存感知能力也不同。老版本JDK不开启-XX:UseContainerSupport的情况下JVM读到的可能是宿主机总内存然后把-Xmx设置得特别大这种组合下内存不足的问题会放大。3.5 别忽略启动脚本和容器层最后说一类大家容易忽略的问题根本不在JVM而在启动方式本身。nohup java -jar app.jar app.log 21 这段命令看似简单里面也有坑。比如日志目录不存在、没有写权限Java进程本身可能正常启动但shell的重定向操作导致命令无法正常返回又比如启动脚本里用了tail -f去等待日志内容tail阻塞了后续流程。容器环境下docker run挂载了不可用的卷、存储驱动IO异常、镜像层损坏都可能导致应用镜像明明存在但容器启动过程异常缓慢。遇到这种情况在宿主机上用docker logs和docker inspect检查容器状态往往比进容器里查应用日志更有效。4. 真正动手时的小工具箱jstack/jcmd/Arthas怎么组合用现场已经确认根因方向大致有数接下来就是用工具取证。我整理了一套自己的操作顺序基本上按这个顺序走大多数情况能在半小时内定位。4.1 jps与jcmd先确认进程身份和JVM参数拿到PID是第一步。jps -l能列出当前Java进程和主类。注意生产环境经常有多个Java进程别搞错对象最好用jps -lv顺带看启动参数根据应用名来确认是哪一个。jcmd PID VM.flags可以看到JVM启动参数和默认参数。这一步很重要因为很多启动卡死跟参数配置有关。比如某个配置项需要连接外部系统而用户把相关地址配成了宿主机IP而非服务名这种参数错误直接看启动脚本就能发现。4.2 jstack三次采样从快照变成证据这是整个排查流程中最关键的步骤。先jstack /tmp/dump1等十秒再jstack /tmp/dump2再过十秒抓第三次。三次dump放一起对比能看出哪些线程长期停在同一个地方。看线程dump的时候我一般只关心三种状态BLOCKED在等待锁重点看waiting to lock后面的对象描述以及谁持有这把锁。WAITING或TIMED_WAITING在Object.wait()或Thread.sleep()上等往下翻调用栈看等的是哪个资源。RUNNABLE不一定在干活尤其是栈停在网络 socket 的read上时实际是在等数据不是在执行计算。一个个人体会先找主线程一般是main线程看它停在哪个栈帧。如果主线程状态正常再找有没有其他线程正在占用关键资源导致主线程在等它。4.3 Arthas的thread命令和traceArthas在启动卡死排查中的价值是动态分析。thread -n 3能看到CPU占用最高的几个线程thread 线程ID能看到具体调用栈效果类似jstack但更聚焦。用thread -b可以直接找出阻塞其他线程的线程省去手动对比锁关系的步骤。如果怀疑是某段初始化代码执行缓慢可以用trace去追踪方法的实际耗时。不过要注意Arthas是attach到JVM上attach本身就是一个在启动阶段可能卡住的操作。如果JVM还没完全启动attach也不一定能成功。所以我的顺序是jstack优先Arthas作为补充。4.4 网络和系统层的命令配合JVM工具只能看到线程层面网络和系统层还得靠Linux命令。我经常搭配使用的是ss -antp、lsof -p PID、iotop、dstat。ss -antp查看进程的网络连接状态重点看目标IP和端口是否符合预期连接数量是否异常有没有大量SYN_SENT或半开连接。lsof -p PID查看进程打开了哪些文件有时能发现它卡在某个异常文件描述符上。iotop/dstat确认IO和CPU是否健康排除磁盘IO卡顿和swap干扰。4.5 一个速查表贴在工位旁边排查层次命令重点看什么进程确认jps -lv进程存在、启动参数JVM参数jcmd PID VM.flags堆大小、GC策略、系统属性线程jstack3次采样BLOCKED/WAITING/RUNNABLE分布线程增强Arthas thread -b / -n阻塞源、最忙线程内存与GCjstat -gcutil PID 1000GC频率、堆使用率网络ss -antp连接状态、对端IP端口文件与IOlsof -p PID打开的文件、socket列表系统资源dstat / iotop / freeCPU、内存、IO、swap5. 一次从“起不来”到“定位锁链”的完整复盘理论说得再多不如走一遍完整案例。我挑一个印象最深的复盘来讲整个过程挺折腾但路径非常有代表性。5.1 案发现场日志停在一个“无关紧要”的中间行项目是一个常规的微服务启动时依赖注册中心和配置中心。现场反馈生产环境启动jar包日志打到某个自定义监听器初始化“async-channel”相关的描述行之后就再也不动了。进程活着CPU占用不到1%端口未监听没有任何报错输出。第一反应是注册中心连接问题。因为外部依赖里只有注册中心可能拖住启动。但是用telnet测试注册中心地址和端口网络是通的。查看日志里注册中心的连接日志也显示注册成功。这就排除了初级的网络不通问题。5.2 采集jstack现场主线程等在一个Socket读上连续采集三次jstack间隔十秒。三次采样中主线程都停在同一个位置一个堆栈片段显示它在等待某个Socket输入流的数据并且调用方是一个用于“配置拉取”的客户端类。通过ss -antp找到这个Socket对应的连接发现对端地址是配置中心的IP连接状态是ESTABLISHED。奇怪的是这个连接上没有数据流动配置中心侧也查不到对应的活跃会话。这就形成了一个闭环应用认为连接还活着所以一直等配置中心认为会话不存在也不会主动推送数据于是应用在启动阶段无限期等待一个永远不会到来的响应。5.3 根因半开连接导致的同步阻塞最终定位到配置中心侧的网络策略在夜间变更过一次旧连接被静默清理但客户端没有收到RST包。TCP层不知道连接已死客户端代码又恰好没有给这个读取操作设置Socket读超时导致同步阻塞永远持续。修起来其实不复杂一个是网络层把白名单和运维变更流程补上另一个是在代码层面给所有启动阶段的外部依赖调用加连接超时和读超时并且设置总超时上限让启动流程不可能被单个外部调用无限期拖住。这个案例给我的教训有三条。第一不要因为日志停在“中间行”就认定是当前行代码有问题它可能是在等上一个调用的返回。第二ESTABLISHED状态不证明连接可用要看连接上有没有数据传输。第三启动阶段所有外部调用都要有超时这是必须写进规范里的红线。5.4 第二个小案例看起来像代码死锁其实是随机数阻塞另外一个案例短一些但也值得一提。某次服务升级后测试环境一切正常预发环境启动必卡。每次卡住的日志位置都不完全一样但主线程栈都包含与安全随机数相关的类。后来系统排查确认预发环境的熵池值长期在个位数徘徊JVM的SecureRandom初始化时读不到足够的熵于是整个启动进程被拖住。用-Djava.security.egdfile:/dev/./urandom作为临时参数验证后启动立即恢复。这个案例进一步证明启动卡死的根因有时候跟代码一点都不沾边查问题要“从底向上”多看一眼环境。6. 把这个坑变成规定动作启动卡死的预防与巡检定位问题、修复问题只是第一步。真正有价值的是把这类问题变成可预防、可检测、可快速回滚的规定动作。我把改过的启动流程总结成下面几项推荐直接照搬。6.1 上线前的启动预检发布新版本之前不要只确认“构建成功”还要准备好启动阶段的外部依赖清单。在发布单里列出本次启动需要访问的数据库地址、Redis地址、配置中心、注册中心、消息队列等服务并提前用curl、telnet或客户端命令验证连通性。这一步能过滤掉一大半环境变更引起的启动卡死。日常发布最怕的就是“版本没问题但环境在凌晨悄悄变了”白名单变更、端口策略变更、DNS记录调整任何一个都可能导致启动静默阻塞。6.2 启动脚本里的超时保护启动脚本要加“启动超时检测”不要用java -jar直接裸奔。推荐写一个简短的循环检查逻辑进程启动后定期检查“Started Application”日志是否出现超过预设时间就输出当前线程dump并把进程标记为启动失败。这样做的价值在于一旦启动卡死现场是自动保留的而不是被运维人员的“重启大法”清掉。这里加一段简单的思路示例# 启动进程 nohup java -jar app.jar app.log 21 PID$! # 等待启动完成超时自动抓现场 for i in $(seq 1 30); do if grep -q Started Application app.log; then echo startup ok exit 0 fi sleep 2 done # 超时处理保留现场后退出 jstack $PID jstack_timeout_$(date %s).txt kill -9 $PID exit 1这个脚本只是骨架生产环境还可以扩展成发送告警、自动采集ss或lsof信息。核心目的就一个超时不可怕怕的是超时后现场没了。6.3 代码层面的硬规矩从代码层面杜绝启动阶段被外部依赖拖死应该立几条规矩所有启动阶段外部调用必须设置连接超时和读超时默认不允许使用无限超时。静态代码块、PostConstruct里不要发起远程调用或长时间IO操作这类代码应该迁移到异步任务或独立线程中执行。初始化顺序敏感的逻辑要显式声明依赖关系不要让两个Bean在启动时互相拉着对方等锁。依赖包定期清理不要把大体积的静态资源打进运行时依赖里。这些规矩看起来像常识但实际出问题的项目里违反的人真不少。6.4 定期演练与巡检最后建议每季度做一次“启动故障演练”选一台预发机器模拟断网、模拟DNS故障、模拟一堆包不可访问。演练的目的不是证明系统在任何情况下都能启动而是确保团队里每个人都熟悉排查工具和流程。就算不能完整演练也建议在巡检脚本里加上三个指标启动耗时、启动阶段线程dump是否存在异常阻塞、启动阶段网络连接清单是否变化。这三个指标能提前暴露很多隐性问题。最后说一点个人体会排查启动卡死这类问题我最深的感受是现场比猜测值钱工具比经验可靠流程比聪明重要。遇到卡死先保存jstack、连接状态、GC日志、系统资源快照再开始猜测原因。不是每次都能一次猜中但只要现场在就能一层层抽丝剥茧找到那个真正让它停住的地方。另外如果项目已经频繁出现启动卡死别只想着“是不是代码太慢”回头把JVM参数、启动脚本、容器资源配置、外部依赖清单都过一遍很多时候答案就藏在这些“看起来没问题”的地方。把这些环节规范起来之后你会发现启动卡死这种问题其实是所有集群故障里最好预防的一类。
RELATED

相关推荐

【生物】基于matlab模拟自适应辅助控制用于上肢康复研究

【生物】基于matlab模拟自适应辅助控制用于上肢康复研究

✅作者简介:热爱科研的Matlab仿真开发者,擅长数学建模、模型改进、算法创新、程序设计科研仿真。🍎 往期回顾关注个人主页:完整代码获取 定制创新 论文复现私信🍊个人信条:做科研,博学之、审问之…

📅 2026/10/12 13:33:54
Windows批量部署核心原理与企业级落地实践

Windows批量部署核心原理与企业级落地实践

1. 这不是“一键重装”,而是批量部署的底层逻辑很多人看到“批量安装Windows系统”第一反应是找一个带“全自动”“免激活”“秒装”的绿色工具,点几下鼠标就搞定几十台电脑——这恰恰是踩坑的起点。我见过某公司IT管理员用这类工具给32台新采购的办公机…

📅 2026/10/12 13:33:54
Unity鸿蒙崩溃监控实战:Sentry符号化还原C#行号全流程

Unity鸿蒙崩溃监控实战:Sentry符号化还原C#行号全流程

Unity 项目要上鸿蒙,崩溃监控接 Sentry,打包要能打通过,而且线上崩溃要能看到 C# 原始行号。这句话是我在需求邮件里读到的,当时瞅了一眼觉得无非是集成个 SDK 的事,真做起来才发现每一步都藏着坑。尤其是“崩溃符号化…

📅 2026/10/12 13:33:54
MORE NEWS

更多资讯

📰

Linux目录树与文件管理实战:从根目录到权限与查找命令

很多人刚接触 Linux 的时候,第一个感受往往是“我这台电脑的文件到底放在哪里?”和 Windows 的 C 盘 D 盘不一样,Linux 一切从根目录/开始,没有盘符的概念,目录树直接裸露在你面前。这篇文章我围绕 Linux 系统基础操作…

📰

ASP.NET餐饮管理系统源码实战:从状态机到本地部署全解析

1. 为什么一家餐厅需要“自己维护”的ASP.NET餐饮管理系统源码先说一个我前阵子遇到的真实场景。有家连锁快餐店找到我,说他们之前用的是一套SaaS模式的点餐系统,功能看着挺全,但后厨、前台、库房三个角色的权限一直理不顺,而且每…

📰

Windows下Maven安装配置实战:JDK环境变量与settings.xml详解

Windows环境下Maven安装与使用,这个话题看起来基础,但实际操作中坑不少。我见过太多人在这一步卡住,明明照着教程一步步来,最后mvn -v就是报错。问题往往出在一些细节上,比如JDK版本不匹配、环境变量没生效、镜像仓库没…

📰

Selenium与Cypress架构对比:2026年自动化测试框架选型指南

1. 一个走浏览器外部、一个钻进浏览器内部:执行模型的本质差异先聊个我自己的经历。十年前我刚接触自动化测试的时候,团队里没有人能说清楚Selenium到底是怎么"控制"浏览器的,反正就是装上driver、写上find_element().click()&…

📰

MinGW-w64完整包配置指南:从解压到CMake构建的Windows C/C++工具链实战

简介:这份资源是面向Windows平台C/C开发者的MinGW64完整配置包,适合刚接触GNU工具链的初学者以及需要快速搭建本地编译环境的中级开发者。包内共2000个文件,以896个Python脚本、787个C头文件、243个C头文件为主,另含少量txt说明、…

📰

Guava EventBus实战:进程内消息队列如何优雅解耦Java业务模块

接手过一套老系统的同学,大概率遇到过我下面说的这种场景:下单方法里硬生生排着发短信、扣库存、埋点、通知推荐系统,再加个新需求就要在核心流程里插一行新代码,改到最后连文档都找不着北。当时我在一个业务后台里折腾这种"…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬