jdk10,被很多Java开发者忽略的实用更新

tmyb

很多Java开发者提到JDK版本,脑子里第一反应就是JDK8、JDK11、JDK17这些长期支持的LTS版本,非LTS版本往往直接被跳过,尤其是夹在JDK9和JDK11之间的JDK10,很多人甚至不知道它更新了什么,连碰都没碰过,但如果你最近几年有过从JDK8升级高版本的经历,或者做过Java应用的容器化改造,其实你早就享受到了JDK10带来的便利,今天我们就来聊聊这个被低估的Java版本。

jdk10,被很多Java开发者忽略的实用更新

从Java迭代史,看JDK10的尴尬定位

要聊JDK10,得先说说它出生的背景,在JDK8之前,Java的发布节奏非常慢,往往三五年才出一个大版本,JDK8从发布到下一个正式版本JDK9,隔了整整5年,慢更新导致Java跟不上云原生、微服务的新趋势,社区吐槽声越来越大,于是Oracle在JDK9发布之后彻底改了发布规则:每半年更新一个小版本,每三年出一个LTS长期支持版本,非LTS版本只更新半年,就会被下一个版本取代。

JDK10就是这个新规则下的第二个非LTS版本,2018年3月正式发布,距离JDK9发布才半年,距离下一个LTS版本JDK11也只有半年,生在两个大版本之间,又是非LTS,生产环境不敢用,学习的时候也没人会专门学它,直接就被跳过了。

根据2024年6月JetBrains最新发布的《Java开发者生态报告》统计,目前全球Java开发者中,仍然有32%的人主力版本还是JDK8,升级到LTS版本JDK17的开发者占比刚刚超过40%,所有非LTS版本加起来的使用率不到10%,其中JDK10的使用率更是不到1%,加上Oracle2023年底还宣布,把JDK8的公众扩展支持从2030年延长到了2032年,很多中小企业更不愿意折腾升级,自然也就没人关注过JDK10。

但尴尬的定位不代表它没有价值,恰恰相反,JDK10是Java转型过程中承上启下的关键版本,我们现在用的很多好用的特性,其实最早都是在JDK10落地测试的。

JDK10最出圈的特性:var类型推断到底香不香

提到JDK10,大多数人第一个想到的就是局部变量类型推断,也就是var关键字,这个也是JDK10带给Java社区最大的改变,我前同事阿凯去年给公司做老电商项目重构,计划从JDK8直接升级到JDK17,团队内部为了要不要用var吵了整整一周,现在想来都有意思。

当时团队里的老开发坚决反对用var,说Java是强类型语言,省略类型就是破坏可读性,以后维护代码的时候,谁知道这个变量是什么类型?改出问题谁负责?年轻一派则说,现在编辑器都带类型提示,悬浮就能看到,写那么长的类型纯纯是浪费时间,代码看起来又乱又长。

后来阿凯翻了JDK的变更历史才发现,原来var这个特性最早就是JDK10引入的,后来才整合到各个LTS版本里,JDK官方设计这个特性本来就不是让你所有地方都瞎用,只是简化不必要的类型声明,我个人也非常认同这个设计,举个最常见的例子,JDK8我们写代码经常会遇到这种情况:

HashMap<String, List<OrderDTO>> orderGroupByUser = queryOrderGroupByUserId(userId);

前面的类型声明占了半行,方法名已经清清楚楚告诉你这个变量是按用户分组的订单Map,为什么还要把长长的类型再写一遍?换成var之后就清爽很多:

var orderGroupByUser = queryOrderGroupByUserId(userId);

只要命名规范,根本不会影响可读性,反而让代码更干净,那是不是所有地方都要用var?当然不是,如果你的代码是这样:

var result = getData();

哪怕你写了完整类型List<Data> result = getData();,照样没人知道result到底是什么,这是命名不规范的问题,不是var的问题。

这么多年用下来,我个人的总结是:var是一个工具,不是语法要求,该省的省,该写清楚的写清楚,用对了能提升至少10%的开发效率,根本不是什么破坏可读性的洪水猛兽,而这个好用的工具,最早就是JDK10带给我们的。

那些藏起来的更新,比var更影响生产环境

除了var,JDK10还做了很多非常实用的底层改进,这些改进大多直接延续到了后续的LTS版本,很多人天天用却不知道源头在JDK10。

