"我明明装了 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、服务进程,逐层核对,别靠猜。
企业QQ咨询




