服务器上怎么部署Java项目

结论: 在服务器上部署 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 + systemdSpring Boot 新项目(推荐)部署简单、版本隔离、启停快每个应用独立占内存
外置 Tomcat + war传统 SSM 项目、多应用共用共用容器、运维统一版本升级互相影响
Docker 镜像微服务、多实例环境一致、易扩缩容需掌握容器技术
面板部署(宝塔等)新手、小项目可视化操作可控性差、排错困难

第一步:安装 JDK 17 或 21

结论:优先使用发行版仓库的 OpenJDK,版本选择看项目要求——Spring Boot 3.x 要求 JDK 17 及以上,老项目多为 JDK 8 或 11。

bash
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 管理:

bash
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 单元显式指定路径。

bash
# 先找到真实路径(注意 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 应用。建一个无登录权限的专用用户,目录权限明确,既安全也便于审计。

bash
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:

bash
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:

ini
[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:

bash
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

启用与管理:

bash
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 error

SuccessExitStatus=143 很关键:Java 收到 SIGTERM 后以退出码 143 结束,不声明它 systemd 会把正常停止判定为失败并触发重启。

第五步:JVM 参数怎么调

结论:JVM 调优的 80% 收益来自三件事——固定堆大小、选对垃圾回收器、打开 OOM 快照。其余参数多为锦上添花。

参数建议值说明
-Xms / -Xmx相同,如 -Xms2g -Xmx2g固定堆大小,避免动态扩缩带来的停顿
-XX:+UseG1GC默认(JDK 9+)G1 兼顾吞吐与停顿,通用性最好
-XX:MaxGCPauseMillis200目标最大停顿毫秒数,是软目标
-XX:InitiatingHeapOccupancyPercent30~45老年代占用达此比例即启动并发标记
-XX:MetaspaceSize / -XX:MaxMetaspaceSize256m / 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 的自适应调节。

运行时排查工具:

bash
jps -l                          # 列出 Java 进程
jstat -gcutil  1000 10     # 每秒采样一次 GC 情况,共 10 次
jmap -heap                 # 查看堆配置与使用情况
jcmd  VM.flags             # 查看实际生效的 JVM 参数
jstack  > /tmp/stack.txt   # 抓取线程栈,排查死锁与 CPU 飙高