第一个就是G1垃圾回收器的并行Full GC改进,G1从JDK7开始引入,JDK9把它设为默认垃圾回收器,但是在JDK10之前,G1的Full GC一直是单线程执行的,如果你的堆内存比较大,触发一次Full GC,单线程处理可能停顿好几秒,对线上业务来说就是P1级事故,所以很多企业当年根本不敢用G1,宁愿继续用老的Parallel GC,JDK10直接把G1的Full GC改成了并行多线程执行,停顿时间直接下降了好几倍,解决了G1最大的痛点,也让G1坐稳了后续十几个版本默认垃圾回收器的位置,这个改进看起来只是调优,实际上影响了所有Java后端应用的稳定性,价值非常大。

第二个就是容器化支持的改进,这个改进不知道救了多少当年做容器化改造的团队,我朋友小宇2022年帮公司做Java应用的K8s迁移,就踩过这个坑,当时他们用的是比较老的JDK8u181,给容器分配了2核4G的资源,结果应用启动没多久就被Docker莫名其妙杀掉,查了整整两天才找到原因:老版本JDK不识别Docker通过cgroups做的资源隔离,读取CPU核心数和内存的时候,读的是宿主机的参数,他们宿主机是32核64G,JDK就自动开了32个GC线程,把堆内存设到了40G,远远超过容器的4G配额,直接被内核OOM杀掉了。

而这个问题,JDK10早在2018年就解决了,JDK10正式支持读取Docker容器的cgroups配额,自动调整堆内存和GC线程数,后来OpenJDK把这个补丁反向移植到了JDK8u191之后的版本,才让老JDK也能正常跑在容器里,现在几乎所有Java应用都跑在Docker和K8s上,云原生已经是绝对的主流,这个改进的影响力怎么说都不为过。

第三个改进是应用类数据共享(CDS)的优化,原来JDK9就引入了CDS,但是只支持系统类,JDK10扩展了CDS的能力,支持应用类共享,还开放了动态归档的功能,能把常用的类提前归档,大幅提升Java应用的启动速度,对于现在的微服务和Serverless场景,启动速度快非常重要,冷启动速度快就能节省资源,用户体验也更好,这个优化也一直延续到了现在的JDK21,后续的AOT编译其实也是在这个方向上继续迭代的。

除此之外,JDK10还强化了JDK9引入的模块系统的访问控制,清理了很多早就废弃的API和工具,比如早就没人用的Java Web Start的javaws就是JDK10移除的,让JDK本身更干净更轻巧。

现在没人用JDK10,还有必要了解它吗?

看到这里很多人会说,现在生产环境都是用LTS版本,JDK10早就停止更新了,我了解它有什么用?我个人的看法是,非常有必要,尤其是对于经常要做版本升级和问题排查的Java开发者,原因有两个。

第一个原因是,很多你跳版本升级遇到的坑,根源都在JDK10的变更里,现在大多数公司的升级路径都是JDK8直接跳JDK17,跳过了JDK9、JDK10、JDK11这些中间版本,很多兼容性问题其实就是JDK10引入的变更导致的,比如阿凯他们升级的时候,就遇到过反射访问sun.misc.Unsafe报错的问题,原来JDK8允许任意反射访问内部API,JDK10强化了模块系统的访问控制,默认不允许第三方代码访问JDK内部模块,所以直接报错,阿凯一开始不知道是JDK10改了规则,瞎debug了大半天,后来翻了JDK10的变更记录,加了一行--add-opens参数就解决了,如果你不了解JDK10的变更,跳版本升级的时候真的会踩很多莫名其妙的坑。

第二个原因是,了解JDK10才能看懂Java这些年的演进逻辑,JDK10是Oracle新发布策略的第一个完整测试的非LTS版本,它验证了半年快速迭代的可行性,正是因为JDK9、JDK10这一批非LTS版本的试验,Java才成功从原来几年一个大版本的慢节奏,转型成现在快速更新、持续迭代的节奏,才能跟上云原生的潮流,不然Java早就被Go、Rust这些新语言抢走更多市场了,现在Oracle还在坚持半年一个版本、三年一个LTS的节奏,2024年刚刚发布了JDK22,这个模式能跑通,离不开JDK10当年的探索。

写在最后

JDK10确实生不逢时,作为非LTS版本从出生就注定不会被很多人用到,但它绝对不是一个可有可无的过渡版本,它贡献了var这样改变编码习惯的语法特性,填了G1垃圾回收的坑,解决了容器化的核心问题,为后续所有Java版本的发展打好了基础。

对于Java开发者来说,我们不用把JDK10放到生产环境用,但多了解一点它的历史和变更,能帮我们在升级版本、排查问题的时候少走很多弯路,也能更清楚Java这十几年的发展逻辑,很多时候就是这些不起眼的中间版本,悄悄推动了整个技术栈的进步。