
JDK11新特性这个话题放到今天来看依然不算过时。很多团队的生产环境还停留在JDK8而JDK11恰恰是从JDK8升级到后续版本时必须跨过的一道门槛它是继JDK8之后的第一个长期支持版本包含了从JDK9到JDK11三个版本累计的新能力。对于平时只关心业务代码、不追版本的人来说JDK11真正值得关注的并不是某一个花哨功能而是一整套关于“模块化”“标准库整合”“垃圾回收器选择”的底层变化。这篇文章我更想以一个实际升过级、趟过坑的人的角度聊聊JDK11里那些能直接用起来的东西以及升级时真正会卡住你的细节。1. 版本背景与升级价值为什么JDK11不是一次小版本升级1.1 从JDK8到JDK11中间到底隔了多远的距离很多人对JDK8之后的版本印象是“版本号跳得快特性记不住”。JDK9、JDK10、JDK11实际上分别承担了不同的任务JDK9把模块系统引入并重构了整个JDK本身JDK10给了局部变量类型推断JDK11则在这个基础上补齐了HTTP客户端、ZGC、Epsilon、JFR开源等一批直接作用于生产环境的能力。可以说JDK11是一个把前面几个版本的“半成品”收敛成真正可用的长期支持版本的版本。JDK11同时也是很多开发工具和框架愿意把最低要求提到这个Level的重要原因。Spring Boot 2.2开始完整支持JDK11再到后来的Spring Boot 3直接要求JDK17起步JDK11处在中间位置兼容了JDK8时代的大量生态又为后来的新特性铺了路。从实际团队选型来看升级到JDK11的阻力远小于直接跳到JDK17因为绝大多数第三方库在JDK9模块化之后都已经完成了适配JDK11反而不像JDK9那样需要处理一堆module-info编译错误。还有一个容易被忽略的点JDK8的最后一个免费公开版本停在JDK8u20x左右之后的更新需要订阅或使用开源社区构建版。JDK11作为LTS官方提供了比较长期的支持这是生产环境愿意升级的核心动力。但支持周期长短并不是最重要的真正让企业下定决心的是JDK11把“模块化”和“标准库自带HTTP客户端”这些能力带到了稳定状态让后续的新项目可以直接建立在更现代的API之上。1.2 版本节奏与升级成本怎么算JDK从JDK10开始采用“每半年一个版本”的发布节奏非LTS版本的维护周期很短不适合直接上生产。所以大家要记住一个规律看到一个新版本先别急着用看看它是不是LTS。JDK11是LTSJDK17是LTSJDK21是LTS中间那些JDK12到JDK16都只是过渡版本。选型时优先落在LTS版本上既能获得新特性又不用每半年被迫升一次级。升级成本主要来自三个方面。第一个是“编译期兼容性”要处理被移除的API和新增的模块化约束第二个是“运行期兼容性”要验证反射、类路径、依赖注入是否受模块强封装影响第三个是“行为差异”比如垃圾回收器的默认变化、TLS1.3开启后的握手行为变化。这三个问题在JDK11里都有典型现象后面会单独展开讲。我见过不少项目升级时只改了java.version就以为完事结果一启动就直接报ClassNotFoundException。JDK11并不是自然过渡它删了Java EE相关的模块很多在JDK8里“开箱即用”的东西到了JDK11变成“开箱即没”。这部分必须在升级前做一次依赖盘点不能靠运气。2. 语言层面的新特性写起来更舒服但别滥用2.1 var局部变量类型推断JDK10引入了varJDK11继承了它。var允许你在局部变量声明时省略类型编译器会根据初始化表达式推断出实际类型。注意var并不是一种新的“动态类型”它仍然在编译期完成类型推断运行时的字节码和你手动写完整类型没有任何区别。它也不是Java的关键字而是“保留类型名”所以你的代码里可以有名为var的变量但我建议别这么干。实际用过之后最大的感受是泛型嵌套类型的声明终于不用再写两遍。比如var list new ArrayListString(); var map new HashMapString, ListInteger(); for (var item : list) { System.out.println(item); }这条代码在JDK8里要写两遍ArrayListString在JDK11里右边写一遍就够了。对现代Java开发来说这大大减少了样板代码。不过var的使用范围严格限定在局部变量字段、方法参数、返回值类型都不能用。如果你试图在方法参数上用var编译器会直接拒绝。从代码可读性角度我给一个经验当类型信息对理解逻辑很重要时不要用var。比如var result getUserService().getUserById(10086);读者完全不知道getUserById返回什么得跳进方法里看。反过来如果右边初始化表达式已经写得很清楚比如var sql select * from t_user;这时候用var就很安全。另外在一些链式调用里var能把一大段泛型声明压缩成一行比如流式操作的中间变量我用得最多。2.2 Lambda参数也可以用varJDK11给var加了一个新场景Lambda表达式的参数。过去我们写Lambda参数要么不写类型要么写完整类型没法写注解。现在可以用var来声明参数主要目的是给参数加注解BiConsumerString, String consumer (NotNull var a, NotNull var b) - { System.out.println(a b); };注意这里有一个“全有或全无”的规则Lambda表达式里的所有参数要么全部省略类型要么全部写var不能一个省略、一个var也不能一个var、一个写具体类型。这个限制是为了保持一致性也让编译器解析起来更简单。实际项目中这个特性的使用率不算高因为大多数Lambda参数不值得加注解但在做一些参数校验框架或代码分析工具时var参数加注解确实很实用。2.3 String与Files新增的“小而香”方法JDK11给String加了好几个直接解决问题的实用方法推荐大家重点使用isBlank()判断字符串是否为空白空白包括空格、制表符和换行。strip()/stripLeading()/stripTrailing()去掉首尾空白替代老的trim()。不同点在于strip()支持Unicode空白对中文场景没有影响但对某些特殊字符更规范。lines()把字符串按换行符拆成一个StreamString处理多行文本很方便。repeat(int count)重复字符串指定次数比手写循环优雅得多。举个实际例子String text hello\nworld ; System.out.println(text.isBlank()); // false System.out.println(text.strip()); // hello\nworld System.out.println(abc.repeat(3)); // abcabcabc text.lines().forEach(System.out::println);对应地java.nio.file.Files也多了两个便捷方法读取和写入文件终于不用再包一层BufferedReader或者Files.write加一堆参数了Path path Path.of(test.txt); Files.writeString(path, hello java 11, StandardCharsets.UTF_8); String content Files.readString(path, StandardCharsets.UTF_8);这俩方法内部已经处理了流关闭适合小文件读写。我自己在写工具类、脚本、临时Demo时经常用省掉不少重复代码。唯一要注意的是Path.of()是从JDK11才开始有的如果你还有JDK8的环境得用Paths.get()代替这个差别在升级时容易漏掉。3. 标准库与API新特性把以前要引第三方包的事变成官方能力3.1 标准HTTP客户端终于可以少引一个依赖了老Java开发者对java.net.HttpURLConnection不会有什么好印象API设计老旧用起来繁琐连接管理也不方便。JDK9开始引入了一个全新的HTTP客户端JDK11把它作为标准API固定下来这就是java.net.http.HttpClient。它支持HTTP/1.1、HTTP/2和WebSocket同步异步都能写设计上也比老API现代得多。基本用法很直观HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); HttpRequest request HttpRequest.newBuilder(URI.create(https://example.com/api)) .header(Accept, application/json) .GET() .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.statusCode()); System.out.println(response.body());异步调用用sendAsync返回一个CompletableFuture适合写非阻塞逻辑client.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenAccept(response - System.out.println(response.body()));这里要特别说一个容易踩的坑HttpResponse.BodyHandlers返回的Handler决定了响应体怎么处理。如果你用ofString()响应体按字符串处理如果接口返回的是二进制要用ofByteArray()如果响应体很大用ofInputStream()流式读取。选错Handler会导致大量内存占用甚至直接OutOfMemoryError。我在项目里主要用它替代老旧的HttpURLConnection和一些轻量HTTP调用场景。它比RestTemplate轻比OkHttp更“原生”不需要额外依赖。但它不像第三方库那样有丰富的拦截器、连接池策略和超时配置抽象复杂场景还是建议上专业HTTP库。JDK11带来的价值是标准API覆盖了80%的调用需求额外引依赖的诉求大大降低了。3.2 ZGC与Epsilon两种“极端”垃圾收集器JDK11最吸引我注意的其实是垃圾回收器。ZGC全称是Z Garbage Collector目标是在任意大小的堆上把暂停时间控制在10毫秒以内。它的思路是把标记、转移等阶段尽量并发执行只有很少数步骤需要STW。JDK11里ZGC还是实验特性需要显式开启java -XX:UnlockExperimentalVMOptions -XX:UseZGC -Xmx16g -jar app.jarZGC在JDK11里的表现已经比想象中好但实验阶段的限制也不少比如只支持64位系统、不支持压缩指针等。很多团队真正敢把ZGC放上生产是等到JDK15转正之后。所以JDK11里的ZGC更像一个“技术预览”适合压测和提前学习不建议核心系统直接上。不过能从这个版本看到官方对未来GC方向的押注低延迟优先。另一个更极端的收集器是Epsilon。Epsilon是一个“什么都不做的GC”它只负责内存分配不回收任何对象。这听起来像玩具但其实有实际应用价值主要面向生命周期极短的应用比如短跑批处理任务、单次执行的命令行工具以及GC性能基准测试。启动时用java -XX:UnlockExperimentalVMOptions -XX:UseEpsilon -jar short-job.jar用Epsilon的应用一旦堆内存不够进程会直接崩溃退出而不是触发GC。对于一些只运行几秒、任务结束后自然退出的工具这反而减少GC开销。不过如果你的应用是长时间运行的Web服务千万别开Epsilon否则内存涨满后等待你的只有OutOfMemoryError。在JDK11里理解这两种极端GC能帮你更清醒地判断自己的应用到底需要什么。3.3 飞行记录器JFR开源JFR在JDK11之前是商业版本的功能需要额外解锁。JDK11把它开源了这是一件被低估的大事。飞行记录器可以在几乎不影响性能的情况下收集JVM运行期间的详细数据包括对象分配、方法采样、锁竞争、GC暂停、IO热点等。以前遇到线上问题只能猜有了JFR就能直接“回放”事件。启动时用参数打开java -XX:FlightRecorder -XX:StartFlightRecordingduration60s,filenameflight.jfr -jar app.jar记录完之后可以通过工具打开.jfr文件或者在运行中用jcmd动态开始和停止记录。实际使用感受是JFR对性能的影响通常控制在1%左右比打日志和上APM轻得多。排查CPU高、GC频繁、线程阻塞这类问题JFR是第一个该用的工具而不是先堆打印日志。JDK11把JFR开源后它就成了诊断Java应用问题的一个“免费利器”。4. 模块系统与整理升级前必须搞懂的破坏性变动4.1 Java模块系统正式落地JDK9引入的Project Jigsaw模块系统JDK11算是正式落地的稳定版本。模块系统的核心目的是让JDK本身支持模块化也让我们自己的工程可以通过module-info.java声明依赖关系。对于普通开发者最明显的感受不是“模块化有多好用”而是“以前能访问的内部API现在访问不了了”。JDK默认把内部API封装起来启动时如果检测到非法访问会打出警告默认行为在JDK11里是--illegal-accesswarn还是--illegal-accessdeny不同版本有变化。实际中常见的问题是很多老框架用反射访问sun.misc.Unsafe或者其他内部类到了JDK11可能直接抛IllegalAccessException或InaccessibleObjectException。解决方案不是去改JDK内部而是升级框架版本或者用--add-opens按需打开某个包。模块化还改变了类路径和模块路径的关系。传统JAR包放在classpath里属于“未命名模块”可以读取所有模块的导出包如果使用模块路径则需要显式声明依赖。对于大多数Spring Boot项目因为你没有创建module-info.java所以仍然走classpath逻辑升级阻力不大。真正需要重点测试的是那些用自定义类加载器、依赖大量反射的中间件。4.2 移除的Java EE与CORBA模块JDK11做了一个很“狠”的决定把Java EE和CORBA相关模块从JDK中移除。这意味着javax.xml.bindJAXB、javax.xml.wsJAX-WS、javax.activation、javax.annotation、com.sun.corba.*等包在JDK11里直接不存在了。对老项目来说最典型的是JAXB报错java.lang.ClassNotFoundException: javax.xml.bind.JAXBException解决办法不是退回JDK8而是引入独立的JAXB依赖。如果你用的是Maven可以加dependency groupIdjakarta.xml.bind/groupId artifactIdjakarta.xml.bind-api/artifactId version2.3.3/version /dependency dependency groupIdcom.sun.xml.bind/groupId artifactIdjaxb-impl/artifactId version2.3.8/version /dependency另外javax.annotation.PostConstruct、javax.annotation.PreDestroy在JDK11里也被移除了但很多框架在运行时仍然需要它们。最好显式引入jakarta.annotation-api依赖。我见过一些项目报的是NoClassDefFoundError排查半天才发现是spring-context的注解扫描依赖了这个包。升级前最好用jdeps扫描一下jdeps --jdkinternals --class-path lib/* target/myapp.jar这个命令会列出所有对JDK内部API的依赖以及被移除的API是升级前最重要的体检工具。4.3 Nest嵌套访问控制的细节改进Nest嵌套巢是JDK11引入的一个比较微妙的新特性。以前Java允许内部类访问外部类的私有成员但这是通过在编译期生成桥接方法实现的比如Outer.access$000()。这种语法层面的“绕过”在反射时会出问题因为桥接方法带有access$字样也影响未来优化。JDK11提出“Nestmates”概念把互为嵌套关系的类在虚拟机层面归为一个逻辑组。嵌套成员之间可以直接互相访问私有成员不需要编译器生成桥接方法。这对业务代码的影响很小但对库作者和反射工具影响比较大。比如某些框架通过反射枚举类的成员时不再看到那些多余的access$方法行为更干净。同时JDK11还新增了getNestHost()和getNestMembers()两个反射API用来查询嵌套关系。这个特性还修掉了一个历史问题之前通过反射调用内部类的私有方法会报IllegalAccessException现在JDK11会基于Nest身份判断内部类之间天然允许访问。对单元测试和依赖注入框架来说这算是一个正面的改善。5. 升级到JDK11的实战记录踩坑与排查5.1 升级前要做的事升级JDK11不是一个“改版本号启动”的简单操作我建议至少按下面几步走一遍。首先用jdeps --jdkinternals扫描目标JAR把所有依赖JDK内部API或Java EE模块的调用找出来这一步会生成一个清单。其次检查项目里是否直接用到了JAXB、JAX-WS、javax.annotation如果有立刻补依赖。再次看有没有使用反射访问JDK内部类比如sun.misc.Unsafe、sun.reflect有则换成公开的API或验证--add-opens方案。最后跑一遍全量测试重点关注序列化、XML解析、反射框架、字节码增强工具这四块。升级过程中还要统一团队所有项目的依赖基线。很多项目JDK8能跑JDK11不能编译往往是某个老版本ASM、CGLIB或字节码库不兼容新版class文件版本。升级第三方库时不要只看主版本要确认它是否声明支持class file 55.0否则会在运行时遇到UnsupportedClassVersionError。5.2 常见报错与解决速查我把实际升级过程中最常见的几类问题整理成了表格照着排查会快很多报错或现象常见原因处理方法ClassNotFoundException: javax.xml.bind.JAXBExceptionJAXB模块被移出JDK引入JAXB API与实现依赖NoClassDefFoundError: javax/annotation/PostConstruct注解API被移除引入jakarta.annotation-apiInaccessibleObjectException模块系统封装了JDK内部包使用--add-opens java.base/java.langALL-UNNAMEDIllegalAccessError: superclass access check failed使用老字节码库或工具升级ASM、CGLIB、ByteBuddy等相关依赖UnsupportedClassVersionError: 55.0编译产物class版本高于运行JVM确保编译目标与运行环境均为JDK11启动时大量Illegal reflective access警告反射访问JDK内部API升级框架版本或用--add-opens不要忽略警告TLS1.3握手失败老客户端不支持TLS1.3客户端升级服务端配置jdk.tls.client.protocols这里要特别强调启动日志里出现“Illegal reflective access”警告时不要当作噪音。它说明你的应用正在以未来可能被禁止的方式访问JDK内部。最好在升级阶段就处理掉而不是等到JDK17强制禁止时再急。5.3 行为差异与运行时细节除了报错还有几个行为差异容易让人迷惑。第一个是JDK11的默认字符集仍然跟随系统环境所以代码里处理文件读写时不要依赖“默认编码”务必显式指定StandardCharsets.UTF_8否则在Windows和Linux上跑出的结果可能不一致。第二个是TLS1.3在JDK11中默认开启这本来是好事但如果你有老版本客户端或者中间设备不支持TLS1.3就需要通过JVM参数调整-Djdk.tls.client.protocolsTLSv1.2第三个是模块化后的JRE体积问题。JDK本身遵循模块化结构我们可以用jlink制作精简运行镜像把不需要的模块裁掉生成一个几十MB级别的运行时。这是JDK11在容器化场景下的一个加分项jlink --module-path $JAVA_HOME/jmods \ --add-modules java.base,java.sql,java.naming \ --output /opt/java-min runtime需要注意jlink要求应用本身走模块路径才能发挥最大作用普通classpath应用也可以用但要明确加足模块列表。我实际做Spring Boot自定义镜像时基本用现成的基础镜像更省事jlink更适合工具类应用和嵌入式场景。6. 个人使用体会与后续扩展建议6.1 我在切换JDK11后最直接的感受从JDK8切到JDK11最明显的变化不是“快”或者“慢”而是写代码的方式变得顺手了。var让DTO初始化和流式处理代码短了一截HTTP Client让一些内部服务的调用不再需要一个额外封装好的工具类Files.writeString也让我在写临时脚本时少了很多模板代码。开发体验的提升虽然没有直接体现在业务指标上但代码维护成本和心智负担确实降下来了。运行时的变化要分场景看。我试过把同一个服务从JDK8直接切到JDK11启动时间略有波动但差距不大。真正影响运行时行为的是GC选择。JDK8的默认GC是ParallelJDK11的默认GC同样是Parallel所以如果你什么都不改老服务的垃圾回收行为不会有太大变化这也降低了升级风险。但如果你的服务是大堆、低延迟敏感型JDK11给了你ZGC这个新选项值得单独做压测对比。我自己的经验是堆内状态不多、要求低延迟的系统ZGC在JDK11里已经能跑出不错的停顿表现但生产上还是建议等等转正版。JFR开源是我个人最推荐大家培养的一个习惯。以前排查CPU高、GC频繁总靠jstack打线程快照看不清全局。现在JFR一开一分钟的记录文件就能看出热点方法和分配压力。建议所有JDK11应用的启动参数里直接加上JFR的默认配置文件线上出问题再动态开启一段记录很多疑难杂症会清晰很多。6.2 后续版本与扩展建议JDK11不是终点。JDK17带来了密封类、模式匹配的预览、switch表达式转正还把NullPointerException的提示改得更好用JDK21则把虚拟线程推到了生产可用级别。如果你已经完成了JDK11迁移后续再看JDK17和JDK21会顺畅很多因为语言层面的增量是渐进的而不是再来一次“拆除式”升级。不过我不建议所有项目都盲目追最新版。JDK11在很长一段时间里仍然是不少基础框架的最低支持版本向上兼容得很好。对于个人学习可以把JDK11作为一个“现代Java”的起点把var、Text Block、Record、Sealed Class这些特性串起来理解。对于团队选型如果时间和精力充裕建议直接评估JDK17甚至JDK21毕竟JDK11的LTS支持窗口也在逐渐收窄晚升不如早升。最后分享一个小细节升级JDK11后把JVM参数里的-Xmx和-Xms设成一样的值对大多数服务依然有用但在ZGC这种并发收集器下堆大小设置可以更大胆一些因为停顿不会随着堆变大而明显恶化。JDK11带给我们的不只是一堆新API更是一套重新思考Java运行方式的视角值得花点时间好好消化。