Linux服务器如何配置环境变量?PATH、JAVA_HOME与持久化设置

"我明明装了 Java,为什么 java -version 说 command not found?""在终端里 echo $JAVA_HOME 有值,为什么 systemd 启动的服务读不到?""为什么我 export 的变量重开一个终端就没了?"——这些问题的根源都是环境变量的作用范围和持久化机制没搞清。环境变量看起来简单,却是 Linux 上最容易让人困惑、也最容易埋坑的知识点之一。

这篇文章会从"环境变量是什么、分几类"讲起,重点讲清楚 PATH、JAVA_HOME、LANG 这类常见变量的作用,然后详细说明几种持久化方式的区别:/etc/profile、/etc/profile.d/、~/.bashrc、~/.bash_profile、/etc/environment,以及最容易被忽略的——systemd 服务如何拿到环境变量。

读完你应该能做到:知道该把变量写在哪个文件里,分清楚登录式 shell 和非登录式 shell 加载哪些文件,让交互式终端和后台服务都能正确读到变量,并且不踩"改了不生效""只有 root 生效"这些常见坑。

一、环境变量基础与作用范围

环境变量是一组"键=值"的变量,进程可以通过它读取配置。它有几个关键特性要记住:

  • 继承性:子进程会继承父进程的环境变量。你在 shell 里 export 的变量,从这里启动的程序都能读到。
  • 方向性:变量只能从父到子传递,子进程改不了父进程的环境。所以在一个脚本里 export 的变量,脚本跑完就没了。
  • 会话性:没有持久化的话,变量只在当前会话有效,关掉终端就消失。
  • 作用范围:分"当前 shell 的局部变量"和"导出的环境变量"。只有 export 过的才会传给子进程。
# 查看所有环境变量
env
printenv
printenv PATH          # 看单个

# 设置变量(只对当前 shell 有效,不传给子进程)
FOO=bar
echo $FOO

# 导出为环境变量(子进程可见)
export FOO=bar
bash -c 'echo $FOO'     # 子 shell 里能读到

# 查看某个变量是否已导出
export -p | grep FOO

# 临时为单条命令设置变量(只对该命令生效)
JAVA_HOME=/usr/lib/jvm/java-17 FOO=bar ./myscript.sh

# 删除变量
unset FOO

理解"是否 export"很关键:很多"脚本里明明设了变量,运行时却读不到"的问题,就是因为忘了 export。

二、PATH:命令查找路径

PATH 是最重要的环境变量,决定了你敲一个命令时,shell 去哪找这个可执行文件。

# 查看当前 PATH,用冒号分隔
echo $PATH
# 例:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# 查看某个命令的实际位置
which java
type -a python3
command -v nginx

shell 会按 PATH 里从左到右的顺序找,找到第一个匹配的就用。所以顺序很重要:如果你把某个目录放在前面,里面的同名程序会覆盖后面的。这也是"装了新版本但 java -version 还是旧版本"的常见原因——旧路径排在前面。

临时修改和永久修改:

# 临时:把目录加到 PATH 最前面(当前会话有效)
export PATH=/opt/myapp/bin:$PATH

# 追加到末尾(优先级最低)
export PATH=$PATH:/opt/myapp/bin

# 检查是否重复添加(避免反复 source 导致 PATH 越来越长)
echo $PATH | tr ':' '\n' | grep -c '/opt/myapp/bin'

一个实用技巧:source 脚本时反复追加会导致 PATH 里同一个目录出现很多次。可以在追加前判断:

case ":$PATH:" in
  *":/opt/myapp/bin:"*) ;;
  *) export PATH="/opt/myapp/bin:$PATH" ;;
esac

三、持久化的几种方式与区别

临时 export 只在当前会话有效。要持久化,就得写进 shell 启动时加载的文件。这里最大的坑是:不同文件在不同场景下加载,写错地方就会出现"某些情况下生效、某些情况下不生效"。

先看 Bash 加载文件的规则:

