ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Maven Process terminated 排查:退出码、JDK、依赖

Maven Process terminated 排查:退出码、JDK、依赖 上周三下午群里甩过来一张截图IDEA 的 Maven 面板最后一行是红色的Process terminated上面没有 Java 堆栈没有行号甚至没有一句像样的英文说明就孤零零一行。发图的人说代码一行没改昨天还能跑今天mvn clean install就挂了。这种哑巴报错是 Maven 编译里最烦人的一类——它不像语法错误那样直接告诉你哪一行少了分号而是把真正的死因藏在了子进程的退出码里。这篇东西就是围绕这个场景写的。我会把 Maven 编译过程中出现Process terminated的常见成因拆成四种情况每种都给出可复现的判断方法和修复路径顺带把排查顺序固定下来避免下次再遇到时东一榔头西一棒槌。内容偏向实操适合天天跟 Maven 打交道、被这类报错折磨过的后端和安卓开发看纯新人也看得懂只是需要你手边有一台能复现问题的机器。1. 这条报错为什么难查先弄清Process terminated是谁抛出来的1.1 Maven 只是个调度器真正编译的是它 fork 出去的子进程很多人对 Maven 有个误解觉得它是编译器。其实 Maven 本身不编译任何 Java 代码它是个构建流程编排工具负责解析pom.xml、下载依赖、按生命周期顺序调用插件。真正把.java变成.class的是maven-compiler-plugin而这个插件在多数情况下并不是在 Maven 自己的 JVM 里跑 javac而是再 fork 一个独立的 Java 进程去执行编译。这个设计是有意为之的。好处是编译期的 JVM 参数比如-Xmx、-XX:MaxMetaspaceSize可以和 Maven 主进程分开配置编译一个巨型项目时不会把 Maven 自己的内存吃光坏处就是一旦这个子进程非正常退出Maven 只能捕获到一个进程没了的信号然后把Process terminated抛给你。父进程并不知道子进程经历了什么它只知道我 fork 出去的那个家伙没正常回来。理解了这层父子进程关系很多现象就说得通了为什么加了-X也看不到详细的编译错误因为 Maven 打印的是自己的视角子进程的 stderr 可能根本没被完整转发出来或者被插件的日志框架吞掉了。1.2 为什么这行字没有堆栈Process terminated本质上是进程级的异常不是代码级的异常。Java 里的Exception有堆栈是因为它是在同一个 JVM 内部被抛出来、被捕获、被打印的抛出点和捕获点之间的调用链清清楚楚。而进程被 kill、被系统回收、因为参数错误直接退出这些事件发生在操作系统层面Maven 顶多能拿到一个exit code。所以当你看到这行字的时候第一反应不应该是我代码哪里写错了而应该是这个子进程为什么会死。方向错了后面所有操作都是浪费。我见过太多人在这一步就开始疯狂改代码、删 target 目录、重装 IDEA其实问题可能只是pom.xml里一行source21/source撞上了本地只有 JDK 8 的机器。改代码当然没用。1.3 三分钟把真实原因逼出来-X、-e 与日志重定向在动手排查之前先把信息量拉满。Maven 默认的输出太克制了必须主动打开它的调试开关mvn clean install -X -e build.log 21三个参数各有用处别省-X--debug打开 debug 级别日志会打印每一个插件的版本、每一个 fork 出去的进程完整命令行、传入的 JVM 参数。这是最关键的一条你能直接看到 javac 被用什么参数调起来的。-e--errors让 Maven 打印完整错误堆栈而不是默认的摘要。很多时候真正的Caused by就藏在这里。 build.log 21把标准输出和标准错误都重定向到文件。终端缓冲区有限报错一长前面的内容就被冲掉了落盘之后可以慢慢搜。拿到日志之后直接在文件里搜这几个关键词比从头读到尾效率高得多搜索关键词能定位到什么Command line options子进程完整的启动参数含 JVM 参数和 classpathEXIT CODE/exit code子进程的退出码配合退出码表判断死因OutOfMemoryError内存类问题注意是 heap 还是 MetaspaceCaused by被包裹的真实异常forked process确认是不是 fork 模式提示日志里如果出现Unable to find javac或者No compiler is provided in this environment基本可以直接跳到第 2 节不用往下看了。有了这三分钟的准备工作后面的排查才有依据不然全靠猜。2. 情况一JDK 与环境变量错位编译进程一启动就死2.1 IDEA 里四处 JDK 设置互相打架这是四种情况里出现频率最高的一种尤其在 IDEA 里。原因是 IDEA 关于 JDK 的设置不止一处而且它们各管各的互相之间不做校验。第一处是Project Structure → Project → SDK决定项目整体用哪个 JDK 做代码索引和语言级别。第二处是Settings → Build, Execution, Deployment → Compiler → Java Compiler里面的Target bytecode version决定编译输出目标。注意这一项在 IDEA 用自己的编译器时生效用 Maven 编译时会被pom.xml覆盖很多人就是被这个看起来生效其实不生效的选项骗了。第三处是Settings → Build Tools → Maven → Importing → JDK for importer管的是 Maven 解析pom.xml、做依赖导入时用的 JDK。第四处是Settings → Build Tools → Maven → Runner → JRE这才是真正决定mvn编译时用哪个 JDK 的地方也是Process terminated最常见的直接元凶。四个地方如果指向不同的 JDK就会出现一种很诡异的现象项目在 IDEA 里索引正常、代码不飘红但一执行 Maven 编译就挂。因为索引用的是一套 JDK编译用的是另一套。我自己就踩过Project SDK 设成了 17Runner 里的 JRE 还停在三年前装的 8结果pom.xml里写着release17/releasejavac 直接拒绝启动。解决办法不复杂把这四处统一到同一个 JDK然后File → Invalidate Caches / Restart重启一次。IDEA 的缓存挺顽固不重启有时候改了也不生效。2.2 Maven 与 JDK 的版本对应关系命令行环境下问题通常出在JAVA_HOME和PATH不一致。Windows 上尤其常见的是装了新 JDK 之后JAVA_HOME更新了但PATH里还留着老 JDK 的bin目录于是java -version和javac -version输出两个不同版本。Maven 启动脚本读的是JAVA_HOME但 fork 出去的进程找javac走的是PATH两边一打架就出事。还有一个经常被忽略的点Maven 本身对 JDK 版本有要求。用太老的 Maven 去跑太新的 JDK也会在编译阶段出莫名其妙的问题。Maven 版本建议的 JDK 范围备注3.6.xJDK 7 ~ 14跑 JDK 17 大概率出问题3.8.xJDK 7 ~ 17目前存量项目最多的一档3.9.xJDK 8 ~ 21新项目建议直接用这档4.xJDK 17 起步配置模型有较大调整迁移需谨慎maven-compiler-plugin也有对应的门槛。release参数是 JDK 9 引入的JDK 8 上写release会被忽略甚至报错只能老老实实用source和target。如果你的项目还在用 JDK 8就别抄网上那些用release的新写法。2.3 落地验证三条命令锁死环境在开始改配置之前先在终端敲这三条把事实确认清楚java -version javac -version mvn -vmvn -v的输出里会明确告诉你 Maven 版本、Maven home、Java version、Java home还有操作系统信息。这三条命令的输出如果互相之间对不上那Process terminated的根因基本就锁定了不用再往下猜。修的时候记住一个原则JAVA_HOME指向 JDK 根目录不是bin目录也不是 JRE 目录。写成C:\Program Files\Java\jdk-17是对的写成...\jdk-17\bin或者...\jdk-17\jre都会出问题。这个低级错误我见过不止一次包括我自己年轻时也犯过。改完环境变量之后命令行要重新开一个窗口才生效IDEA 也要重启。旧窗口里的环境变量是启动时快照不会自动刷新。3. 情况二编译器插件参数把 javac 逼到崩溃3.1 source/target 与 --release 混用引发的问题环境没问题了下一个怀疑对象就是pom.xml里的编译配置。典型的maven-compiler-plugin配置长这样plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration source17/source target17/target encodingUTF-8/encoding /configuration /plugin这里最容易出的问题是用source/target写了一个本地 JDK 不支持的版本号。比如本地是 JDK 11配置里写source17/sourcejavac 会直接报invalid source release: 17然后退出。某些 Maven 版本下这个错误的呈现形式就是一行Process terminated真实信息被淹没了。另一类问题是在 JDK 9 上同时写release和source/target。release的语义是按指定版本的 API 编译它内部会自己去设置 source/target两者同时存在时行为不确定插件可能给出警告也可能直接异常退出。正确做法是二选一JDK 9 及以上优先用releaseJDK 8 只能用source/target。3.2 fork 模式下 executable 写错的连锁反应maven-compiler-plugin有几个和进程相关的参数配置错了会直接影响子进程能否启动configuration forktrue/fork executable${env.JAVA_HOME}/bin/javac/executable meminitial256m/meminitial maxmem1024m/maxmem compilerArgs arg-parameters/arg /compilerArgs /configurationforktrue/fork强制插件启动独立进程这时executable必须指向一个真实存在的javac。写错了、路径里有空格没加引号、用了${java.home}但那个变量在当前环境里解析不出来都会导致子进程无法创建。Maven 的表现就是一句Process terminated因为连进程都没起来。compilerArgs也是个雷区。往里塞了一个当前 JDK 不认识的参数比如在 JDK 8 上写--enable-previewjavac 会直接报错退出。这类问题在日志里搜Command line options就能看到完整的参数拼接结果一眼就能发现哪个参数不对。注意forktrue/fork和maxmem是搭配使用的。如果你不需要自定义编译期内存直接把fork去掉让编译在当前进程内完成反而少一层故障点。只有在确实需要独立内存配置时才开 fork。3.3 Lombok 与注解处理器路径的版本坑注解处理器是另一大高频原因尤其是 Lombok。Lombok 是通过修改编译器行为工作的它对 JDK 版本极其敏感Lombok 版本支持的 JDK 上限1.18.20 及以下JDK 161.18.22 ~ 1.18.24JDK 171.18.30 及以上JDK 21在 JDK 17 上用 1.18.20 的 Lombok编译时会抛出java.lang.NoSuchFieldError: Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field然后子进程崩溃。这个堆栈有时候能被-e打出来有时候就只剩Process terminated。用annotationProcessorPaths显式声明处理器路径的写法也容易出问题annotationProcessorPaths path groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.30/version /path /annotationProcessorPaths一旦这里的版本和dependencies里声明的 Lombok 版本不一致就会同时加载两个版本处理器初始化阶段直接炸。我的习惯是把 Lombok 版本提成一个 property两处都引用它从源头上杜绝版本漂移。4. 情况三内存不足与进程被系统回收4.1 Metaspace 撑爆时的典型症状前两种情况是配置错这种是资源不够。大型多模块项目编译时javac 需要加载大量符号信息堆和 Metaspace 都会膨胀。默认参数下Maven 主进程和 fork 出去的编译进程各有各的内存上限任何一个撞墙都会让编译中断。Metaspace 不够的典型信号是日志里出现java.lang.OutOfMemoryError: Metaspace或者更隐蔽的OutOfMemoryError: Compressed class space。堆不够则是Java heap space。这两种的解法完全不同别混为一谈前者要调-XX:MaxMetaspaceSize后者要调-Xmx。配置入口有两处。一处是环境变量MAVEN_OPTS作用于 Maven 主进程export MAVEN_OPTS-Xms512m -Xmx2048m -XX:MaxMetaspaceSize512mWindows 下在系统环境变量里加注意别覆盖了已有值。另一处是项目根目录下的.mvn/jvm.config文件Maven 3.3.1 支持内容就是纯 JVM 参数一行一个-Xms512m -Xmx2048m -XX:MaxMetaspaceSize512m.mvn/jvm.config的好处是跟着项目走团队成员和 CI 环境自动一致不用每个人去配环境变量。我现在的做法是所有团队项目都放这个文件省掉大量在我机器上是好的的扯皮。4.2 退出码是最好用的线索子进程被系统强制干掉时Maven 打印的退出码就是最直接的证据可惜很多人不看。常见的几个退出码含义大概率原因1一般性错误编译失败、参数非法、类找不到2使用方式错误javac 参数写错137128 9SIGKILL被 OOM Killer 杀掉内存严重不足143128 15SIGTERM被外部信号终止常见于 CI 超时-10737418190xC0000005 访问冲突Windows 下 JVM 崩溃或原生库问题看到 137就别再折腾pom.xml了一定是内存问题去查容器内存限制或者宿主机可用内存。看到 143八成是 CI 平台的执行超时去调流水线配置。看到-1073741819那可能是 JVM 本身或者某个 native 库出了问题答案不在 Maven 这一层。4.3 调参与并行编译的取舍内存参数不是越大越好。-Xmx设得太夸张反而会让 JVM 延迟回收、GC 停顿变长编译总时间上升。一组在多数中大型项目上比较稳的起点-Xms512m -Xmx2g -XX:MaxMetaspaceSize768m -XX:UseG1GC-Xms和-Xmx设成一样的值可以避免堆动态扩张带来的开销但会占用更多常驻内存在开发机上不一定划算。我一般开发机上Xms设小一点CI 上设成和Xmx相同。并行编译要谨慎开。mvn -T 1C按 CPU 核数并行构建模块模块之间没有依赖关系时提速明显但每个并行任务都要占内存内存本来就紧张的话开并行等于加速崩溃。多模块项目遇到Process terminated时第一件事就是去掉-T参数再试一遍如果去掉就好了那问题就是并行度超过了资源上限。5. 情况四本地仓库与依赖的隐性损坏5.1 .lastUpdated、残缺 jar 与 _remote.repositories本地仓库放在~/.m2/repositoryWindows 是C:\Users\你的用户名\.m2\repository。这个目录是 Maven 的缓存正常情况下不用管但它出问题时非常隐蔽。典型的损坏形式有三种。第一种是.lastUpdated文件当你某次下载依赖失败网络抖动、仓库不可达Maven 会写一个xxx.jar.lastUpdated文件记录失败时间。在更新周期内默认 24 小时Maven 认为这个依赖刚试过、下不来于是不再重试直接报依赖解析失败。表现就是明明仓库里配置没问题某个依赖死活拉不下来。第二种是下载中断导致的残缺 jar。文件存在但大小不对解压报错或者缺失关键 class。编译期引用到这个 jar 里的类时javac 加载失败子进程异常退出。第三种是_remote.repositories文件记录了构件来源仓库。当你切换了镜像仓库之后这个文件记录的源和当前配置不匹配Maven 会拒绝使用本地已有的 jar转而重新下载如果新仓库里又没有就直接失败。处理办法是按梯度来别一上来就删整个仓库# 第一档强制更新快照和发布版本 mvn clean install -U # 第二档清掉指定依赖再拉 mvn dependency:purge-local-repository -DmanualIncludecom.example:broken-lib # 第三档实在不行再删目录重下 rm -rf ~/.m2/repository/com/example-U参数的作用就是忽略.lastUpdated的缓存强制检查远程更新。很多昨天还能下今天不行的问题一条-U就好了。5.2 镜像配置写错会拉回一堆假依赖settings.xml里的mirrors配置是另一个重灾区。常见错误是配了多个镜像每个的mirrorOf都写*mirror idmirror-a/id mirrorOf*/mirrorOf urlhttps://example-a.com/repository/maven-public//url /mirror mirror idmirror-b/id mirrorOf*/mirrorOf urlhttps://example-b.com/repository/maven-public//url /mirrorMaven 的镜像匹配规则是按顺序取第一个匹配的后面的直接忽略而且不会给任何提示。你以为配了两个做备份实际上永远只有第一个在干活。更糟的情况是两个仓库的坐标体系不完全一致某个构件只有 B 有但请求全被 A 截走了结果就是依赖解析失败。正确的做法是让mirrorOf精确一点只代理 central 就写central需要代理全部外部仓库写external:*排除 localhost 和 file:// 协议需要排除特定仓库用external:*,!repo-id。提示改完settings.xml一定要跑一次mvn help:effective-settings它能告诉你最终生效的镜像配置是什么。别猜看结果。5.3 清理的边界删什么、留什么删了重来确实是万能解但代价太大。一个有几万个构件的仓库重新拉一遍半小时起步。我的清理边界是这样的可以放心删target目录、.lastUpdated文件、_remote.repositories文件、报错的那几个具体构件目录。谨慎删整个~/.m2/repository。只在前面所有手段都无效时作为最后手段且删之前确认网络能正常访问镜像仓库否则删了也下不回来。不要删settings.xml、~/.m2/wrapper、自己mvn install上去的私有构件这些远程仓库里没有删了就得重新构建上游项目。另外如果是公司内网环境镜像仓库地址配错或者临时不可达表现也是依赖解析失败进而编译中断。这种情况先ping一下仓库域名或者直接浏览器打开仓库地址看看能不能访问比在pom.xml里瞎改快得多。6. 一套能反复用的定位顺序6.1 从退出码往下推前面四种情况讲完了但真实场景里它们是混在一起的你得有个顺序。我的顺序是从最外层往最内层推第一步看退出码。有退出码就先查表137 直接去查内存143 去查超时1 和 2 再往下看。这一步能砍掉一半的可能性。第二步看mvn -v。确认 Maven 版本和 Java 版本以及它们是否在建议的匹配区间内。不匹配就先解决环境问题。第三步看-X日志里 fork 出去进程的完整命令行。参数拼接是否正常-source/-target/--release的值本地 JDK 是否支持一眼就能确认。第四步才轮到pom.xml的插件配置和依赖。这个顺序的核心逻辑是先排除外部因素再看项目自身配置。反过来做的话很容易在一个本身就配错的pom.xml上反复折腾最后发现是机器 JDK 版本不对。6.2 二分法拆模块、跳测试、剥插件如果退出码正常、环境也正常那就得用二分法缩小范围。三个维度可以切按模块切。多模块项目用mvn clean install -pl 目标模块 -am只构建出错的那个模块及其上游依赖。如果单模块能过说明问题出在模块间的交互上比如传递依赖冲突。按阶段切。-DskipTests跳过测试-Dmaven.test.skiptrue连测试代码都不编译。如果跳过测试就好了问题出在测试类或测试依赖上比如某个测试作用域的 jar 版本冲突。按插件切。把可疑插件临时注释掉看构建能不能往下走。maven-compiler-plugin之外maven-enforcer-plugin、maven-shade-plugin、protobuf-maven-plugin这些都是常见的中途退出制造者。# 只构建单模块及其依赖 mvn clean install -pl user-service -am -DskipTests # 跳过测试并打开调试 mvn clean test-compile -X -e debug.log 21二分法的价值在于把整个项目哪里都可能有问题缩小到就是这一条路径有问题。每次排查我都从这几个维度试比漫无目的地改参数高效得多。6.3 排查决策表把前面所有内容压缩成一张表贴在工位上随时可以对照现象优先检查对应情况报错前没有任何编译信息一闪而过mvn -v、IDEA Runner JRE情况一日志里出现invalid target releasepom.xml的 release/source/target情况二日志里出现Unable to find javacexecutable路径、JAVA_HOME情况一 / 二OutOfMemoryError: MetaspaceMaxMetaspaceSize、.mvn/jvm.config情况三退出码 137 / 143容器内存限制、CI 超时情况三某个依赖反复解析失败.lastUpdated、镜像配置情况四本地 jar 解压报错删除该构件目录重新拉取情况四只在 CI 上出现本地正常环境变量差异、JVM 参数差异情况一 / 三这张表不是万能的但它能帮你快速排除掉大部分干扰项把注意力集中到真正可能的地方。7. 我实际踩过的几个坑7.1 路径里的中文与空格项目路径里带中文或者说空格在 Windows 上是经典的踩坑点。C:\Users\张三\Documents\我的项目这种路径某些 Maven 插件在拼命令行时不做引号转义fork 出去的子进程参数就被空格切断了javac 收到一个不存在的文件路径直接退出。中文的话如果 JVM 的file.encoding和实际编码不一致路径解析也可能出问题。处理办法很朴素项目尽量放在纯英文、无空格的路径下比如D:\work\project-name。IDEA 的工作区路径、本地仓库路径、Maven 安装路径通通同理。这条我第一次听说时觉得是玄学直到自己遇到一次改路径之后立刻就好了。7.2 杀毒软件、文件锁与 target 目录Windows 上的实时防护、Mac 上的某些文件同步工具会实时扫描target目录里新生成的.class文件。当编译速度很快、生成文件很密集时扫描进程可能锁住文件导致 javac 写不进去进程异常退出。表现是随机性的同一个项目十次编译可能失败两次重跑又好了。这种随机失败最容易被归咎于Maven 不稳定其实是文件锁。验证方法很简单把target目录从实时扫描的排除列表里加进去如果不失败了就确认了。IDEA 里还有一层文件系统监听Settings → Appearance Behavior → System Settings → File System同步任务太频繁时也可能造成类似干扰。7.3 .idea 与本地缓存的残留配置最后这个坑最阴。.idea目录里存着 IDE 层面的配置包括 Maven 的 JRE 设置、编译输出路径等。如果这个目录是从别人那里拷来的、或者从旧版本 IDE 迁移过来的里面的配置可能和当前环境完全不兼容但 IDEA 不会提示你。判断方法是用命令行跑一遍。命令行能过、IDEA 不能过那就是 IDE 配置或缓存问题不是项目本身的问题。这时直接删掉.idea目录重新导入项目或者 File → Invalidate Caches / Restart。如果.idea已经被提交到 Git 仓库里这种情况比想象中多那更要清理一遍还要往.gitignore里补上。每个人的 IDE 环境不同这类文件根本不该进版本控制。我个人现在遇到Process terminated的固定流程是先跑mvn -v确认环境再跑一次带-X -e的命令行构建拿到退出码然后按退出码分流最后才去动 IDE 和pom.xml。这套流程走到今天还没遇到过查不出来的情况区别只是花的
返回列表