JDK 11 发布到现在已经不算新了,但每隔一段时间我都会遇到有人问:项目还在 JDK 8,要不要升 11?升了到底能带来什么?说实话,JDK 11 这个版本在 Java 发展史上是一个非常特殊的节点,它既是 Oracle JDK 和开源版 JDK 合并后的第一个长期支持版本,也是很多人从 8 直接跳过的那个版本。如果你现在还在 JDK 8 上观望,这篇文章刚好适合你;如果你已经升到 11,想看看有哪些特性当时没注意到,也可以继续往下翻。
我会从类库、JVM、语法、迁移踩坑这几个维度拆开讲,每个点尽量说清楚“为什么要这么设计”和“真实项目里怎么用”,而不是罗列特性清单。
1. 版本策略与安装配置:为什么 JDK 11 是个分水岭
1.1 许可证与版本号的坑
先聊点不太像技术、但实际影响决策的事。Java 的版本节奏在 JDK 9 之后从“隔几年一个大版本”变成了“每六个月一个功能版本”,也就是说你会看到 9、10、11 一年内连着出。很多人一看 9 和 10 是短命版本就没敢动,直接等 11,这其实是明智的,因为 11 是继 8 之后的第二个长期支持版本(LTS),会有持续数年的安全更新和维护。
但这里有个容易踩的坑:从 11 开始,商用 JDK 发行版的许可协议变了,在生产环境使用需要付费订阅,而开源构建版是免费的。很多公司一开始没搞清楚这个区别,代码里全是 JDK 11 的 API,结果一查 license 发现商用收费,只能临时迁移。我的建议是:如果没有特殊需求,直接用开源版 JDK 构建即可,功能几乎一样,不用担心许可问题。而且要特别注意,JDK 8 里很多被广泛使用的私有 API(比如 sun.misc.Unsafe),在 11 里的访问限制突然严格了,不是说没有,而是默认禁止反射调用,这个问题后面我会专门展开。
1.2 安装配置要点
JDK 11 的安装和 8 大同小异,但有一个细节值得强调:不要直接使用系统包管理器默认源里的 JDK,除非你能确认它的版本号符合预期。实际踩过的场景是,某台服务器上 apt/yum 装出来的 java -version 显示的是 1.8.0_292,但你明明记得自己配过 JAVA_HOME,这就是典型的 PATH 优先级覆盖问题。
正确的做法是在 /etc/profile 或者用户级 .bashrc 里显式导出:
export JAVA_HOME=/opt/jdk-11.0.21 export PATH=$JAVA_HOME/bin:$PATH配置完成后用 which java 和 java -version 双重确认。还有一个容易被忽略的是:如果你同时装了多个版本,Linux 上 /usr/bin/java 可能是一个软链接,即使你 reset 了 PATH,它也可能优先被命中。这时候直接把 /usr/bin/java 替换或调整 update-alternatives 的优先级才是一劳永逸的办法。
2. 日常开发最实用的类库增强
2.1 HttpClient:终于不需要第三方的 HTTP 库了
JDK 11 正式引入了 java.net.http.HttpClient,这对很多小项目来说是一个重大利好。在此之前,Java 标准库里的 HttpURLConnection 老态龙钟,发个 POST 请求要写一大堆样板代码,很多人宁可引入一个大而全的第三方 HTTP 库。现在标准库自带的 HttpClient 支持 HTTP/1.1、HTTP/2、WebSocket,还支持同步和异步两种模式。
异步模式配合 CompleteableFuture 用起来非常舒服:
HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://example.com/api")) .timeout(Duration.ofSeconds(30)) .header("Content-Type", "application/json") .POST(BodyPublishers.ofString("{\"name\":\"test\"}")) .build(); client.sendAsync(request, BodyHandlers.ofString()) .thenAccept(response -> System.out.println(response.body())) .join();这段代码最直观的感受是:没有多余的 try-catch 包装,没有繁琐的连接管理,语义清晰。我建议如果你的项目只是做简单的接口调用,可以直接切换到 HttpClient,减少一个重的第三方依赖;但如果你的项目需要连接池、负载均衡、拦截器链等高级能力,那还是保留成熟的第三方库,没必要硬换。
2.2 String 与文件 API:平时写代码最爽的改动
String 在 JDK 11 里新增了几个小方法,单看都不起眼,合在一起能减少大量工具方法的重复编写。isBlank() 判断字符串是否为空白(包含空串和纯空格),lines() 按行拆分返回 Stream ,strip() 去除首尾空白,repeat(int) 重复字符串。尤其 strip() 和 trim() 的区别值得一说:trim() 只能去除码点小于等于 U+0020 的字符,而 strip() 基于 Character.isWhitespace(),能识别全角空格等 Unicode 空白字符。如果你处理过带全角空格的对账文件,就知道这个差异在实战中有多重要。
文件读写也有了极简方案:
String content = Files.readString(Path.of("data.txt")); Files.writeString(Path.of("out.txt"), "新的内容");就这么两行,省掉了以前 BufferedReader 逐行读、Writer 手动 flush 的繁琐过程。需要注意的是,readString 是一次性读入内存,适合配置文件、小文本等场景,如果处理几个 GB 的日志文件,千万不要这么用,老老实实走流式读取。
2.3 不可变集合的补课
严格来说 List.of()、Set.of()、Map.of() 是 JDK 9 引入的,但在 8 升 11 的项目里,这些 API 往往是大家最先也最容易用错的地方。和 Arrays.asList() 不同,List.of() 返回的集合是不可变的,不能 add 也不能 set,且不允许包含 null 元素;Map.of() 最多支持 10 组键值对,超过要用 Map.ofEntries()。
我见过不止一次线上报 UnsupportedOperationException,原因就是同事把 List.of() 当普通 List 用,后续代码里还做了 remove 操作。所以这里没有太多原理,就是一条:只读集合就用 List.of(),需要后续改动的集合一律用 new ArrayList<>()。
3. 垃圾收集器的两条支线
3.1 ZGC:低停顿 GC 的第一次正式露面
JDK 11 最惊艳的更新无疑是 ZGC,虽然它当时还是实验特性,但设计理念已经非常超前。ZGC 的目标是把垃圾收集的停顿时间控制在 10 毫秒以内,而且这个时间不随堆大小线性增长。传统的 G1 在大堆场景下,Mixed GC 的停顿时间会明显上升,而 ZGC 通过染色指针、读屏障、内存多重映射等技术,把大部分 GC 工作都挪到了和应用线程并发执行,真正需要 STW 的阶段被压缩到极小。
为了理解它的原理,可以做个生活化类比:传统 GC 相当于清扫一个大厅时需要暂停所有人,扫完再让大家继续走;ZGC 则是在大厅里贴满彩色标签,每个人走路时顺带看一眼标签颜色来修正路线,清扫工只处理极少量的收尾工作,大家几乎感觉不到停顿。
在你的 JVM 参数里启用 ZGC 其实非常简单:
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx16g -jar app.jar我们当时在模拟项目X上做过一次对比,同样 24GB 堆、同样压测流量,G1 设置 -XX:MaxGCPauseMillis=50 的实测 GC 停顿经常飘到 100ms 以上;换成 ZGC 后,日志里的最大停顿基本没超过 10ms。代价是 ZGC 会额外占用一部分堆内存(用于染色指针和转发表),如果你给 JVM 只分了 2GB 堆,那 ZGC 的优势会大打折扣,甚至可能出现比 G1 更频繁的分配失败。
JDK 11 里的 ZGC 需要注意几个限制:首先它只支持 Linux x64 环境,macOS 和 Windows 要到后续版本才有;其次最大堆支持大约 4TB,大多数场景够用,但如果你有超大规模堆,要留意版本限制;再有就是它没有分代设计,从 JDK 21 开始 ZGC 才有了分代模式,这是后话。
3.2 Epsilon:一个什么都不做的 GC
JDK 11 还带了一个很有意思的实验性 GC:Epsilon。它不执行任何垃圾回收,只负责内存分配,堆满了就直接抛 OutOfMemoryError。听起来很离谱,但它有明确的使用场景:短生命周期任务。比如一个批处理程序,启动后分配一批对象、计算结果、立即退出,那根本不需要 GC 来回收内存,交给操作系统在进程结束时统一回收反而更快。
Epsilon 的用法是:
java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xmx1g -jar short-task.jar我个人认为它最大的价值不是生产落地,而是做性能测试:在对比不同 GC 收集器时,有一个完全不做收集的“对照组”,能帮你算清楚 GC 到底占了多少开销。如果你只是好奇,也可以跑一跑看看内存耗尽时的堆栈信息,能加深对 JVM 内存模型的理解。
4. 语法与运行时层面的几个变化
4.1 var 用在 Lambda 参数上
很多人知道 JDK 10 引入了 var 做局部变量类型推断,但 JDK 11 做了一件更精细的事:允许在 Lambda 表达式的参数上使用 var。别小看这个改动,它解决了一个很实际的问题——Lambda 参数本来是没法加注解的,有了 var 之后,注解可以往里放了。
但这里有个容易踩的语法坑:var 用于 Lambda 参数时,必须用括号括起来,也就是说 (var x) -> x + 1 是合法的,var x -> x + 1 直接编译报错。原因是它和类型推断规则冲突。另外,你不能混着写,比如 (var x, Integer y) -> 0 也是非法的,要么参数全用 var,要么全写显式类型。这个语法在业务代码里用到的地方不算多,主要是在使用 Spring 式注解校验、或者写泛型工具类时会觉得顺手。
4.2 单文件源码启动:小工具脚本的新选择
JDK 11 支持直接运行单个 .java 文件而不需要先 javac 编译:
java Hello.java这个特性把 Java 往脚本语言的方向推了一步,适合写小工具、快速验证算法片段的时候用。它背后的做法不是让你绕开编译,而是启动器内部帮你完成了一次内存中的编译,效果和 javac 基本等价。但要注意:这种方式只支持单个源文件,如果你的代码里 import 了项目中的其他类,或者依赖了第三方 jar,它就没那么方便了。官方设计的初衷是给“单文件级程序”用的,不是用来替代 Maven/Gradle 项目的。我在本地写一些临时数据清洗脚本时会直接 java Test.java,跑完就删,确实省事。
4.3 jlink 定制运行时:给容器化部署减负
jlink 是 JDK 9 引入的模块化工具链之一,JDK 11 里已经非常成熟。它可以根据你项目实际用到的模块,生成一个精简的运行时镜像,而不是把整个 JDK 塞进容器。
举个例子,假设你的模块只有 java.base 外加 java.logging:
jlink --module-path $JAVA_HOME/jmods --add-modules java.base,java.logging --output /opt/custom-jre生成出来的目录只有几十 MB,和完整 JDK 动辄两三百 MB 相比,镜像体积能降一大截。对现在这种容器化交付的流程来说,少几十 MB 可能不算什么,但精简镜像的好处不仅在于体积,还在于攻击面变小了:只包含你真正用到的模块,不需要的类库不会出现在运行时里。
需要注意的是,jlink 对非模块化项目支持得非常痛苦。如果你的依赖没有 module-info.class,则需要把整个 classpath 一起塞进去,最后生成的东西往往比预想大很多。所以我的建议是:新项目从一开始就按模块化思想设计,老项目用不用 jlink 要看依赖迁移成本。
5. 从 JDK8 迁移到 JDK11 的实操路线
5.1 jdeps 依赖分析
迁移的第一步不是改代码,而是先摸清项目的依赖情况。JDK 自带的 jdeps 工具在 11 里非常好用,它能够分析你的 jar 或 class 文件依赖了哪个 JDK 模块:
jdeps --multi-release 11 --class-path libs/*.jar myapp.jar输出的结果会显示哪些包来自 java.base,哪些来自你已经移除的 Java EE 模块,比如 java.xml.bind。这一步能让你在动手改代码之前就知道风险点在哪里。我们在迁移某跨平台系统时就发现,一个很久没动过的工具模块居然用了 java.se.ee 里的 JAXB 类,之前因为一直跑在 JDK 8 上没暴露,jdk 9 以后把这些模块移除了,一升级就炸。
5.2 反射与模块强封装
如果你的项目用了反射去访问 JDK 内部类,那么 JDK 11 是第一个让你明显感觉到“疼”的 LTS 版本。JDK 9 开始模块化后,默认情况下不允许反射访问非导出包,JDK 11 延续并强化了这个约束。常见的报错是:
java.lang.IllegalAccessError: class X cannot access class sun.misc.Unsafe很多字节码操作库、动态代理库、甚至一些 ORM 框架都会踩到。解决办法有几个:一是升级依赖版本,看看新版本是否适配了模块化;二是如果框架实在没法升,那就用 JVM 参数开放对应包,比如:
java --add-opens java.base/sun.nio.ch=ALL-UNNAMED -jar app.jar但 --add-opens 属于治标不治本,而且开放过多包会重新引入安全风险。我更推荐用 jdeps 分析出到底哪个第三方库在访问内部 API,然后逐个升级替换。这步的排查工作量最大,但也是最值得花时间的。
5.3 字符串编码与 TLS 降级
从 8 升 11 之后,最容易出现的中文乱码问题往往和默认字符集有关。JDK 11 在 Linux 环境下默认字符集依然跟随系统 locale,如果系统 locale 不是 UTF-8,那 Files.readString 和 HttpURLConnection 的默认编码就可能和你预期不一致。安全的做法是在启动参数里显式指定:
java -Dfile.encoding=UTF-8 -jar app.jar另一个隐蔽的变化是 TLS。JDK 11 默认启用了 TLS 1.3,但很多老系统(比如某些银行接口、老旧硬件网关)只支持到 TLS 1.1,联调时会突然发现握手失败。不要慌,先在 JVM 参数里限制客户端协议版本,比如:
java -Djdk.tls.client.protocols=TLSv1.2 -jar app.jar不过这只是权宜之计,从安全角度我还是建议推动被调用的服务端升级。毕竟 TLS 1.2 也已经在逐步退役的路上了。
6. 常见问题与排查实录
这里整理几个迁移 JDK 11 后最常见的故障现象和处理思路,都是我实际遇到过的,按出现频率排一下:
| 现象 | 原因 | 处理办法 |
|---|---|---|
| java -version 显示的还是 8 | PATH 中有旧版本优先 | which java 定位软链,调整 JAVA_HOME 或 update-alternatives |
| 启动报 java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException | Java EE 模块被移除 | 引入依赖,比如 javax.xml.bind:jaxb-api,或改用内置 XML 解析方案 |
| 编译时报 invalid target release: 11 | IDE 或构建工具用的是 JDK 8 | 检查 Project Structure 和 maven-compiler-plugin 的 source/target 版本 |
| 运行时报 IllegalAccessError | 反射访问 JDK 内部 API | 用 jdeps 查依赖,升级第三方库,必要时 --add-opens |
| 启动时提示 Unrecognized VM option UseCGroupMemoryLimitForHeap | 旧版容器参数 | 删除旧参数,改为 -XX:+UseContainerSupport |
| GC 日志不输出 | JDK 9+ 日志体系重写 | 改用 -Xlog:gc*:file=gc.log:time,uptime,level,tags |
第一个问题特别常见,很多人明明安装了 11,但 /etc/profile 里 export 的顺序不对,导致 bash 启动时命中的还是 /usr/bin/java。我习惯先执行 type -a java 看一下所有候选路径,再逐一排查。安装时也要注意:下载官方 tar.gz 解压后,JAVA_HOME 应该指向带版本号的目录,而不是解压出来的外层目录。
还有一个不太起眼但很实用的点:从 8 升级到 11 后,Maven 和 Gradle 的编译器插件版本要一起升。Maven 的话建议 maven-compiler-plugin 至少 3.8.0,并且显式配置 release:
<properties> <maven.compiler.release>11</maven.compiler.release> </properties>release 参数会同时控制 source 和 target,并且能额外检查你代码里是否误用了更高版本的 API,比单独配 source/target 更严谨。
另外,如果你是 Spring Boot 用户,注意 Spring Boot 2.1 以下版本对 JDK 11 的支持不完整,尤其是 Hibernate 和 Tomcat 版本兼容性。升到 JDK 11 前先把 Spring Boot 升到 2.1+ 会更稳妥。我见过一个项目只改了 JDK 没升框架,结果启动时 CGLIB 代理直接报错,查了一天才发现是框架版本太老。
再补充一个 JDK 11 的隐藏变化:Nashorn JavaScript 引擎被标记为废弃,但还没移除。如果你的项目里用到了 javax.script 跑 JS 脚本,短时间内不会挂,不过要提前规划迁移方案,别把它当成长期依赖。
最后说下我个人对 JDK 11 的定位:它不是那种让你写完代码拍大腿“太爽了”的版本,它的价值更多体现在 JVM 层面的长期演进上,ZGC、模块化、精简运行时这些基础打好了,后面 17、21 的很多能力才有立足点。如果你已经在这条升级路上,我的体会是:不要试图一次性把全部特性都用上,先只做 JDK 版本切换,保持代码不变,跑通整条链路之后,再逐步用新 API 替换旧实现,这样风险最可控。JDK 11 是个承上启下的版本,用好了,后面再升 17 或 21 会有更平滑的路径。