结论: 在服务器上部署 Java 项目的标准流程是:安装 JDK 17 或 21 并配置 JAVA_HOME,把 Spring Boot 打成的可执行 jar 上传到 /opt/apps,用带 JVM 参数的 java -jar 启动,再用 systemd 服务单元托管实现开机自启与崩溃重启,最后由 Nginx 反向代理 8080 端口对外提供服务。
引言:Java 项目部署的两种形态
Java Web 项目在服务器上主要有两种运行形态:
- 内嵌容器的可执行 jar(Spring Boot 默认方式):一个 jar 包自带 Tomcat,一条
java -jar就能跑,是目前的主流。 - 外置 Tomcat 的 war 包:把 war 放进 Tomcat 的
webapps目录,适合多个应用共用同一个 Tomcat 实例的传统项目。
无论哪种,部署的难点都不在"启动",而在四件后续事项:进程怎么守护、内存怎么分、日志怎么管、端口怎么对外。本文以 Ubuntu 24.04 LTS + JDK 21 + Spring Boot 3.x 为主线(截至 2026 年,JDK 17 与 21 是两个主流 LTS 版本),兼顾外置 Tomcat 的说明。
Java 项目部署方式怎么选?
先给结论:新项目一律用内嵌 jar;只有"多应用共用一个 Tomcat""运维规范要求""老项目无法改造"时才用 war。
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 可执行 jar + systemd | Spring Boot 新项目(推荐) | 部署简单、版本隔离、启停快 | 每个应用独立占内存 |
| 外置 Tomcat + war | 传统 SSM 项目、多应用共用 | 共用容器、运维统一 | 版本升级互相影响 |
| Docker 镜像 | 微服务、多实例 | 环境一致、易扩缩容 | 需掌握容器技术 |
| 面板部署(宝塔等) | 新手、小项目 | 可视化操作 | 可控性差、排错困难 |
第一步:安装 JDK 17 或 21
结论:优先使用发行版仓库的 OpenJDK,版本选择看项目要求——Spring Boot 3.x 要求 JDK 17 及以上,老项目多为 JDK 8 或 11。
sudo apt update
# 二选一,推荐 21(当前 LTS)
sudo apt install -y openjdk-21-jdk-headless
sudo apt install -y openjdk-17-jdk-headless
java -version
javac -version
ls /usr/lib/jvm/若项目需要 Oracle JDK 或特定发行版(如 Temurin),可手动安装并统一由 update-alternatives 管理:
sudo mkdir -p /opt/jdk
sudo tar -zxf OpenJDK21U-jdk_x64_linux_hotspot_21.0.5_11.tar.gz -C /opt/jdk
sudo update-alternatives --install /usr/bin/java java /opt/jdk/jdk-21.0.5+11/bin/java 2100
sudo update-alternatives --install /usr/bin/javac javac /opt/jdk/jdk-21.0.5+11/bin/javac 2100
sudo update-alternatives --config java # 多版本时切换第二步:配置 JAVA_HOME 环境变量
结论:很多"手动能跑、systemd 起不来"的问题源于 JAVA_HOME 只对登录 Shell 生效。系统级配置应写在 /etc/profile.d/ 下,或直接为 systemd 单元显式指定路径。
# 先找到真实路径(注意 java 通常是符号链接)
readlink -f $(which java)
# 输出形如 /usr/lib/jvm/java-21-openjdk-amd64/bin/java
sudo tee /etc/profile.d/jdk.sh <<'EOF'
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib
EOF
sudo chmod +x /etc/profile.d/jdk.sh
source /etc/profile.d/jdk.sh
echo $JAVA_HOME注意:/etc/profile.d/ 只对登录 Shell生效,systemd 服务、cron 任务读不到。因此在服务单元里要么写 Environment="JAVA_HOME=...",要么 ExecStart 直接用绝对路径 /usr/lib/jvm/java-21-openjdk-amd64/bin/java。
第三步:创建专用用户并部署 jar 包
结论:绝不用 root 跑 Java 应用。建一个无登录权限的专用用户,目录权限明确,既安全也便于审计。
sudo useradd -r -m -d /opt/apps -s /usr/sbin/nologin appuser
sudo mkdir -p /opt/apps/myapp /var/log/myapp /data/heapdump
sudo chown -R appuser:appuser /opt/apps/myapp /var/log/myapp /data/heapdump
# 上传 jar 包(本地执行 scp)
# scp target/myapp.jar root@<服务器IP>:/opt/apps/myapp/
sudo -u appuser ls -l /opt/apps/myapp/先手动启动一次验证能否跑通,再交给 systemd:
sudo -u appuser /usr/lib/jvm/java-21-openjdk-amd64/bin/java \
-Xms1g -Xmx1g -Dfile.encoding=UTF-8 \
-jar /opt/apps/myapp/myapp.jar \
--spring.profiles.active=prod \
--server.port=8080常用启动参数:
--spring.profiles.active=prod:激活生产配置文件。--server.port=8080:指定监听端口。--server.address=127.0.0.1:只监听本机,配合 Nginx 使用更安全。--logging.file.name=/var/log/myapp/app.log:指定日志输出文件。
确认能启动后用 Ctrl+C 停掉,进入下一步。
第四步:用 systemd 托管服务
结论:systemd 是守护 Java 进程的最佳选择——开机自启、崩溃自动重启、优雅停止、日志统一进 journald。
创建 /etc/systemd/system/myapp.service:
[Unit]
Description=My Java Spring Boot Application
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/apps/myapp
Environment="JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64"
Environment="PATH=/usr/lib/jvm/java-21-openjdk-amd64/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
EnvironmentFile=-/opt/apps/myapp/myapp.env
ExecStart=/usr/lib/jvm/java-21-openjdk-amd64/bin/java \
$JAVA_OPTS \
-jar /opt/apps/myapp/myapp.jar \
--spring.profiles.active=prod
ExecStop=/bin/kill -TERM $MAINPID
SuccessExitStatus=143
TimeoutStopSec=40
Restart=always
RestartSec=10
KillSignal=SIGTERM
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target把 JVM 参数与环境变数集中到 /opt/apps/myapp/myapp.env:
sudo tee /opt/apps/myapp/myapp.env <<'EOF'
JAVA_OPTS=-Xms2g -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/heapdump -Xlog:gc*:file=/var/log/myapp/gc.log:time,uptime,level:filecount=5,filesize=20M -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom
TZ=Asia/Shanghai
EOF
sudo chmod 600 /opt/apps/myapp/myapp.env
sudo chown appuser:appuser /opt/apps/myapp/myapp.env启用与管理:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp --no-pager
sudo systemctl restart myapp
journalctl -u myapp -f --no-pager
journalctl -u myapp --since '30 min ago' --no-pager | grep -i errorSuccessExitStatus=143 很关键:Java 收到 SIGTERM 后以退出码 143 结束,不声明它 systemd 会把正常停止判定为失败并触发重启。
第五步:JVM 参数怎么调
结论:JVM 调优的 80% 收益来自三件事——固定堆大小、选对垃圾回收器、打开 OOM 快照。其余参数多为锦上添花。
| 参数 | 建议值 | 说明 |
|---|---|---|
-Xms / -Xmx | 相同,如 -Xms2g -Xmx2g | 固定堆大小,避免动态扩缩带来的停顿 |
-XX:+UseG1GC | 默认(JDK 9+) | G1 兼顾吞吐与停顿,通用性最好 |
-XX:MaxGCPauseMillis | 200 | 目标最大停顿毫秒数,是软目标 |
-XX:InitiatingHeapOccupancyPercent | 30~45 | 老年代占用达此比例即启动并发标记 |
-XX:MetaspaceSize / -XX:MaxMetaspaceSize | 256m / 512m | 限制类元数据区,防止无上限增长 |
-XX:+HeapDumpOnOutOfMemoryError | 必开 | OOM 时自动生成快照,是排错依据 |
-XX:HeapDumpPath | /data/heapdump | 快照存放路径,需预留 1.5 倍堆大小的磁盘 |
-Xlog:gc* | 建议开 | JDK 9+ 统一日志,替代旧的 -XX:+PrintGCDetails |
内存分配的经验法则:
- 堆大小 = 可用内存的 50%~70%,剩下的留给元空间、线程栈、直接内存与系统页缓存。4 GB 的机器建议
-Xmx2g,8 GB 建议-Xmx4g~5g。 - 容器环境用
-XX:MaxRAMPercentage=75.0代替固定-Xmx,让 JVM 按容器限额自动分配(JDK 10+ 默认开启UseContainerSupport)。 - 线程数多的应用注意
-Xss(默认 1 MB/线程),线程上千时栈内存占用可观。 - 不使用
-Xmn(固定新生代大小),会干扰 G1 的自适应调节。
运行时排查工具:
jps -l # 列出 Java 进程
jstat -gcutil 1000 10 # 每秒采样一次 GC 情况,共 10 次
jmap -heap # 查看堆配置与使用情况
jcmd VM.flags # 查看实际生效的 JVM 参数
jstack > /tmp/stack.txt # 抓取线程栈,排查死锁与 CPU 飙高
企业QQ咨询




