生产环境JRE 11离线部署:JDK与JRE区别及Java运行时环境配置指南 简介针对服务器资源紧张、又需要运行Java 11应用的场景这份精简版JRE11值得使用。它从JDK11中提取而来解压后约158MB打包压缩后仅56.27MB相比完整JDK节省大量磁盘占用可直接部署到生产环境。压缩包内含350个文件以dll、exe运行组件为主同时包含license与copyright许可信息、policy安全策略、properties配置文件以及h头文件和md说明文档等。核心模块modules、安全证书cacerts等一应俱全满足JVM启动和类加载要求。目前已有4215人学习下载适合运维人员、后端开发者在资源受限服务器或容器中快速搭建Java11运行环境。相比自行处理JDK模块这个JRE包免去额外裁剪步骤解压即用。同时附带标准许可与配置模板便于在离线或内网环境批量交付。若服务器磁盘空间告急安装这一份即可无需再为完整JDK买单。1. 服务器缺的不是JDK是一个干净的JRE 11.0.10有一次给某公司内部的发布脚本做升级构建机已经用 java11 编译好应用部署到服务器上启动时直接抛 UnsupportedClassVersionError。排查了半天才发现那台服务器上的 java 命令还是老版本运行时而且它被系统包管理器托管随便替换会把其他服务一起带崩。这大概是“服务器配置 jre 包”场景里最典型的开场应用升级了运行环境没跟上而生产服务器又不允许动全局配置。与其装一整套 JDK把编译器、调试器这些用不到的工具也铺上去不如放一个解压即用的 JRE 11.0.10 离线包。这份资源解决的就是“在无外网环境里用固定版本 java11 运行时把应用可靠拉起”这件事适合维护 Java 应用服务器、写部署脚本以及对环境变量有切肤之痛的人直接照着做。2. 为什么是 JRE 11.0.10模块化、LTS 与生产环境的裁剪逻辑很多人拿到这个包的第一反应是为什么不直接下 JDK功能全、省事装完一个顶所有。这个想法在开发机上是合理的换到服务器上就不一定划算了。服务器要的是把构建好的 class 跑起来不是提供一个完整的编译调试环境。动手之前先把 JDK 和 JRE 的边界讲清楚再给出三个必须做的检查免得后面把系统环境搅乱。2.1 JDK 与 JRE 的分工为什么生产环境可以只装 JRE一套完整的 JDK里面至少包含三块东西编译器 javac、打包与调试工具jar、javadoc、jdb 等以及 Java 运行时。JRE 去掉前面两块只留下 java 命令和标准类库。对绝大多数部署场景来说发布物就是一个 jar 或一组 class 文件服务器上唯一需要的就是“能把它加载并执行起来”的运行时javac 根本不会被用到。Java 9 模块化改造之后还有一个容易被忽略的变化。在那之前JDK 安装完JRE 作为 JDK 目录下的一个子目录跟着出现很多人的习惯是直接把整个 JDK 丢到服务器上然后只调用其中的 java。模块化之后独立运行包成为正式的发布形态JRE 可以单独下载、单独部署不再需要从 JDK 里抠目录。这让“服务器只装 JRE”从一种临时做法变成了被官方支持的部署方式。版本选择上11 属于长期支持版本近些年大量主流服务框架把运行时基线从 8 往上抬11 成为生产环境最常见的选择之一。而 11.0.10 作为 11 序列里的一个补丁版本固定它是有实际意义的同一个“Java 11”不同补丁版本之间的安全修复、垃圾回收器行为细节和类库兼容性并不完全相同。安全基线要求高的环境不能接受“反正是 Java 11”这种模糊说法。包管理器的软件源版本往往跟随发行版节奏不一定正好是你要的补丁版本离线包反而能把版本钉死。不过 JRE 不是万能的。如果业务里还有 JSP 动态编译、在运行时用动态字节码生成的框架、或者干脆需要执行 javac 来生成代码那这些路径依赖 JDK 里才有的编译能力。这种应用要是只装 JRE表现会很诡异应用能正常启动等到第一次触发编译流程时才抛异常日志里全是类找不到或者编译失败。判断标准可以简化成一句发布物是纯编译好的 class 或 jar业务侧没有动态编译需求JRE 就够用有这种需求老实装 JDK。2.2 动手前先回答三个问题版本、路径、共存先看服务器上现有的 java 命令是什么来路。很多发行版默认带了一套老版本运行时而且 /usr/bin/java 往往不是真实路径而是一条符号链接链最终指向包管理器维护的版本目录。动手之前先把这三条命令跑一遍。# 列出 PATH 里能解析到的所有 java确认有没有多个候选 which -a java 2/dev/null # 把 /usr/bin/java 的符号链接链捋直看清真实指向 readlink -f $(which java) 2/dev/null # 打印版本信息java 版本输出走的是 stderr手动重定向到 stdout 才不会被吞 java -version 21 | head -n 1逻辑说明第一行用于发现隐藏的多个 Java 副本如果系统里装了两套运行时这里会全部暴露第二行负责把“用户看到的 java 命令”和“真实文件”之间的符号链接解包方便判断它是不是包管理器托管的状态第三行加 21 是因为 JDK 系命令把版本信息打印到错误输出不重定向的话在脚本里取版本号会取不到内容。参数说明readlink -f 会递归解析所有中间链接比 readlink 不带参数只读一步更可靠head -n 1 只要第一行防止有些环境在版本号后面继续打印版权信息。回答这三个问题之后再决定怎么做。第一个问题是版本线上应用要求 Java 11那运行端就必须是 11.x 的某个具体补丁不要靠“系统里凑近的版本”凑合。第二个问题是用不用发行版包管理器包管理器装 Java 方便但它接管了目录结构和版本节奏补丁版本不一定由你决定想要锁定 11.0.10 的公司安全基线离线包是把版本主权拿回自己手里的选择。第三个问题是共存如果这台服务器上还跑着需要旧版 Java 的老应用别动系统级符号链接让每个应用在各自脚本里 export 自己的 JAVA_HOME新老运行环境互不干扰。把这三件事确认完“解压离线包 应用级环境变量”这个方案就已经确定了接下来进入具体安装。3. 把 JRE 11.0.10 装进服务器从解压到 JAVA_HOME 的完整操作3.1 目录规划与解压服务器的 Java 运行时我一般统一放在 /opt/java 下面一个版本一个目录目录名带版本号。这样做的好处是升级时新增一个目录回滚时改一个软链接不用把正在跑的应用目录拆得七零八落。下面这组命令完成规划、解压和重命名。# 建目录统一放所有 Java 运行时 mkdir -p /opt/java # 解压 zip 格式的独立运行包到目标目录如果拿到的是 tar.gz换成 tar -zxvf 包名 -C /opt/java unzip -q jre-11.0.10.zip -d /opt/java # 有些独立包解压出来的目录名是带其他前缀的版本目录统一改名为干净的 jre-11.0.10脚本里路径才好写 if [ -d /opt/java/jdk-11.0.10 ]; then mv /opt/java/jdk-11.0.10 /opt/java/jre-11.0.10 fi # 直接用绝对路径验证暂时不依赖任何环境变量 /opt/java/jre-11.0.10/bin/java -version逻辑说明unzip 的 -d 参数把解压目标指向 /opt/java避免先解压到当前目录再移动重命名是为了让后续所有脚本、unit 文件都引用稳定路径。最后一条故意用绝对路径调用 java是为了把“包本身是否可用”和“环境变量是否配好”两件事分开如果这一步输出版本号而后面应用找不到 java问题就锁定在环境变量层而不是包坏了。参数说明-q 表示安静模式避免解压时刷屏tar 场景下 -z 表示 gzip 压缩、-x 表示解压、-v 打印解压过程、-f 指定压缩包文件名四个参数是处理这类压缩包的固定组合。重命名这里有一个值得养成的习惯每次升级版本目录名里的版本号都会变。我一般会额外建一个 /opt/java/current 软链接指向当前使用的版本目录应用统一引用 current这样以后升级只要改软链接后面的 JAVA_HOME、systemd 文件、cron 脚本全部不用动。接下来配置环境变量重点在于分清作用域。3.2 环境变量配置的三种作用域很多人配置 JAVA_HOME 的第一反应是改 /etc/profile但实际部署里这个文件只对交互式登录 Shell 生效。SSH 非登录执行、cron 定时任务、systemd 服务都不会读它。把环境变量放错作用域是“手动敲命令一切正常服务一启动就找不到 java”的头号原因。交互式登录环境配置方式是放在 /etc/profile.d 下面建一个独立脚本。登录时脚本会自动被 source好处是不污染主 profile 文件升级时删掉一个文件就能整体移除配置。# /etc/profile.d/java11.sh export JAVA_HOME/opt/java/jre-11.0.10 export PATH$JAVA_HOME/bin:$PATH逻辑说明export 让 JAVA_HOME 和 PATH 对后续所有子进程可见PATH 把 JAVA_HOME/bin 放在最前面这样当前 Shell 解析 java 命令时会优先命中这个 JRE而不是系统默认位置。参数说明注意 JAVA_HOME 用的是 /opt/java/jre-11.0.10 这个具体版本路径。如果用了 /opt/java/current 软链接这个文件在版本升级时就不用再改。systemd 服务是另一个常见场景它默认不读 /etc/profileunit 文件里必须自己声明环境变量。[Service] # systemd 进程不继承登录 Shell 环境必须显式给运行时路径 EnvironmentJAVA_HOME/opt/java/jre-11.0.10 EnvironmentPATH/opt/java/jre-11.0.10/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin ExecStart/opt/java/jre-11.0.10/bin/java -jar /srv/app/app.jar Restarton-failure RestartSec5逻辑说明两行 Environment 会作为服务进程的私有环境注入只影响当前服务不影响系统里其他应用这是多版本共存的基础ExecStart 直接使用绝对路径避免依赖 PATH 解析顺序的差异。参数说明Restarton-failure 表示进程非正常退出时自动拉起RestartSec5 是重启前等待 5 秒防止进程崩溃后进入快速重启死循环。cron 定时任务的环境比 systemd 还精简连基础的 PATH 都是最小集。不要在 crontab 里依赖 profile正确写法是脚本开头自己 export。#!/bin/bash # cron 环境不读 /etc/profile脚本开头必须自己把 JAVA_HOME 带进来 export JAVA_HOME/opt/java/jre-11.0.10 export PATH$JAVA_HOME/bin:$PATH /opt/java/jre-11.0.10/bin/java -jar /srv/app/report-job.jar逻辑说明如果省掉前两行cron 执行时会去系统最小 PATH 里找 java结果大概率命中旧版本或者直接 command not found显式 export 后脚本内部和它启动的子进程都能拿到正确的运行时。三种作用域的差异用一个表总结场景配置位置生效范围常见翻车点交互登录 Shell/etc/profile.d/java11.sh登录后的交互环境su - 也能读到su 不加 - 不读 profile部分 SSH 客户端不触发登录 Shellsystemd 服务unit 里的 Environment 或 EnvironmentFile仅该服务进程systemd 不读 profile漏配会直接 command not foundcron 定时任务脚本内 export仅该脚本进程cron 环境是精简集不继承用户 Shell 配置3.3 最小化启动验证与典型参数环境变量配好之后用一个最小启动脚本把应用拉起来顺便把生产环境常用的启动参数一并讲清楚。#!/bin/bash # start.sh 生产环境最小启动脚本 JAVA_HOME/opt/java/jre-11.0.10 export JAVA_HOME cd /srv/app exec $JAVA_HOME/bin/java \ -Djava.awt.headlesstrue \ -Xms512m \ -Xmx1024m \ -jar app.jar逻辑说明JAVA_HOME 先赋值再 export脚本内部和 java 进程都能读到cd 到应用目录是为了让应用内部的相对路径配置生效exec 会用 java 进程替换掉当前 Shell 进程这样 systemd 和监控系统看到的 PID 就是 Java 进程本身的 PID而不是外层 Shell 的 PID。参数说明-Djava.awt.headlesstrue 告诉 Java 运行在无头模式不尝试连接显示器报表、图片渲染类组件在服务器上依赖这个参数-Xms512m 设置堆初始大小-Xmx1024m 设置堆最大大小。小内存服务器上不要把两者设成同一个值启动即占满会让同一个宿主机上的其他服务很难受。启动之后再用两个命令确认进程状态这一步很多人会跳过结果后面出问题才回头查# 确认进程实际使用的 java 路径和启动参数 ps -ef | grep java | grep -v grep # 确认环境变量在应用进程里被正确注入 cat /proc/$(pgrep -f app.jar | head -n 1)/environ | tr \0 \n | grep JAVA_HOME逻辑说明第一行看进程的完整命令行确认里面是 /opt/java/jre-11.0.10/bin/java 而不是系统默认位置第二行直接读进程的环境变量列表这是确认 systemd 或脚本注入是否生效的最可靠方法。参数说明pgrep -f app.jar 按完整命令行匹配进程名tr 把环境变量之间的分隔符换成换行方便阅读如果 grep 不到 JAVA_HOME说明环境变量作用域配错了优先检查 unit 文件或脚本的 export 位置。注意JRE 的 bin 目录里只有 java 和少量运行工具jcmd、jmap、jstack 这些都是 JDK 才有的。拿到 JRE 后不要按 JDK 的记忆去找这些命令进程诊断要用系统层的排查手段。到这里安装部分已经全部闭环包解压了环境变量配了应用拉起来了进程也验证了。但部署过 Java 应用的人都知道真正让人熬夜的不是安装步骤而是那些“怎么想都想不通”的运行时问题下面专门拆这些现场。4. 避坑与排查版本漂移、PATH 覆盖、字体与内存的五个现场下面的内容全部来自实际部署中踩过的坑。每一节都是一个独立的小现场按“现象、原因、解决”三条线写遇到相似问题可以直接对照。4.1 装完新 JREjava -version 还是旧版本现象包解压了profile.d 文件写了重新登录后执行 java -version输出的还是老版本号甚至自己都不确定这版 java 是哪儿来的。原因PATH 里旧路径排在 JAVA_HOME/bin 前面系统解析 java 时先命中旧位置或者 /usr/bin/java 的符号链接链还指向包管理器托管的旧版本而 type 命令解析到的正是这条链接。还有一个容易被忽略的因素是 Shell 的路径缓存登录 Shell 记住过旧路径后不清理缓存就会一直用旧解析结果。解决先跑命令看清现状。# 列出所有候选路径确认系统解析到底走了哪一条 which -a java # 看 PATH 里实际生效顺序JAVA_HOME/bin 是否排在最前 echo $PATH # 清掉当前 Shell 的路径缓存让重新登录后的解析结果生效 hash -r逻辑说明第一行把所有能解析到的 java 全列出来第二行看 PATH 顺序第三行清理缓存。如果确认是系统包管理器托管的符号链接不要去改系统级链接链——那会全局替换影响其他还依赖旧版的服务。正确做法是让应用脚本或 unit 文件用绝对路径调用 $JAVA_HOME/bin/java绕过系统解析。4.2 JAVA_HOME 指向一个不存在的目录现象应用能启动但日志里打出来的 JAVA_HOME 是一个不存在的路径比如升级前残留的 /opt/java/jre-11.0.9当前目录树里已经没有这个目录。原因脚本或 systemd 文件里写死了旧的版本路径。升级时新增了新版本目录但引用旧路径的配置文件没有被同步更新结果应用启动时要么找不到 java要么靠 PATH 兜底找到一个错误的运行时。解决建立 /opt/java/current 软链接指向当前版本应用脚本和 unit 文件统一引用这个稳定路径。升级流程固定为解压新版本目录、验证、改 current 软链接目标、重启应用。从那以后脚本里永远不会出现具体版本号也就不会出现“目录已经删了配置还在”的状态。4.3 UnsupportedClassVersionError现象应用的 class 文件明明是从 Java 11 环境编译出来的服务器上装的也是 JRE 11启动却报 UnsupportedClassVersionError。原因版本号对不上。所谓“服务器上装的也是 JRE 11”可能只是心理预期实际被进程使用的 java 来自另一个目录。class 文件的主版本号是硬约束52 对应 Java 855 对应 Java 1161 对应 Java 17。编译端和运行端的版本要精确匹配到这一层。解决先用 ps -ef | grep java 看进程实际使用的 java 路径这比相信环境变量可靠得多再用 javap 看编译产物版本。# 查看 class 文件主版本号major55 对应 Java 11major52 对应 Java 8 javap -verbose /srv/app/classes/com/example/Main.class | grep major逻辑说明javap 是 JDK 自带的类文件反汇编工具-verbose 输出完整类信息grep major 只留下版本行。多数情况下问题出在脚本里还有一条写死的老路径优先检查 unit 文件和启动脚本里的 ExecStart 行。4.4 无头服务器上字体缺失导致渲染失败现象应用启动正常功能入口也能点但一到生成验证码图片、图表、PDF 导出这类环节就抛 FontConfiguration 相关异常或者图片里的中文全部变成方框。原因JRE 渲染文本时依赖系统的字体库和 fontconfig 配置。很多服务器为了精简没装中文字体有的连基本字体都没有。JRE 本身不携带完整的字体集找不到可用字体时就把渲染任务失败掉。解决在服务器上安装系统字体包并确认字体目录能被 fontconfig 搜索到对纯后台服务启动参数加上 -Djava.awt.headlesstrue 让 Java 不尝试连接显示设备。这个坑和 JRE 包本身无关但每次换新环境部署报表类应用我都会先确认字体再启动顺序反了会白白排错半小时。4.5 内存参数没生效应用还是 OOM现象启动脚本里写了 -Xmx1024m容器或宿主机的限制是 4G应用运行时还是频繁 OOM有时启动阶段直接失败。原因-Xmx 只是 Java 堆的上限堆外的元空间、线程栈、JRE 自身和本地内存都不受它管辖。Java 11 的容器感知默认开启JVM 会读取容器限制来计算默认堆大小如果你只给了 -Xmx 而没有管理堆外部分堆和系统限制之间就没有清晰边界两者一起用满就会 OOM。解决启动时加 -Xlog:gc 看实际 GC 行为和堆占用在容器化部署里用 -XX:MaxRAMPercentage70.0 这类按容器内存比例计算堆的参数而不是手工拍脑袋定 -Xmx小内存服务器上再补一个 -XX:MaxMetaspaceSize把动态生成类带来的元空间增长也框起来。内存参数是 Java 应用里最接近玄学的部分但把日志打开之后每一步都有迹可循。5. 进阶把 JRE 11.0.10 做成可控的运行时基底5.1 jlink 的边界这个包不能用来做二次裁剪很多人在服务器上装完 JRE 后下一个念头是用 jlink 做一个更小的自定义运行时。这里明确一下边界jlink 命令在 JDK 的 bin 目录里独立 JRE 包里没有它。拿着这份 JRE 做不了模块裁剪要做就得另外装一套 JDK用 jlink 把需要的模块输出到新目录。所以这份 JRE 的正确定位是“应用运行基底”不是用来做二次加工的原料。有裁剪需求时另外准备完整的 JDK 构建环境。5.2 多版本隔离的 systemd 写法一个宿主机上同时跑 Java 8 和 Java 11 的应用是常态。全局 PATH 不可能同时满足两边我的做法是把每个应用拆成独立的 systemd unit每个 unit 显式声明自己的运行时路径应用之间完全隔离。版本目录加 current 软链接的组合让 SSH 上去排查的人一眼就能确认当前主用版本是谁。[Service] WorkingDirectory/srv/app EnvironmentJAVA_HOME/opt/java/current EnvironmentPATH/opt/java/current/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin ExecStart/opt/java/current/bin/java -Djava.awt.headlesstrue -Xms512m -Xmx1024m -jar app.jar Restarton-failure RestartSec5逻辑说明WorkingDirectory 指定工作目录应用内相对路径配置以它为基础JAVA_HOME 和 PATH 指向 /opt/java/current升级版本时只需要改软链接unit 文件本身不动。参数说明Environment 行注入的 PATH 只对当前服务有效其他服务不会感知Restarton-failure 配合 RestartSec5让应用崩溃后自动拉起同时用 5 秒间隔防止快速重启。5.3 部署前强制走一遍的验证清单这份 JRE 11.0.10 的离线包我习惯下载后放在内部制品库里留存新服务器初始化时直接取用。下面是每次上线前必跑的验证清单它不能覆盖所有问题但能把版本、路径、环境变量、进程这几个最容易出事的环节全部过一遍。检查项命令期望结果运行时版本/opt/java/current/bin/java -version输出 11.0.10JAVA_HOME 解析echo $JAVA_HOME/opt/java/current命令优先级which java/opt/java/current/bin/java服务进程ps -efgrep java健康检查curl -s http://localhost:端口/health返回 HTTP 200这张表的顺序是有讲究的先验证包本身再验证环境变量再看进程最后走业务健康检查。任何一步不过都能迅速定位到具体层面而不是从头开始排查。从那以后我每次给服务器配 Java 应用环境都强制自己走一遍这张表解压、重命名、配环境变量、拉进程、跑健康检查一个都不能少。版本漂移和路径指向的坑再也没有在半夜找过我。希望帮到你。本文还有配套的精品资源点击获取