一台干净的Linux服务器摆在面前,任务很明确:装上JDK,把打包好的jar包跑起来,然后再让它稳定地在后台运行。这个流程我做过几十遍,也帮别人排查过各种“玄学”问题。说句实在话,安装本身不难,难的是那些零碎的坑——下载地址不知道去哪找、环境变量配了不生效、jar包起来三秒就挂、日志又没什么有价值的信息。所以这篇内容就围绕“Linux系统上安装JDK、部署jar包,以及如何指定JDK运行jar文件”这条主线展开,把版本选型、安装方式、多版本共存、后台运行、日志管理和常见故障排查一次讲透。不管你是刚接触云服务器的新手,还是在虚拟机里折腾Linux的学生,甚至是团队里负责发布的老哥们,只要目标是让一个Java程序在Linux上正常跑起来,这篇基本够用了。
1. 装JDK之前:先想清楚版本、系统架构和安装方式
很多新手上来就急着下载JDK,结果发现装到一半要么版本不对,要么系统不兼容。这一节先把前置问题理清楚,后面执行起来就会顺很多。
1.1 JDK版本怎么选:8、11、17还是21
选版本这件事直接决定你后面会不会被“缺模块”“编译报错”之类的问题折磨。我的建议是看项目实际需求,而不是追求最新:
- 如果项目是老的Spring Boot 2.x、老版Hadoop或其他历史包袱重的技术栈,通常需要Java 8,也就是JDK 1.8,这个版本至今在不少企业里仍然是绝对主力。
- 如果项目用的是Spring Boot 3.x,那最低要求是Java 17。很多新项目现在直接踩在17这个LTS版本上,性能和生态都比较均衡。
- Java 11也算LTS,早期Spring Boot 2.x的某些版本支持得不错,但现在逐步边缘化。
- Java 21是最新的LTS版本,适合全新项目,但依赖组件也要跟进,否则可能碰到兼容性问题。
建议直接选择LTS版本,别用中间版本。你搜索“jdk 17下载”或“jdk下载官网”时,注意区分Oracle JDK和OpenJDK。生产环境多数人用OpenJDK,因为免费且无调整期问题;Oracle JDK本身并不是不能用,主要是要注意授权条款。腾讯云、阿里云软件源也都有OpenJDK,直接改源后用包管理器装也行,省去不少麻烦。
1.2 确认Linux发行版和CPU架构
不同发行版安装命令不一样,CPU架构不同JDK的包也不同。登录服务器后先执行三条命令:
# 查看系统版本信息 cat /etc/os-release # 查看CPU架构,x86_64就是64位,aarch64就是ARM64 uname -m # 看看系统里是不是已经装了Java java -version这几条命令属于最基础的Linux常用命令,但非常重要。比如你在CentOS 7上用了Ubuntu的apt,肯定装不上;你下载了x64的JDK包装到ARM服务器上,运行时会直接报“cannot execute binary file”。如果系统里已经有Java,先看版本,再决定要不要卸载重装。
1.3 明确安装方式:包管理器还是手动解压
Linux下装JDK有两条主流路线,我之前都实测过:
- 包管理器安装:
yum install java-17-openjdk或apt install openjdk-17-jdk。优点是快、自动处理依赖和环境变量,缺点是版本受软件源控制,想装特定小版本或Oracle JDK就不方便。 - 手动解压tar.gz包并配置环境变量。优点是版本完全可控,想装哪个装哪个,想共存几个JDK都行,Java多版本切换很灵活,生产环境我推荐这种方式。
下面的章节我会把两条路线都详细走一遍,重点放在第二种,因为它能解决“linux指定jdk运行jar文件”这个核心诉求。
2. 安装JDK的两种主流路线
2.1 用包管理器安装:适合快速验证或临时环境
如果你只是想快速搭个环境跑一下,包管理器是最省事的。CentOS、Rocky Linux这类RedHat系用yum或dnf,Debian、Ubuntu用apt。
CentOS/RHEL系安装Java 17:
# 先搜一下可用版本 yum list available | grep openjdk # 安装Java 17 yum install -y java-17-openjdk # 验证 java -versionUbuntu/Debian系安装Java 17:
# 更新软件源 apt update # 安装JDK apt install -y openjdk-17-jdk # 验证 java -version安装完成后,用which java看一下路径。这种方式的优点是java命令自动进入PATH,环境变量基本不用管。但如果你想指定某个版本的JDK去跑一个jar,后续还是需要手动定位到具体的JAVA_HOME路径。
2.2 手动解压安装并配置环境变量:生产环境首选
这是我推荐的方式。流程就四步:下载、解压、配置环境变量、验证。关键在于每一步都要知道自己做了什么。
第一步,下载JDK。我一般去Oracle官网或者Adoptium(Eclipse Temurin)下载稳定版,有些网络环境下官网慢,也可以找国内云厂商的镜像站。下载前确认你需要的版本和CPU架构。比如我要在x86_64的Linux上装JDK 17:
# 进入安装目录习惯用的地方 cd /opt # 用wget下载tar.gz包,实际链接以你选择的版本为准 wget https://下载地址/jdk-17_linux-x64_bin.tar.gz如果服务器上没有wget,用curl -O也可以。下载完成后先校验下文件大小,至少确认不是几KB的错误页面。
第二步,解压到统一目录。我习惯把JDK统一放在/usr/local/java目录下:
mkdir -p /usr/local/java tar -zxvf jdk-17_linux-x64_bin.tar.gz -C /usr/local/java # 看一眼解压出来的目录名 ls /usr/local/java解压后一般是一个类似jdk-17.0.x这样的目录。为了方便后续升级和路径切换,我还会给它做一个不带版本号的软链接:
ln -s /usr/local/java/jdk-17.0.x /usr/local/java/jdk17这样环境变量里写/usr/local/java/jdk17,以后JDK小版本更新,只需要调整软链接指向,不用改环境变量。
第三步,配置环境变量。编辑/etc/profile:
vim /etc/profile在文件末尾追加:
export JAVA_HOME=/usr/local/java/jdk17 export PATH=$JAVA_HOME/bin:$PATH export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar保存后让配置立即生效:
source /etc/profile这里解释一下为什么设置JAVA_HOME这么重要。很多中间件,比如Tomcat、Spring Boot的启动脚本,都会直接读取这个变量来找java命令。如果你只改了PATH而没设置JAVA_HOME,就可能出现命令行java -version能显示,但某些工具启动时依然报“Unable to locate a Java Runtime”。
第四步,验证:
java -version javac -version echo $JAVA_HOME如果屏幕上显示出对应版本号,说明安装成功。
2.3 环境变量配置失败的高频原因
“jdk环境变量配置失败”是搜索热词,说明这个问题太常见了。我踩过或被问过的坑主要有这几个:
- 忘记执行
source /etc/profile,或者新开的SSH会话没有重新登录,导致环境变量不生效。很多初学者容易犯这个错,以为配完就能用。 - 变量名写错,比如
JAVA_HOME拼成JAVAHOME,或者路径中间少了/。配置完建议用env | grep JAVA检查一下。 - 不小心删掉了
PATH中原有的内容。你写export PATH=$JAVA_HOME/bin:$PATH时,$PATH一定不能省,否则连ls、vim这些基础命令都会变得不可用。 - 在同一台机器上既装了OpenJDK又手动配了Oracle JDK,导致
java命令被软链接指向了另一个版本。这种情况下要用type -a java查看所有路径,再把旧的软链接或旧路径从PATH中移除。
提示:如果你在环境中发现
ls都不见了,大概率是PATH被覆盖了,紧急恢复办法是直接用绝对路径执行命令,比如/usr/bin/vim /etc/profile,把错误配置改正后重新登录会话。
3. 让jar包用指定的JDK版本运行
多版本JDK共存是非常现实的场景。一台服务器上同时部署老项目和新项目,老项目用Java 8,新项目用Java 17,这不奇怪。下面是我常用的两种做法。
3.1 多版本JDK共存的实际场景
你可能有多个Java应用要部署,而它们依赖的JDK版本不一样。最典型的两种:
- 老项目:基于Spring Boot 2.x,需要JDK 8。
- 新项目:基于Spring Boot 3.x,强制JDK 17。
如果系统里已经装了一套JDK 8,再装一套JDK 17,两个版本的目录互相独立存放,环境变量只需要指向默认版本。启动具体应用时,用具体JDK的java命令去启动jar包,这就是“指定JDK运行”的核心思路。
比如旧的demo-old.jar必须用JDK 8跑:
/usr/local/java/jdk8/bin/java -jar /opt/app/demo-old.jar新的demo-new.jar用JDK 17跑:
/usr/local/java/jdk17/bin/java -jar /opt/app/demo-new.jar这种直接指定绝对路径的方式最直观,也最不容易出错。你不需要关心当前PATH里默认的java是谁,只要确认目标JDK路径存在、权限可执行,就能精确控制。
3.2 处理“找不到jdk”的问题
很多时候jar包启动脚本用的是java命令,而不是绝对路径。如果脚本里写的是java -jar xxx.jar,它找的是PATH里的java。想让脚本找到我们指定的JDK,只需要在运行前临时设置:
export JAVA_HOME=/usr/local/java/jdk17 export PATH=$JAVA_HOME/bin:$PATH java -jar xxx.jar这种方式在当前Shell会话内有效,关闭会话后失效。如果不想每次手动敲,可以把这些export写进一个启动脚本里,比如:
#!/bin/bash export JAVA_HOME=/usr/local/java/jdk17 export PATH=$JAVA_HOME/bin:$PATH nohup java -jar /opt/app/demo-new.jar &这样既固定了JDK版本,又保留了一段时间内可手动复用的逻辑。
3.3 更规范的方案:update-alternatives和软链接
在Debian/Ubuntu系统上,可以用update-alternatives管理java版本:
sudo update-alternatives --config java执行后会出现一个交互式列表,输入数字就能切换系统默认的java。这种方式适合只想整体切换默认java的场景,但如果你要同时运行多个不同JDK的应用,其实还是得回到“脚本里指定绝对路径”这个思路。
软链接的方式同样有效。假设JDK 8和JDK 17都放在/usr/local/java下:
ls -l /usr/local/java/jdk8 /usr/local/java/jdk17你只需要保证应用启动脚本里JAVA_HOME变量指向对应的软链接,就能在多个版本间切换。这种做法的好处是JDK目录本身不动,只是切换入口,回滚很容易。
4. 部署jar包的完整实操流程
环境好了,接下来才是重头戏:把jar包发到服务器上并跑起来。
4.1 从本地打包到上传服务器
本地如果是Maven项目,打包命令一般是:
mvn clean package -DskipTests打包完成后,在target目录下会生成一个xxx.jar文件。如果是Spring Boot项目,这个jar是一个可执行jar,内部自带Tomcat等内嵌容器,不需要再装额外的应用服务器。
上传方式我用得最多的有三种:
# 方式一:scp(Linux/Mac本机直接传) scp target/demo-0.0.1-SNAPSHOT.jar root@你的服务器IP:/opt/app/ # 方式二:rz上传(Xshell、SecureCRT里常用,需要先安装lrzsz) yum install -y lrzsz rz # 方式三:在Windows下用WinSCP或FinalShell这类图形工具直接拖拽小文件用rz最顺手,几百MB以上的大包建议用scp或者图形工具,rz在4G以上文件经常不稳。上传完记得用ls -lh看一下文件大小,确认完整。
4.2 看懂jar包内部结构,才能明白“lib中是什么”
总有人问“jar包的lib中是什么”。这里统一回答:Spring Boot可执行jar里的BOOT-INF/lib目录存放的是项目所有依赖的jar包,包括各种第三方库,比如Spring MVC、Jackson、数据库驱动等。BOOT-INF/classes目录存放的是项目自身编译后的class文件和配置文件。META-INF/MANIFEST.MF文件则记录了程序入口,也就是Main-Class。
我一般用jar tf查看包内容:
jar tf demo-0.0.1-SNAPSHOT.jar | head -20如果看到BOOT-INF/、META-INF/、org/springframework/boot/loader/这些路径,说明它是一个标准的Spring Boot fat jar。这种jar之所以能直接java -jar运行,关键就在于MANIFEST.MF里指定了启动类,JarFile会在启动时从嵌套的jar中加载类。所以不要试图把BOOT-INF/lib下那些jar解压出来放到classpath里,它们本身就是被组织在fat jar内层的。
如果你是用普通Maven打包,没有配置spring-boot-maven-plugin,那么打出来的jar会是一个普通jar,运行时会提示“no main manifest attribute”,这时需要在pom.xml里配置mainClass,或者改用它对应的上层工具来启动。
4.3 nohup后台启动与日志管理的正确姿势
最常见、也最容易演示的后台运行方式是用nohup配合&:
cd /opt/app nohup java -jar demo-0.0.1-SNAPSHOT.jar --server.port=8080 > /var/log/app/demo-app.log 2>&1 &解释一下这个命令:
nohup让命令忽略挂断信号,保证SSH断开后进程继续跑。&把进程放到后台执行。> /var/log/app/demo-app.log 2>&1把标准输出和错误输出都重定向到指定日志文件。如果不加这个,日志默认写到nohup.out,久了容易乱。
启动后第一件事就是确认进程状态和日志输出:
# 查看Java进程 ps -ef | grep java # 看看端口有没有监听 ss -lntp | grep 8080 # 滚动查看日志,此时能看到Spring Boot启动横幅和端口号 tail -f /var/log/app/demo-app.log这一步我重点强调:先看日志,再看进程。日志显示“Started DemoApplication in xx seconds”,才是真的起来了。如果日志没动静,或者出现Exception,那么就算有java进程也说明有问题,要立刻去查。
另外,很多人问怎么让进程名字看起来有辨识度,因为用java -jar启动后ps看到的进程都是java,根本无法区分是哪个应用。我的做法是在启动脚本里借助exec -a指定一个任务名:
bash -c 'exec -a demo-app /usr/local/java/jdk17/bin/java -jar demo-0.0.1-SNAPSHOT.jar'执行后在ps -ef里进程名就会变成demo-app。不过这种方式的稳定性和兼容性在不同系统上有差异,我更推荐直接结合systemd管理,后面会讲。
5. 常见问题排查与运维提升
部署过程中遇到问题是常态,我这里把出现频率最高的几类问题整理出来,可以直接对照排查。
5.1 jar包启动失败的几种典型原因
下表是我在实际排障中反复遇到的,基本可以覆盖90%的启动失败场景:
| 现象 | 可能原因 | 排查命令或思路 |
|---|---|---|
| 提示 no main manifest attribute | jar不是Spring Boot可执行jar | 用jar tf查看META-INF/MANIFEST.MF,检查打包配置 |
| 端口被占用 | 另一个进程占用了相同端口 | ss -lntp | grep 端口号,改端口或杀旧进程 |
| ClassNotFoundException | JDK版本不对或依赖缺失 | 确认用对了JDK版本,临时用-d参数看详细启动日志 |
| 内存不足 | 服务器内存太小或JVM参数给得太高 | free -h查看内存,调整-Xmx参数 |
| 启动后马上退出 | 数据库连不上、配置文件缺失 | 看tail -100 日志文件,重点搜Caused by |
| 提示 JDK版本错误 | 用了Java 8跑需要Java 17的应用 | 切换JAVA_HOME或直接用绝对路径JDK的java命令 |
“启动后马上退出”是最常见的坑。很多新手只看到进程没了,没看到日志,就手足无措。我提醒一下:先别急着启动第二次,先把日志文件翻出来看最后一百行,通常真正的报错线索都在Caused by那一行。
5.2 用systemd把Java应用变成系统服务
比nohup更可靠的生产级做法是写一个systemd服务,让应用开机自启、崩溃自动拉起、还可以统一查看日志。项目上线后我首推这种方式。
新建一个服务文件:
vim /etc/systemd/system/demo-app.service内容如下:
[Unit] Description=Demo Spring Boot Application After=network.target [Service] User=root WorkingDirectory=/opt/app ExecStart=/usr/local/java/jdk17/bin/java -jar /opt/app/demo-0.0.1-SNAPSHOT.jar --server.port=8080 SuccessExitStatus=143 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target然后执行:
systemctl daemon-reload systemctl enable demo-app systemctl start demo-app systemctl status demo-appExecStart里直接写JDK绝对路径,这就又一次落实了“指定JDK运行”的需求。这里的SuccessExitStatus=143是为了兼容Spring Boot优雅停机,普通应用可以根据需要酌情配置。日志查看可以用:
journalctl -u demo-app -f好处是统一管理,不用自己手动写日志重定向,systemd会自动捕获标准输出。缺点是如果你有特殊日志切割需求,可能还要额外配logrotate。
5.3 几个对面试和日常运维都有用的细节
最后补充几个容易被忽视但很重要的点:
第一个是关于JVM参数。启动jar包时,内存参数是继JDK版本之后最值得关心的项。比如一个常见的启动命令:
nohup java -Xms256m -Xmx1024m -jar demo.jar > startup.log 2>&1 &-Xmx指定堆上限,-Xms指定初始堆大小。服务器2G内存就别把-Xmx顶到2G,否则系统可能不断发生内存交换。观察可用内存用free -h,观察进程实际占用的物理内存用ps -o pid,rss,cmd -p PID。
第二点是关于kill进程。kill -9是最后的手段,直接强杀可能会导致数据不一致或端口未释放。更规范的做法是先试kill PID,等待几秒,不行再kill -9。线上环境尽量做好优雅停机,配合systemctl stop更好。
第三点是JDK降级问题。热词里有“jdk降级到17”,其实更常见的是从JDK 17降级到JDK 8。这类需求通常是因为某个老应用在本地用高版本编译或运行,但服务器上没有对应版本。切记:版本切换时,贴好JAVA_HOME路径,别贪图方便用软链接乱指,不然会出现你现在以为用的是17,实际进程跑起来用的却是默认8的尴尬局面。
我在实际操作中体会最深的一点是:无论环境多乱,只要两个东西抓住,Java应用就能稳定跑起来。一个是JAVA_HOME到底指到了哪个目录,另一个是启动日志谁在看、怎么看。很多所谓的“怪问题”,最后都不过是这两个基本盘出了问题。如果你第一次部署,可以先把JDK绝对路径和日志文件背后这套逻辑跑通,再往上加systemd、监控都不迟。