文件 加载时机 典型用途
/etc/profile 登录式 shell 启动时(全局) 全局环境变量
/etc/profile.d/*.sh 被 /etc/profile 调用(全局) 推荐的全局变量存放处
/etc/bashrc 或 /etc/bash.bashrc 交互式 shell 全局别名、提示符
/etc/environment PAM 级别,登录时由系统读取 简单 KEY=VALUE,无 shell 语法
~/.bash_profile 用户登录式 shell 用户级登录变量
~/.bashrc 用户交互式非登录 shell 别名、函数、每开一个终端都加载
~/.profile 登录式 shell(无 bash_profile 时) 用户级登录变量

关键区分:

  • 登录式 shell(login shell):比如 SSH 登录、su -、在 tty 上登录。会加载 /etc/profile 及 ~/.bash_profile(或 ~/.profile)。
  • 非登录式交互式 shell:比如在图形终端里新开一个标签、或 bash 命令进入的 shell。会加载 ~/.bashrc。
  • 非交互式 shell:比如 ssh host 'command' 执行的命令、cron 任务。通常不加载上述任何 profile 文件,这就是为什么 cron 里读不到你手动设的变量。

针对不同需求选择位置:

需求 推荐位置
全系统所有用户都要的变量 /etc/profile.d/xxx.sh
只有某个用户要,且所有登录场景 ~/.bash_profile(并 source 一下 ~/.bashrc)
只有某个用户,且每个交互终端都要 ~/.bashrc
不依赖 shell 解析的简单变量 /etc/environment
后台服务(systemd)要的变量 服务单元 Environment= 或 EnvironmentFile=

推荐做法:全局的就新建 /etc/profile.d/myapp.sh,而不是直接改 /etc/profile。这样清晰、可维护、卸载时删文件即可。

# 新建全局环境变量文件(注意:文件名以 .sh 结尾才会被加载)
sudo vi /etc/profile.d/myapp.sh
# /etc/profile.d/myapp.sh 内容示例
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export PATH=$JAVA_HOME/bin:$PATH
export APP_HOME=/opt/myapp
export LANG=en_US.UTF-8
# 让当前会话立即生效(新开的 shell 会自动加载)
source /etc/profile.d/myapp.sh
# 或
. /etc/profile

注意:/etc/profile.d/ 下的文件需要被 /etc/profile 调用才会加载,主流发行版默认都有这个机制。Debian 系一般没有 /etc/profile.d 的调用是先天的?实际上 Debian 的 /etc/profile 也会遍历 /etc/profile.d/*.sh,但如果你发现没生效,检查一下 /etc/profile 是否有对应循环。

/etc/environment 的写法要注意:它不是 shell 脚本,不支持 $PATH 这种展开,只能写死值:

# /etc/environment(由 PAM 读取,无 export、无变量展开)
JAVA_HOME=/usr/lib/jvm/java-17-openjdk
LANG=en_US.UTF-8

如果你在里面写 PATH=$PATH:/opt/bin,那个 $PATH 不会被展开,是无效的。这是经典坑。

四、JAVA_HOME 与常见语言变量

JAVA_HOME 是很多 Java 应用和中间件启动脚本依赖的变量,配错会导致"找不到 Java"或"用了错误版本"。

# 1. 先确认系统装了哪些 Java、路径在哪
ls -l /usr/lib/jvm/
update-alternatives --list java    # Debian/Ubuntu
alternatives --list                # RHEL 系(列出所有 alternatives)
readlink -f $(which java)          # 反查 java 实际指向

# 2. 安装 JDK(如果还没装)
# RHEL / Rocky / Alma
sudo dnf install -y java-17-openjdk-devel
# Debian / Ubuntu
sudo apt install -y openjdk-17-jdk

# 3. 设置 JAVA_HOME 到 profile.d
sudo tee /etc/profile.d/java.sh >/dev/null <<'EOF'
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
export PATH=$JAVA_HOME/bin:$PATH
EOF
sudo chmod +x /etc/profile.d/java.sh

# 4. 生效并验证
source /etc/profile.d/java.sh
echo $JAVA_HOME
java -version
javac -version

实际路径因发行版和版本而异:Debian/Ubuntu 常是 /usr/lib/jvm/java-17-openjdk-amd64,RHEL 系常是 /usr/lib/jvm/java-17-openjdk-17.x.x...。用 readlink -f $(which java) 或列 /usr/lib/jvm 确认实际路径,别照抄。

其他常见变量:

# 多版本 Java 切换(Debian/Ubuntu)
sudo update-alternatives --config java
sudo update-alternatives --config javac

# Python/Node 相关
export PYTHONPATH=/opt/myapp/lib
export NODE_PATH=/usr/lib/node_modules

# 数据库客户端字符集
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

五、systemd 服务为什么读不到环境变量

这是最经典的困惑:你在终端里配好了 JAVA_HOME,服务脚本用 systemd 启动,结果报"JAVA_HOME not set"。原因很简单:systemd 不加载 /etc/profile 和任何 shell profile 文件,它有自己独立的环境。

有几种正确的做法:

# 方式一:在服务单元里直接写 Environment=
sudo systemctl edit myapp.service     # 会用 drop-in 覆盖,最安全
# 追加:
# [Service]
# Environment="JAVA_HOME=/usr/lib/jvm/java-17-openjdk"
# Environment="APP_ENV=prod"

# 方式二:用 EnvironmentFile 指向一个变量文件(推荐,便于管理)
sudo vi /etc/sysconfig/myapp     # RHEL 系习惯
# 或 /etc/default/myapp          # Debian 系习惯
# /etc/sysconfig/myapp 或 /etc/default/myapp 内容
JAVA_HOME=/usr/lib/jvm/java-17-openjdk
APP_HOME=/opt/myapp

配套的服务单元:

[Unit]
Description=My Java App
After=network.target

[Service]
Type=simple
EnvironmentFile=/etc/sysconfig/myapp
ExecStart=/usr/lib/jvm/java-17-openjdk/bin/java -jar /opt/myapp/app.jar
User=appuser
Restart=on-failure

[Install]
WantedBy=multi-user.target
# 让 systemd 重新读取并验证服务的环境
sudo systemctl daemon-reload
sudo systemctl restart myapp
sudo systemctl show myapp -p Environment     # 看服务实际拿到的环境
# 或看进程的环境变量
cat /proc/$(pgrep -f app.jar)/environ | tr '\0' '\n'

关键要点:服务单元里的 ExecStart 最好用绝对路径,因为服务运行时不一定有你交互终端里的 PATH。依赖 PATH 找命令是服务启动失败的常见原因之一。

六、常见坑与排查

# 排查"变量有没有生效"的通用步骤
# 1. 当前 shell 有没有
echo $JAVA_HOME
# 2. 子进程能不能继承
bash -c 'echo $JAVA_HOME'
# 3. 登录式 shell 有没有
bash -l -c 'echo $JAVA_HOME'
# 4. 服务进程有没有
sudo systemctl show myapp -p Environment
现象 原因 解决
新终端有值,服务读不到 systemd 不加载 profile 用 Environment=/EnvironmentFile=
重开终端就没了 只 export,没持久化 写入 profile.d 或 ~/.bashrc
改了不生效 没 source,或写错文件 source 或重开终端,确认文件被加载
cron 任务读不到 cron 不加载 profile 在脚本里显式 export,或在 crontab 顶部设变量
su - user 没值 不同用户不同 profile 配全局 profile.d
/etc/environment 里 $PATH 无效 它不是 shell,不展开变量 写死值或换用 profile.d
PATH 越加越长 重复 source 追加前判重
用了错误版本的 java PATH 顺序问题 调整顺序或改 alternatives

一个特别值得记住的点:判断"文件是否被加载",可以在文件里加一句 echo "loaded",然后新开终端看有没有输出;或者用 bash -x 跟踪。别靠猜。

七、把环境变量配置规范起来

在团队协作和批量运维的场景下,环境变量最好按统一规范管理,避免"每台机器配得都不一样",否则很容易出现"新扩容的机器服务起不来"这类问题。

几个建议:全局变量统一放在 /etc/profile.d/ 下的独立文件里,文件名体现用途(比如 java.sh、app.sh),内容保持简短并加注释,避免所有东西都往 /etc/profile 里堆;服务相关的变量统一走 systemd 的 EnvironmentFile,文件放在发行版约定的位置(RHEL 系习惯 /etc/sysconfig/,Debian 系习惯 /etc/default/),这样服务配置和系统配置分离,既清晰又便于版本管理。

如果用了配置管理工具(Ansible、SaltStack 之类),把这些环境变量文件纳入模板统一管理,新机器上线时自动带上正确变量,就能从根上消除"手动漏配"的风险。最后,改完环境变量文件后,别忘了让相关服务执行 systemctl daemon-reload 并重启,同时新开一个登录终端验证交互式场景,两头都确认无误,这次改动才算真正完成。

常见问题

Q1:环境变量到底该写在哪个文件?
全系统共用写 /etc/profile.d/xxx.sh;单用户登录场景写 ~/.bash_profile;每个交互终端都要写 ~/.bashrc;后台服务写 systemd 的 Environment=/EnvironmentFile=。

Q2:为什么 systemd 服务读不到我配的 JAVA_HOME?
因为 systemd 不执行 shell 的 profile 文件。用 Environment= 或 EnvironmentFile= 显式传给服务。

Q3:改了 /etc/profile 为什么当前终端没生效?
profile 只在登录时加载。已打开的终端要手动 source /etc/profile(或新开一个登录终端)才会生效。

Q4:cron 任务里能用环境变量吗?
cron 运行环境很干净,基本不加载 profile。做法是在脚本内显式 export,或在 crontab 顶部定义变量,或用绝对路径。

Q5:PATH 里重复目录太多有什么影响?
会让命令查找变慢(虽然影响通常很小),也可能因为优先级混乱导致用了非预期的程序。追加前做判重能避免。

总结

  • 环境变量有继承性、会话性和作用范围,只有 export 过的才会传给子进程,临时设置关掉终端即失效。
  • PATH 决定命令查找顺序,前面的目录优先;改 PATH 要持久化到 /etc/profile.d/ 或 ~/.bashrc,追加前最好判重。
  • 持久化位置按场景选:全局用 /etc/profile.d/*.sh,用户级用 ~/.bash_profile/~/.bashrc,/etc/environment 只适合写死值且不支持变量展开。
  • 后台服务必须用 systemd 的 Environment= 或 EnvironmentFile= 传变量,且 ExecStart 用绝对路径,不要指望读 shell profile。
  • 排查"变量不生效"要分层确认:当前 shell、子进程、登录式 shell、服务进程,逐层核对,别靠猜。