Java基础--6-为什么大厂都在升级 JDK 17?密封类 + 模块化 = 更安全、更可控的现代 Java 架构

Java基础--6-为什么大厂都在升级 JDK 17?密封类 + 模块化 = 更安全、更可控的现代 Java 架构

JDK 17 LTS 新特性全实战:从 JDK 8 迈向现代化 Java 的终极指南

作者:Weisian
日期:2026年1月29日

在这里插入图片描述
“如果说JDK 8是Java走向现代化编程的开端,那么JDK 17就是这场演进走向成熟的关键里程碑。”

2026 年,Java 生态早已进入“LTS 版本为主流,非 LTS 版本为创新试验场”的迭代节奏。JDK 8 作为“十年经典”,依然支撑着大量遗留系统,但在云原生、高并发、低延迟的现代业务场景下,JDK 17 LTS 已成为企业级开发的基线版本

很多开发者困惑:“我已经熟练掌握 JDK 8 了,为什么还要升级 JDK 17?”“LTS 到底是什么?升级后能带来什么实际收益?”“从 JDK 8 迁移过去,代码要怎么改?”

在这里插入图片描述

今天,我们就以「JDK 8 对比」为核心视角,从基础概念到实战代码,从语法简化到性能优化,全方位拆解 JDK 17 LTS 的所有核心新特性——既让新手能看懂入门,也让中高级开发者能落地企业级项目,更能理解 Java 现代化的演进逻辑。


一、基础认知:先搞懂「LTS」,再谈升级

在拆解新特性之前,我们必须先厘清一个核心概念:什么是 LTS? 这是很多基础开发者容易混淆的点,也是企业选择 JDK 17 的核心原因。

1.1 LTS 是什么?(大白话解释)

LTS 是 Long-Term Support(长期支持)的缩写,是 Oracle 及 OpenJDK 社区为企业级应用提供的「稳定版本保障」。

  • 非LTS版本:每6个月发布一次,更像是“尝鲜版”。它们包含最新的功能,但官方支持周期很短(通常只有6个月)。适合个人开发者体验新特性。
  • LTS版本:大约每2-3年发布一次,会获得长达数年的官方支持,包括安全更新和错误修复。JDK 17作为LTS版本,将获得至少8年的支持(直到2029年9月),这为企业提供了升级和长期运行的稳定基础。

用一个生活中的比喻:非LTS版本就像最新款的概念车,展示了前沿技术;而LTS版本则是经过充分测试、性能稳定、可大规模量产的家用车,是企业车队可靠的选择。

在这里插入图片描述

1.2 Java LTS 版本迭代脉络(重点关注)

从 JDK 8 开始,Java 确立了「每 6 个月发布一个非 LTS 版本,每 3 年发布一个 LTS 版本」的迭代节奏:

  1. JDK 8 LTS(2014 年发布):经典中的经典,支撑了无数企业系统,支持周期已接近尾声,目前仅提供关键安全补丁;
  2. JDK 11 LTS(2018 年发布):继 JDK 8 后的又一主流版本,引入模块化雏形,目前仍有大量企业在使用;
  3. JDK 17 LTS(2021 年发布):当前企业升级的核心选择,沉淀了 JDK 9 到 JDK 16 的所有优秀特性,性能、安全性、语法简洁度均远超 JDK 8/11;
  4. JDK 21 LTS(2023 年发布):正在普及的版本,在 JDK 17 基础上新增虚拟线程等特性,未来将逐步替代 JDK 17。
在这里插入图片描述

1.3 为什么企业优先选择 JDK 17?(对比 JDK 8)

对于基础开发者来说,升级的核心动力是「更简洁的语法、更低的编码出错率」;对于企业来说,升级的核心动力是「降本增效、降低风险」,具体体现在 3 点:

  1. 长期支持,无后顾之忧:支持到 2029 年,无需频繁升级版本,减少版本迭代带来的适配成本,对比 JDK 8 即将停止全面支持,更具可持续性;
  2. 性能飙升,运维更轻松:ZGC 垃圾收集器默认可用(毫秒级停顿)、启动速度更快、内存占用更低,对比 JDK 8 的 G1 GC,在高并发、大内存场景下优势显著;
  3. 安全性增强,抵御外部威胁:默认启用强封装、移除过时不安全组件、新增多种加密算法,对比 JDK 8 的老旧安全机制,能更好地抵御现代网络攻击;
  4. 语法简化,开发效率更高:密封类、模式匹配、简化的集合操作等特性,对比 JDK 8 的冗余代码,能大幅减少模板代码,降低 Bug 率。
在这里插入图片描述

二、JDK 8 vs JDK 17:语法新特性实战(从繁琐到极简)

JDK 17 沉淀了 JDK 9 到 JDK 16 的诸多语法特性,核心目标是「简化编码、聚焦业务逻辑」,我们以「JDK 8 怎么写」vs「JDK 17 怎么写」的对比方式,逐个拆解实战,每个特性都附带可直接运行的代码示例。

2.1 密封类(Sealed Classes):控制继承的「白名单」(JDK 17 正式版)

什么是密封类?

密封类是一种「限制继承关系」的类,它可以明确指定「哪些子类可以继承自己」,相当于给父类加了一个「继承白名单」,避免无关子类随意继承导致的逻辑混乱。

在这里插入图片描述
场景背景(JDK 8 的痛点)

在 JDK 8 中,我们定义一个父类后,无法限制其他开发者随意创建子类,尤其是在领域模型、状态机设计中,很容易出现「继承泛滥」的问题:

// JDK 8:父类(无法限制子类继承)publicabstractclassShape{publicabstractdoublegetArea();}// JDK 8:合法子类(符合设计预期)publicclassCircleextendsShape{privatedouble radius;@OverridepublicdoublegetArea(){returnMath.PI * radius * radius;}}// JDK 8:非法子类(不符合设计预期,但可以随意创建)// 其他开发者不小心创建了 Triangle 子类,导致后续状态判断遗漏publicclassTriangleextendsShape{privatedouble base;privatedouble height;@OverridepublicdoublegetArea(){return0.5* base * height;}}

更麻烦的是,当我们用 switch 判断子类类型时,无法保证「穷尽所有子类」,必须加一个无用的 default 分支,容易出现线上 Bug。

JDK 17 怎么写?(密封类实战)

密封类的核心语法是 sealed(声明密封类)和 permits(指定允许继承的子类),子类必须显式声明为 final/sealed/non-sealed 三者之一:

// JDK 17:密封类(仅允许 Circle、Square 继承,相当于白名单)publicsealedclassShapepermitsCircle,Square{publicabstractdoublegetArea();}// JDK 17:子类 1 - final(不能再被继承,最常用)publicfinalclassCircleextendsShape{privatedouble radius;@OverridepublicdoublegetArea(){returnMath.PI * radius * radius;}}// JDK 17:子类 2 - sealed(自身也作为密封类,指定子类)publicsealedclassSquareextendsShapepermitsRedSquare{privatedouble side;@OverridepublicdoublegetArea(){return side * side;}}// JDK 17:子类 3 - non-sealed(打破密封,允许任意子类继承,按需使用)publicnon-sealedclassRedSquareextendsSquare{privateString color;}
在这里插入图片描述
核心优势:与 switch 表达式配合,实现穷尽性检查

密封类最实用的功能,是和 switch 表达式(JDK 14 预览,JDK 17 增强)配合,实现「编译器级别的穷尽性检查」——覆盖所有子类就无需 default,遗漏子类会直接编译报错:

// JDK 8:switch 判断(必须加 default,否则编译报错,容易遗漏子类)publicStringgetShapeName(Shape shape){if(shape instanceofCircle){return"圆形";}elseif(shape instanceofSquare){return"正方形";}else{return"未知形状";// 无用但必须写,容易遗漏 Triangle 子类}}// JDK 17:switch 表达式(无需 default,编译器自动检查穷尽性)publicStringgetShapeName(Shape shape){returnswitch(shape){caseCircle c ->"圆形";caseSquare s ->"正方形";// 遗漏子类会直接编译报错,从根源避免 Bug};}
在这里插入图片描述
适用场景
  1. 领域模型设计(如支付方式、订单状态);
  2. 状态机开发(如流程节点、消息类型);
  3. 协议解析(如请求类型、响应格式)。
性能 & 安全价值
  • 编译期安全:新增子类时,所有 switch 自动报错,强制更新
  • 模型封闭:领域对象只能是你定义的几种状态,杜绝非法扩展
  • 适用场景:状态机、协议解析、策略模式(如支付方式)

2.2 模式匹配 for instanceof(JDK 17 正式版)

场景背景(JDK 8 的痛点)

在 JDK 8 中,我们使用 instanceof 判断对象类型后,必须手动强制类型转换,代码冗余且容易出错:

// JDK 8:instanceof + 手动强制转换(冗余且易出错)publicvoidprintUserInfo(Object obj){if(obj instanceofUser){// 手动强制转换,多写一行代码,且容易写错类型User user =(User) obj;System.out.println("用户名:"+ user.getName()+",年龄:"+ user.getAge());}elseif(obj instanceofAddress){Address address =(Address) obj;System.out.println("城市:"+ address.getCity());}}
JDK 17 怎么写?(模式匹配简化)

JDK 17 允许在 instanceof 中直接声明变量,无需手动强制转换,代码更简洁、更安全:

// JDK 17:模式匹配 for instanceof(直接声明变量,无需手动转换)publicvoidprintUserInfo(Object obj){if(obj instanceofUser user){// 直接使用 user 变量,编译器自动完成类型转换System.out.println("用户名:"+ user.getName()+",年龄:"+ user.getAge());}elseif(obj instanceofAddress address){System.out.println("城市:"+ address.getCity());}}
在这里插入图片描述
进阶:结合逻辑运算符使用

还可以在 instanceof 后直接添加逻辑判断,进一步简化代码:

// JDK 17:instanceof + 逻辑判断(一步到位)publicvoidprintAdultUserInfo(Object obj){// 直接判断类型 + 年龄条件,无需额外嵌套 ifif(obj instanceofUser user && user.getAge()>18){System.out.println("成年用户名:"+ user.getName());}}

2.3 简化的集合操作:List.of()、Set.of()、Map.of() 与 toList()

场景背景(JDK 8 的痛点)

在 JDK 8 中,创建一个不可变集合或快速转换集合,需要编写大量冗余代码:

// JDK 8:创建不可变 List(繁琐,需要借助 Collections.unmodifiableList)List<String> immutableList =Collections.unmodifiableList(newArrayList<>(Arrays.asList("张三","李四","王五")));// JDK 8:Stream 结果转换为 List(需要使用 collect(Collectors.toList()))List<User> userList =Arrays.asList(newUser("张三",20),newUser("李四",18));List<String> userNameList = userList.stream().map(User::getName).collect(Collectors.toList());// 代码冗长,不易记忆

而且 JDK 8 提供的 Arrays.asList() 返回的集合是「半不可变」的(不能添加/删除元素,但可以修改元素),容易引发误解。

在这里插入图片描述
JDK 17 怎么写?(简化集合操作)

JDK 9 引入了 List.of()Set.of()Map.of() 用于创建「完全不可变集合」(不能添加、删除、修改元素),JDK 16 引入了 toList() 用于简化 Stream 结果转换,JDK 17 中这些特性已非常成熟:

1. 快速创建不可变集合
// JDK 17:快速创建不可变 List(一行代码,简洁高效)List<String> immutableList =List.of("张三","李四","王五");// JDK 17:快速创建不可变 Set(自动去重)Set<String> immutableSet =Set.of("张三","李四","张三");// JDK 17:快速创建不可变 Map(最多支持 10 个键值对,超过则使用 Map.ofEntries())Map<String,Integer> immutableMap =Map.of("张三",20,"李四",18);// JDK 17:创建超过 10 个键值对的不可变 MapMap<String,Integer> bigImmutableMap =Map.ofEntries(Map.entry("张三",20),Map.entry("李四",18),Map.entry("王五",22));
注意:不可变集合的核心优势是「线程安全、无需额外同步、内存占用更低」,适合用于存储常量数据。
2. 简化 Stream 结果转换
// JDK 17:Stream 结果转换为 List(无需 collect(Collectors.toList()))List<User> userList =List.of(newUser("张三",20),newUser("李四",18));List<String> userNameList = userList.stream().map(User::getName).toList();// 一行简化,返回不可变 List(如需可变 List,可继续使用 collect)

2.4 文本块(Text Blocks):告别字符串拼接的噩梦(JDK 17 正式版)

场景背景(JDK 8 的痛点)

在 JDK 8 中,当我们需要编写多行字符串(如 SQL 语句、JSON 数据、HTML 代码)时,必须使用 + 拼接或转义字符 \n,代码可读性极差,容易出错:

// JDK 8:多行字符串拼接(繁琐、可读性差、容易出错)String sql ="SELECT id, name, age "+"FROM user "+"WHERE age > 18 "+"ORDER BY age DESC";String json ="{\n"+" \"name\": \"张三\",\n"+" \"age\": 20,\n"+" \"city\": \"北京\"\n"+"}";
在这里插入图片描述
JDK 17 怎么写?(文本块简化)

JDK 17 引入了文本块(用 """ 包裹),支持多行字符串直接编写,无需拼接和转义,代码可读性大幅提升:

// JDK 17:文本块编写 SQL(简洁、可读性强、无拼接)String sql =""" SELECT id, name, age FROM user WHERE age > 18 ORDER BY age DESC """;// JDK 17:文本块编写 JSON(无需转义 \n 和 ")String json =""" { "name": "张三", "age": 20, "city": "北京" } """;
进阶:文本块格式化(结合 String.formatted())

还可以使用 {} 作为占位符,结合 String.formatted() 实现动态赋值,进一步简化代码:

// JDK 17:文本块 + 动态赋值String userName ="张三";int userAge =20;String userInfo =""" 用户名:%s 年龄:%d 状态:成年 """.formatted(userName, userAge);System.out.println(userInfo);

你希望在「2.5 其他语法简化特性」中补充 Record 类日期时间 API 增强(JDK 17 对 JDK 8 旧日期 API 的优化与补充),我会保持原有行文风格,以「JDK 8 痛点 vs JDK 17 简化」的对比方式,完善这部分内容,让整体更完整。


2.5 其他语法简化特性(JDK 17 实用补充)

除了上述核心特性,JDK 17 还有一些针对 JDK 8 的语法优化,虽然看似微小,但能大幅提升日常开发效率:

1. 局部变量类型推断(var):告别冗长的类型声明

JDK 10 引入 var,JDK 17 中广泛使用,允许编译器自动推断局部变量类型,简化冗长的类型声明:

// JDK 8:冗长的类型声明Map<String,List<User>> userMap =newHashMap<>();// JDK 17:var 简化类型声明(编译器自动推断,不影响类型安全)var userMap =newHashMap<String,List<User>>();
注意:var 仅适用于「局部变量」,不能用于成员变量、方法参数、返回值,避免破坏代码可读性。
在这里插入图片描述
2. Optional 增强:ifPresentOrElse() 与 orElseThrow()

JDK 8 引入的 Optional 解决了 NPE 问题,JDK 9+ 对其进行了增强,JDK 17 中这些增强特性已非常实用:

// JDK 8:Optional 处理(无值时只能返回默认值,无法执行额外逻辑)Optional<User> optionalUser =Optional.ofNullable(getUserById(1));String userName = optionalUser.map(User::getName).orElse("未知用户");// JDK 17:Optional 增强 - ifPresentOrElse(有值执行消费逻辑,无值执行兜底逻辑) optionalUser.ifPresentOrElse( user ->System.out.println("用户名:"+ user.getName()),()->System.out.println("用户不存在"));// JDK 17:Optional 增强 - orElseThrow(无值时抛出指定异常,简化异常处理)User user = optionalUser.orElseThrow(()->newRuntimeException("用户不存在"));
在这里插入图片描述
3. Record 类:告别样板化的实体类(JDK 16 正式版,JDK 17 广泛使用)
场景背景(JDK 8 的痛点)

在 JDK 8 中,我们定义一个「数据载体类」(如 DTO、VO、实体类)时,必须编写大量样板代码:private 成员变量、getter 方法、equals() 方法、hashCode() 方法、toString() 方法,这些代码无业务逻辑,仅用于数据存储和展示,编写繁琐且容易出错,维护成本极高:

// JDK 8:数据载体类(样板代码占 90%,无核心业务逻辑)publicclassUserDTO{// 私有成员变量privateString name;privateInteger age;privateString city;// 构造方法publicUserDTO(String name,Integer age,String city){this.name = name;this.age = age;this.city = city;}// getter 方法(每个字段都要写,繁琐)publicStringgetName(){return name;}publicIntegergetAge(){return age;}publicStringgetCity(){return city;}// equals() 方法(用于对象比较,容易写错)@Overridepublicbooleanequals(Object o){if(this== o)returntrue;if(o ==null||getClass()!= o.getClass())returnfalse;UserDTO userDTO =(UserDTO) o;returnObjects.equals(name, userDTO.name)&&Objects.equals(age, userDTO.age)&&Objects.equals(city, userDTO.city);}// hashCode() 方法(与 equals() 配套,必须编写)@OverridepublicinthashCode(){returnObjects.hash(name, age, city);}// toString() 方法(用于日志打印,繁琐)@OverridepublicStringtoString(){return"UserDTO{"+"name='"+ name +'\''+", age="+ age +",+ city +'\''+'}';}}
在这里插入图片描述
JDK 17 怎么写?(Record 类简化)

JDK 16 引入 Record 类,JDK 17 中广泛使用,它是一种「不可变数据载体类」,编译器会自动为其生成 private final 成员变量、getter 方法、equals() 方法、hashCode() 方法、toString() 方法,无需手动编写,一行代码即可定义一个数据载体类:

// JDK 17:Record 类定义数据载体类(一行代码,无样板代码)// 编译器自动生成:private final 成员变量、getter、equals、hashCode、toStringpublicrecordUserDTO(String name,Integer age,String city){}
核心使用示例(与 JDK 8 效果一致,代码量大幅减少)
// JDK 17:使用 Record 类(与 JDK 8 的 UserDTO 用法一致)publicclassRecordDemo{publicstaticvoidmain(String[] args){// 创建对象(编译器自动生成构造方法,无需手动编写)UserDTO userDTO =newUserDTO("张三",20,"北京");// 访问字段(编译器自动生成 getter 方法,无 get 前缀,直接用字段名访问)System.out.println("用户名:"+ userDTO.name());System.out.println("年龄:"+ userDTO.age());// 自动生成 toString() 方法System.out.println("用户信息:"+ userDTO);// 自动生成 equals() 和 hashCode() 方法UserDTO userDTO2 =newUserDTO("张三",20,"北京");System.out.println("两个对象是否相等:"+ userDTO.equals(userDTO2));}}
核心特性与注意事项
  1. 不可变性:Record 类的成员变量默认是 private final,创建对象后无法修改字段值,线程安全,适合作为 DTO、VO 等数据载体;
  2. 无需手动编写样板代码:编译器自动生成 getter(无 get 前缀)、equals()hashCode()toString(),大幅减少开发工作量;
  3. 可自定义方法:Record 类中可以添加自定义方法(如业务校验、数据转换),增强灵活性;
  4. 不可继承:Record 类默认继承 java.lang.Record,无法继承其他类,也不能被其他类继承(类似 final 类)。
在这里插入图片描述
// JDK 17:Record 类添加自定义方法publicrecordUserDTO(String name,Integer age,String city){// 自定义业务校验方法publicbooleanisAdult(){returnthis.age()>=18;}// 自定义构造方法(紧凑构造方法,用于字段校验,无需写参数列表)publicUserDTO{if(age <0){thrownewIllegalArgumentException("年龄不能为负数");}if(name ==null|| name.isEmpty()){thrownewIllegalArgumentException("用户名不能为空");}}}
4. 日期时间 API 增强:JDK 8 基础上的优化与补充

JDK 8 引入了 java.time 包,解决了旧 DateCalendar 类的线程不安全、设计混乱等问题,JDK 9+ 到 JDK 17 对其进行了持续增强,让日期时间处理更简洁、更强大。

在这里插入图片描述
场景背景(JDK 8 的小痛点)

JDK 8 的 java.time 包已能满足大部分场景,但在一些细节场景下仍有不足:比如无法直接获取「当月第一天/最后一天」、无法便捷地格式化带时区的日期、缺少一些常用的日期计算工具。

JDK 17 日期时间 API 增强实战(对比 JDK 8)
增强 1:便捷获取当月/当年的第一天/最后一天(JDK 8 需手动计算,JDK 17 简化)
// JDK 8:获取当月第一天(手动计算,繁琐)LocalDate todayJdk8 =LocalDate.now();LocalDate firstDayOfMonthJdk8 = todayJdk8.with(TemporalAdjusters.firstDayOfMonth());LocalDate lastDayOfMonthJdk8 = todayJdk8.with(TemporalAdjusters.lastDayOfMonth());// JDK 17:获取当月第一天(语法简化,更直观,与 JDK 8 效果一致,可读性更强)LocalDate todayJdk17 =LocalDate.now();LocalDate firstDayOfMonthJdk17 = todayJdk17.atStartOfMonth().toLocalDate();LocalDate lastDayOfMonthJdk17 = todayJdk17.withDayOfMonth(todayJdk17.lengthOfMonth());// JDK 17:获取当年第一天/最后一天(简化)LocalDate firstDayOfYearJdk17 = todayJdk17.withDayOfYear(1);LocalDate lastDayOfYearJdk17 = todayJdk17.withDayOfYear(todayJdk17.lengthOfYear());
增强 2:DateTimeFormatter 增强,支持更多时区格式(JDK 17 更稳定)
// JDK 8:格式化带时区日期(支持,但语法繁琐)ZonedDateTime zonedDateTimeJdk8 =ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));DateTimeFormatter formatterJdk8 =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzz");String formattedTimeJdk8 = zonedDateTimeJdk8.format(formatterJdk8);// JDK 17:格式化带时区日期(语法简化,支持更多时区格式,更稳定)ZonedDateTime zonedDateTimeJdk17 =ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));DateTimeFormatter formatterJdk17 =DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzzz",Locale.CHINA);String formattedTimeJdk17 = zonedDateTimeJdk17.format(formatterJdk17);System.out.println("JDK 8 格式化结果:"+ formattedTimeJdk8);System.out.println("JDK 17 格式化结果:"+ formattedTimeJdk17);
增强 3:Duration 与 Period 增强,支持更便捷的时间间隔计算(JDK 17 补充更多方法)
// JDK 8:计算两个日期的间隔(仅支持基础计算)LocalDate startDateJdk8 =LocalDate.of(2026,1,1);LocalDate endDateJdk8 =LocalDate.of(2026,1,29);Period periodJdk8 =Period.between(startDateJdk8, endDateJdk8);System.out.println("JDK 8 日期间隔:"+ periodJdk8.getDays()+" 天");// JDK 17:计算两个日期/时间的间隔(增强方法,支持更精细的计算)LocalDate startDateJdk17 =LocalDate.of(2026,1,1);LocalDate endDateJdk17 =LocalDate.of(2026,1,29);Period periodJdk17 =Period.between(startDateJdk17, endDateJdk17);// 新增:判断间隔是否为正数boolean isPositive = periodJdk17.isPositive();// 新增:获取总月数(JDK 8 需手动计算)long totalMonths =ChronoUnit.MONTHS.between(startDateJdk17, endDateJdk17);// 时间间隔增强:计算两个时间的毫秒差(JDK 17 更简洁)LocalTime startTimeJdk17 =LocalTime.of(10,0,0);LocalTime endTimeJdk17 =LocalTime.of(10,30,0);Duration durationJdk17 =Duration.between(startTimeJdk17, endTimeJdk17);long millis = durationJdk17.toMillis();// 转换为毫秒System.out.println("JDK 17 日期间隔:"+ periodJdk17.getDays()+" 天");System.out.println("JDK 17 总月数:"+ totalMonths);System.out.println("JDK 17 时间间隔(毫秒):"+ millis);
增强 4:避免闰秒问题,支持更精准的时间处理(JDK 17 底层优化)

JDK 17 对 java.time 包的底层实现进行了优化,自动处理闰秒问题,无需手动配置,在金融、电信等对时间精度要求极高的场景下,比 JDK 8 更稳定、更精准。


三、JDK 8 vs JDK 17:平台级特性实战(性能与架构升级)

如果说语法特性是「提升开发效率」,那么平台级特性就是「提升系统性能与架构可维护性」。JDK 17 的平台级特性核心是「模块化系统(JPMS)」和「高性能垃圾收集器(ZGC)」,还有「清理历史包袱」,这些特性对企业级项目的升级至关重要。

3.1 模块化系统(JPMS):告别「类路径地狱」(JDK 17 完善版)

什么是模块化系统?

模块化系统(Java Platform Module System,简称 JPMS),简单来说就是「给 Java 项目做「拆分」和「标记」」——把一个庞大的项目拆分成一个个独立的「小模块」(比如用户模块、订单模块、商品模块),每个模块都明确标注「我依赖哪些其他模块」「我对外提供哪些功能(API)」「我允许哪些模块访问我的内部内容」。

你可以把它类比成「快递打包与运输」:

  • JDK 8 的「类路径」:相当于把所有货物(代码、依赖 Jar 包)都堆在一个大箱子里,没有分类,找东西难、容易互相挤压(类冲突)、搬运起来重(启动慢);——结果就是:拿错包裹、偷看隐私、甚至把别人的快递当成自己的。
  • JDK 17 的「模块化」:相当于把货物按品类拆分成一个个小快递盒(模块),每个盒子上都贴了标签(module-info.java),写着「里面装了什么(对外 API)」「需要搭配哪些盒子使用(依赖模块)」「哪些人可以打开看内部(反射权限)」,分类清晰、搬运轻松、不易出错。

它的核心目标就是解决 JDK 8 中经典的「类路径地狱」问题,让项目更清晰、更易维护、运行更高效。

在这里插入图片描述

场景背景(JDK 8 的痛点)

在 JDK 8 中,我们所有的业务代码、引入的第三方 Jar 包(比如 Spring、MyBatis),都被一股脑地放在「类路径(classpath)」这个「大箱子」里,开发和运维时会遇到 4 个让人头疼的问题:

  1. 类冲突(最常见):不同 Jar 包里有同名的类(比如两个不同版本的工具包都有 com.example.utils.StringUtils),JVM 加载时不知道该选哪个,直接抛出 ClassNotFoundExceptionNoClassDefFoundError,排查起来要翻遍所有 Jar 包,耗时耗力;
  2. 隐式依赖(维护噩梦):项目里用了某个 Jar 包的类,但没有明确声明「我依赖这个 Jar 包」,后续其他人接手项目、或部署到新环境时,很容易漏掉这个依赖,导致项目启动失败;
  3. 启动缓慢(微服务痛点):JVM 启动时,会把类路径下所有的类都加载一遍,哪怕有些类从头到尾都没被使用过(比如某个 Jar 包的冷门功能),不仅启动速度慢,还占用大量内存,在微服务场景下,这个问题会被放大(很多微服务需要快速启动、快速扩容);
  4. 无强封装(耦合过高):只要类是 public 修饰的,其他任何代码都能访问它,哪怕这个类是某个框架的内部实现类、不希望被外部调用,这就导致项目之间耦合过高,后续修改内部代码时,很容易影响到外部调用者。
在这里插入图片描述

JDK 17 怎么写?(模块化系统实战)

模块化系统的核心是 module-info.java 文件——它是每个模块的「身份证」和「说明书」,放在每个模块的 src/main/java 根目录下(和你的业务包(如 com.example.demo)同级)。

下面我们以「一个简单的 Spring Boot 项目」为例,一步步教你搭建模块化项目。

在这里插入图片描述
步骤 1:准备环境(新手必看,确保环境无误)

首先要确保你的开发环境满足 2 个条件,避免后续踩坑:

  1. JDK 版本:安装 OpenJDK 17(推荐 Adoptium Temurin 17),并在 IDEA 中配置为项目的 JDK;
  2. Spring Boot 版本:选择 2.7.x 及以上(最好 3.0.x 及以上),这些版本对 JDK 17 的模块化有良好适配(低版本 Spring Boot 不支持模块化);
  3. IDEA 版本:2021.3 及以上,支持自动识别 module-info.java 文件,提供语法提示和校验。
步骤 2:创建项目结构(模块化项目的目录结构)

我们先创建一个简单的模块化项目,项目名称为 demo-jdk17-module,包含 2 个模块:

  • 核心模块(demo-core):提供基础的 DTO 和工具类,对外暴露 API;
  • 业务模块(demo-business):依赖核心模块,实现具体的业务逻辑。

先看最终的项目目录结构(新手可直接照着创建):

demo-jdk17-module/ (项目根目录) ├── demo-core/ (核心模块,对外提供 API) │ ├── src/ │ │ ├── main/ │ │ │ ├── java/ │ │ │ │ ├── module-info.java (核心模块的「说明书」,必须有) │ │ │ │ └── com/ │ │ │ │ └── example/ │ │ │ │ └── core/ │ │ │ │ ├── dto/ (对外暴露的 DTO 包) │ │ │ │ │ └── UserDTO.java (数据载体类) │ │ │ │ └── util/ (对外暴露的工具类包) │ │ │ │ └── StringUtil.java (工具类) │ │ │ └── resources/ │ └── pom.xml (Maven 配置,依赖第三方 Jar 包) └── demo-business/ (业务模块,依赖核心模块) ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ ├── module-info.java (业务模块的「说明书」,必须有) │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── business/ │ │ │ ├── service/ (业务服务包) │ │ │ │ └── UserService.java (业务逻辑类) │ │ │ └── Application.java (项目启动类) │ │ └── resources/ └── pom.xml (Maven 配置,依赖 demo-core 模块) 
步骤 3:编写核心模块(demo-core)(分步实操)

核心模块的作用是「提供基础功能,对外暴露 API」,我们一步步来写:

子步骤 3.1:编写核心模块的业务代码

demo-corecom.example.core.util 包下,创建 StringUtil(工具类,对外提供字符串处理功能):

// demo-core 模块:对外暴露的工具类packagecom.example.core.util;publicclassStringUtil{// 对外提供的功能:判断字符串是否为空publicstaticbooleanisEmpty(String str){return str ==null|| str.trim().isEmpty();}// 对外提供的功能:字符串脱敏(隐藏手机号中间 4 位)publicstaticStringdesensitizePhone(String phone){if(isEmpty(phone)|| phone.length()!=11){return phone;}return phone.substring(0,3)+"****"+ phone.substring(7);}}

demo-corecom.example.core.dto 包下,创建 UserDTO(用 JDK 17 的 Record 类,简化数据载体类):

// demo-core 模块:对外暴露的 UserDTOpackagecom.example.core.dto;publicrecordUserDTO(String name,Integer age,String city){}
子步骤 3.2:创建核心模块的 module-info.java(关键)

demo-coresrc/main/java 根目录下,创建 module-info.java 文件(这是模块的「说明书」),编写如下内容:

// 步骤 1:声明模块名称(必须唯一,建议格式:公司/组织.项目.模块名,和包名对应)modulecom.example.demo.core{// 步骤 2:声明该模块依赖的其他模块(按需添加)// 这里核心模块只用到 JDK 自带的基础功能,无需依赖第三方模块,暂时只声明依赖 java.base(JDK 核心模块,可省略,默认自动依赖)requiresjava.base;// 步骤 3:声明对外暴露的包(其他模块只能访问这些包下的类,核心!)// 暴露 dto 包:其他模块可以使用 UserDTOexportscom.example.core.dto;// 暴露 util 包:其他模块可以使用 StringUtilexportscom.example.core.util;// 步骤 4:如果该模块需要被其他框架反射访问(比如核心模块有配置类),则用 opens 声明(当前核心模块无配置类,暂时省略)}

大白话解释这个文件

  • module com.example.demo.core:告诉 JVM,这个模块叫 com.example.demo.core,是唯一的;
  • requires java.base:告诉 JVM,这个模块需要依赖 JDK 的核心模块(java.base 包含了 java.langjava.util 等基础包,所有模块都会默认依赖,可省略不写);
  • exports com.example.core.dto:告诉 JVM,把 com.example.core.dto 这个包对外暴露,其他模块可以自由访问这个包下的所有 public 类;
  • 没有被 exports 声明的包(如果有的话),其他模块无法访问,这就是「强封装」,避免内部代码被外部随意调用。
在这里插入图片描述
子步骤 3.3:配置核心模块的 pom.xml(Maven 配置)
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><!-- 模块基本信息 --><groupId>com.example</groupId><artifactId>demo-core</artifactId><version>1.0.0</version><name>demo-core</name><description>模块化项目核心模块</description><!-- JDK 版本配置 --><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties></project>
步骤 4:编写业务模块(demo-business)(分步实操)

业务模块的作用是「依赖核心模块,实现具体业务逻辑」,我们继续一步步来写:

子步骤 4.1:配置业务模块的 pom.xml(依赖核心模块)

首先在业务模块的 pom.xml 中,添加对 demo-core 模块的依赖,这样才能使用核心模块的功能:

<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><!-- 模块基本信息 --><groupId>com.example</groupId><artifactId>demo-business</artifactId><version>1.0.0</version><name>demo-business</name><description>模块化项目业务模块</description><!-- 父工程(Spring Boot 依赖管理),方便引入 Spring Boot 依赖 --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.0</version><relativePath/></parent><!-- JDK 版本配置 --><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><!-- 依赖配置 --><dependencies><!-- 1. 依赖核心模块 demo-core --><dependency><groupId>com.example</groupId><artifactId>demo-core</artifactId><version>1.0.0</version></dependency><!-- 2. Spring Boot 核心依赖(方便启动项目) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency></dependencies></project>
子步骤 4.2:编写业务模块的业务代码

demo-businesscom.example.business 包下,创建 Application(Spring Boot 启动类):

// demo-business 模块:项目启动类packagecom.example.business;importcom.example.business.service.UserService;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ConfigurableApplicationContext;@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[] args){// 启动 Spring Boot 项目ConfigurableApplicationContext context =SpringApplication.run(Application.class, args);// 获取 UserService 并测试业务功能UserService userService = context.getBean(UserService.class); userService.createUser("张三",20,"北京","13800138000");}}

demo-businesscom.example.business.service 包下,创建 UserService(业务逻辑类,依赖核心模块的 UserDTOStringUtil):

// demo-business 模块:业务逻辑类,使用核心模块的功能packagecom.example.business.service;importcom.example.core.dto.UserDTO;importcom.example.core.util.StringUtil;importorg.springframework.stereotype.Service;@ServicepublicclassUserService{// 业务功能:创建用户并返回脱敏后的手机号(使用核心模块的 UserDTO 和 StringUtil)publicUserDTOcreateUser(String name,Integer age,String city,String phone){// 使用核心模块的 StringUtil 进行字符串判空if(StringUtil.isEmpty(name)){thrownewIllegalArgumentException("用户名不能为空");}// 使用核心模块的 StringUtil 进行手机号脱敏String desensitizedPhone =StringUtil.desensitizePhone(phone);System.out.println("脱敏后的手机号:"+ desensitizedPhone);// 使用核心模块的 UserDTO 封装返回结果returnnewUserDTO(name, age, city);}}
子步骤 4.3:创建业务模块的 module-info.java(关键,重点讲解 requiresopens

demo-businesssrc/main/java 根目录下,创建 module-info.java 文件,编写如下内容:

// 步骤 1:声明模块名称(唯一,与核心模块对应)modulecom.example.demo.business{// 步骤 2:声明该模块依赖的其他模块(核心!)// 2.1 依赖核心模块 com.example.demo.core(才能使用核心模块的 UserDTO 和 StringUtil)requirescom.example.demo.core;// 2.2 依赖 Spring Boot 核心模块(才能使用 Spring Boot 的注解和功能)requiresspring.boot;requiresspring.boot.autoconfigure;requiresspring.context;// 步骤 3:声明允许反射的包(核心!Spring Boot 需要反射扫描注解(如 @Service、@SpringBootApplication))// 开放 com.example.business 包给 spring.context(Spring 上下文模块),允许其反射访问包下的所有类openscom.example.businesstospring.context;// 开放 com.example.business.service 包给 spring.context,允许其反射扫描 @Service 注解openscom.example.business.servicetospring.context;// 步骤 4:声明对外暴露的包(当前业务模块是最终模块,无需对外暴露功能,可省略 exports)}

大白话解释这个文件(新手重点理解)

  1. requires com.example.demo.core:告诉 JVM,这个业务模块需要依赖核心模块 com.example.demo.core,只有声明了这个依赖,才能使用核心模块中 exports 暴露的 UserDTOStringUtil
  2. requires spring.boot:告诉 JVM,这个模块需要依赖 Spring Boot 的核心模块,才能使用 @SpringBootApplication@Service 等注解;
  3. opens com.example.business to spring.context:这是 Spring Boot 模块化项目的「关键配置」——Spring Boot 是通过「反射」来扫描注解(如 @SpringBootApplication@Service)并创建 Bean 的,而模块化系统默认不允许外部模块反射访问内部包,所以需要用 opens 明确声明「允许 spring.context 模块反射访问 com.example.business 包下的所有类」,否则 Spring Boot 无法扫描到启动类和业务类,项目会启动失败;
  4. 为什么没有 exports?因为当前业务模块是「最终消费模块」,不需要对外暴露任何功能给其他模块,所以可以省略 exports,这也是模块化的「强封装」优势——只在需要的时候对外暴露功能。
步骤 5:运行项目,验证模块化是否生效
  1. 先使用 Maven 打包核心模块 demo-core(IDEA 中右键 demo-core 模块 → Mavencleaninstall,将核心模块安装到本地仓库);
  2. 右键运行 demo-business 模块的 Application 类的 main 方法,启动 Spring Boot 项目;
  3. 查看控制台输出,若出现「脱敏后的手机号:138****8000」,说明模块化项目搭建成功——业务模块成功依赖并使用了核心模块的功能,Spring Boot 也成功通过反射扫描到了注解,模块化配置生效!
补充:核心关键字对比(新手必记,更通俗的解读)
关键字作用范围反射可见核心用途(大白话)类比(快递盒)
module--声明一个模块,给模块起一个唯一的名字给快递盒贴一个唯一的快递单号
requires编译时 + 运行时声明当前模块依赖哪些其他模块,没有这些模块就无法运行标注这个快递盒需要和哪些其他快递盒一起使用,才能完成功能
exports编译时 + 运行时声明当前模块对外暴露哪些包,其他模块只能访问这些包下的类标注快递盒上「可以对外展示的内容」,其他人只能看这些内容,不能看内部其他东西
opens仅运行时声明当前模块允许哪些外部模块反射访问哪些包(主要用于 Spring、MyBatis 等框架)标注快递盒上「允许特定人员打开查看内部的区域」,只有指定人员能打开,其他人不能
3. 模块化的核心优势(对比 JDK 8)(更通俗的解读)
  1. 告别类冲突(最核心的优势):每个模块都有自己的「命名空间」,即使不同模块中有同名类(比如 com.example.utils.StringUtils),只要不对外暴露,JVM 就不会混淆,彻底解决了 JDK 8 中「翻遍所有 Jar 包找冲突类」的噩梦;
  2. 依赖清晰(维护更轻松):每个模块的 module-info.java 都明确标注了「依赖哪些模块」,后续其他人接手项目时,一看这个文件就知道模块之间的依赖关系,不用再去翻 pom.xml 或猜测「这个类是从哪个 Jar 包来的」;
  3. 启动更快(微服务福音):JVM 启动时,只会加载模块 requires 声明的依赖模块,以及模块中实际用到的类,不会像 JDK 8 那样加载类路径下所有的类,在微服务场景下,启动速度通常能提升 30%+,内存占用也会大幅降低;
  4. 强封装性(降低耦合):只有被 exports 声明的包才会对外暴露,其他包都是模块的「内部实现」,外部模块无法访问,这就避免了「外部模块随意调用内部实现类」的问题,降低了项目耦合度,后续修改内部代码时,只要不改变对外暴露的 API,就不会影响到其他模块。
4. Spring Boot 落地建议(企业级)(更实用的解读)
  1. 环境选择优先:一定要选择 Spring Boot 2.7.x 及以上版本(最好 3.x),低版本 Spring Boot 不支持 JDK 17 的模块化,会出现各种反射扫描失败的问题;
  2. 模块划分要合理:按「业务边界」划分模块(比如用户模块 user-module、订单模块 order-module、商品模块 product-module),不要过度模块化(比如把一个简单的项目拆分成十几个模块),否则会增加维护成本(模块之间的依赖关系会变得复杂);
  3. exports 遵循「最小暴露原则」:只对外暴露必要的 API 包(比如 DTO、接口包),内部实现包(比如 service 实现类、mapper 包)一定不要 exports,避免外部模块依赖内部实现,导致后续修改困难;
  4. opens 遵循「最小开放原则」:只开放需要被框架反射扫描的包(比如 Spring Boot 的启动类包、@Service 注解所在的包),并且只开放给需要的框架模块(比如 to spring.context),不要随意使用 opens com.example.demo;(开放整个模块),否则会失去模块化的强封装优势;
  5. 逐步迁移,不要一步到位:如果你的项目是从 JDK 8 迁移过来的,不要一开始就把整个项目改成模块化,可以先从某个独立的小模块(比如工具类模块)开始尝试,熟悉模块化的配置和坑点后,再逐步推广到整个项目。

你现在的JDK 17项目没有使用module-info.java文件,本质上是运行在「兼容模式」下,和JDK 8的运行模式高度相似,但并非完全一致。下面我会先给你一个通俗的整体结论,再详细拆解「存在」和「不存在」module-info.java的核心区别,最后补充你关心的「无module-info.java时,JDK 17和JDK 8的细微差异」。

项目没有 module-info.java 或把它删除了,会则么样?
  1. module-info.java(你的当前项目):JDK 17会把整个项目(包括所有业务代码、第三方Jar包)都当作一个「匿名模块」(也叫「未命名模块」,unnamed module),这个模块的行为和JDK 8的「类路径(classpath)」模式几乎一致,你不用做任何额外配置,就能像JDK 8一样开发和运行项目,这是JDK为了向下兼容设计的。
  2. module-info.java(模块化项目):项目会被拆分成多个「具名模块」(named module),每个模块都有明确的依赖、暴露和反射权限配置,这是JDK 9+引入的全新模块化模式,解决了JDK 8类路径的诸多痛点,但需要额外配置module-info.java

简单说:module-info.java = JDK 17兼容JDK 8模式(省心但保留旧痛点);有module-info.java = JDK 17全新模块化模式(稍复杂但解决旧痛点)

3.2 垃圾收集器:从 JDK 8 Parallel 到 JDK 17 G1/ZGC 选型指南

(1) 垃圾收集器演进与 ZGC 详解:JDK 8 → JDK 17 权威对比(基于 OpenJDK 官方标准)
  1. JDK 8 默认 GC:Parallel GC(并行收集器)
    • G1 在 JDK 8 中是可选、非默认,必须手动加 -XX:+UseG1GC 才能启用。
  2. JDK 9 → JDK 17 官方默认 GC:G1 GC
    • 从 JDK 9 开始,OpenJDK 将默认 GC 从 Parallel GC 改为 G1 GC,并一直延续到 JDK 17、JDK 18、JDK 19、JDK 20
    • JDK 17 官方默认仍然是 G1 GC,不是 ZGC
  3. ZGC 在 JDK 17 中的定位
    • ZGC 在 JDK 15 已结束实验,成为正式生产特性;
    • JDK 17 中 ZGC 是高可用、可生产的正式 GC,但不是默认 GC
    • 必须通过 -XX:+UseZGC 手动显式启用。
  4. ZGC 成为默认 GC 的版本
    • JDK 21 开始,OpenJDK 才将默认 GC 从 G1 改为 ZGC。
    • 很多资料混淆了「JDK 17 正式可用」和「JDK 17 默认启用」,这是最常见的错误。
在这里插入图片描述

(2)官方依据与版本变迁表

下面是 OpenJDK 官方确定的、跨版本默认 GC 变迁,无任何争议

JDK 版本官方默认垃圾收集器是否支持 ZGCZGC 状态
JDK 8Parallel GC(Parallel Scavenge + Parallel Old)不支持未引入
JDK 9G1 GC不支持未引入
JDK 10G1 GC不支持未引入
JDK 11G1 GC实验特性(Linux x64),需 UnlockExperimentalVMOptions
JDK 12G1 GC实验特性
JDK 13G1 GC实验特性
JDK 14G1 GC实验特性
JDK 15G1 GC正式特性,不再是实验版
JDK 16G1 GC正式特性
JDK 17G1 GC正式生产可用,非默认
JDK 18G1 GC正式可用
JDK 19G1 GC正式可用
JDK 20G1 GC正式可用
JDK 21+ZGC默认启用
权威来源:OpenJDK JDK 17 默认 GC 源码与发布日志JEP 248(Make G1 the Default Garbage Collector,JDK 9)JEP 377(ZGC: A Scalable Low-Latency Garbage Collector,JDK 15 正式化)JEP 439(Generational ZGC,JDK 21 分代 ZGC 并成为默认)

(3) 版本默认 GC 权威澄清

在学习和迁移 JDK 17 时,默认垃圾收集器是高频误区,我们先以官方标准明确结论:

  1. JDK 17 中的 ZGC:正式可用,需手动开启
    ZGC 从 JDK 15 开始结束实验状态,成为正式生产特性
    JDK 17 中 ZGC 功能完整、跨平台支持(Linux / Windows / macOS)、稳定性极高,是低延迟场景的首选,但必须手动配置启用,不属于默认行为。
  2. ZGC 成为默认的正确版本:JDK 21+
    从 JDK 21 这个新 LTS 版本开始,ZGC 才正式成为 OpenJDK 的默认垃圾收集器。

JDK 9 ~ JDK 17 默认:G1 GC
从 JDK 9 开始,OpenJDK 通过 JEP 248 将默认 GC 切换为 G1 GC,并一直保持到 JDK 17。
G1 的设计目标是在吞吐量与延迟之间做平衡,支持可预测的停顿目标(-XX:MaxGCPauseMillis),适合绝大多数中大型服务、常规微服务架构。也就是说:

JDK 17 官方标准默认 GC 是 G1 GC,不是 ZGC。

JDK 8 默认:Parallel GC
JDK 8 的默认收集器是 Parallel GC(年轻代 Parallel Scavenge,老年代 Parallel Old),以高吞吐量为设计目标,多 GC 线程并行工作,但全程 STW,适合对延迟不敏感、追求 CPU 利用率的离线计算、大数据任务。G1 GC 在 JDK 8 中已经成熟,但并非默认,必须显式开启:

java -XX:+UseG1GC -jar app.jar 

(4) 为什么建议是 ZGC?

ZGC(Z Garbage Collector)是 Oracle 主导开发的一款可扩展、低延迟、高吞吐的垃圾收集器,核心设计目标是:

  • 停顿时间极短且不随堆大小增长
  • 支持从几百 MB 到 TB 级别的堆内存
  • 几乎所有核心阶段(标记、转移、重定位)都与应用线程并发执行
  • 全程无长时 STW,停顿时间稳定控制在 10ms 以内,大多数场景在 1~3ms

ZGC 使用彩色指针 + 读屏障技术实现并发对象搬迁,不再依赖传统分代的强绑定,特别适合:

  • 金融交易、支付、秒杀等低延迟要求严苛的在线业务
  • 微服务、云原生应用
  • 大堆内存(16GB 及以上)场景
  • 要求高可用、无明显卡顿的核心系统
在这里插入图片描述

(5) JDK 8 默认 Parallel GC 与 JDK 17 G1/ZGC 对比
核心特性对比
特性JDK 8 Parallel GC(默认)JDK 17 G1 GC(官方默认)JDK 17 ZGC(正式可选)
设计目标高吞吐量优先吞吐量与延迟平衡极低延迟优先,兼顾吞吐
典型停顿时间年轻代 10~50ms,Full GC 可达 数百ms~秒级目标 200ms 以内,实际常见 50~200ms,极端情况仍有长停顿全程 <10ms,几乎不受堆大小影响
堆内存适配适合中小堆,大于 32GB 后表现明显下降适合中到大堆,推荐 8~64GB原生支持大堆,可轻松支撑 128GB~TB 级
内存碎片老年代压缩时 STW 较长分区回收,碎片可控,但仍可能触发并发模式失效并发复制整理,几乎无内存碎片
并发能力几乎无并发,全程 STW部分阶段并发,仍有较长 STW绝大多数工作并发,STW 极短
JDK 17 启用方式-XX:+UseParallelGC默认,无需参数必须显式加 -XX:+UseZGC
适用场景离线计算、批处理、数据分析通用微服务、后端服务、常规业务低延迟核心服务、大内存服务、金融/电商核心链路
关键痛点对比
JDK 8 Parallel GC 的问题
  • 依赖完全 STW,高并发下延迟毛刺严重
  • Full GC 一旦触发,可能造成秒级卡顿,影响用户体验和服务可用性
  • 大堆下 GC 时间急剧上升,难以支撑 16GB 以上堆稳定运行
  • 不适合对响应时间有 SLA 要求的在线系统
JDK 17 默认 G1 GC 的问题
  • 仍然存在明显的 STW 阶段,尤其在并发标记失败、晋升失败时,会切换到单线程 Full GC,导致不可预测的长停顿
  • 堆越大,GC 开销和停顿波动越明显
  • 对超大规模堆(64GB+)的优化不如 ZGC 彻底
JDK 17 ZGC 的优势(对比前两者)
  • 停顿时间不随堆容量、存活对象数量变化,16GB 和 1TB 堆的停顿差异极小
  • 全程无长时 STW,业务几乎无感知
  • 并发回收、并发整理,不会因碎片触发 Full GC
  • 大堆下表现远超 Parallel 和 G1
  • JDK 17 跨平台稳定,生产环境可放心使用

(6) JDK 17 中 G1 与 ZGC 的启用方式(正确实操)

这里是你项目中真正可用、不会踩坑的配置方式:

方式 1:使用 JDK 17 默认 GC(G1)

直接运行,无需任何 GC 参数,就是 G1:

# JDK 17 默认:G1 GC java -Xms8g -Xmx8g -jar demo.jar 
方式 2:显式指定 G1(推荐明确化,避免环境差异)
# 显式启用 G1(JDK 17 标准默认) java -XX:+UseG1GC -Xms8g -Xmx8g -jar demo.jar 
方式 3:启用 ZGC(低延迟场景推荐)

ZGC 在 JDK 17 中必须手动开启,典型生产启动参数:

# JDK 17 启用 ZGC,固定堆,控制最大停顿 java -XX:+UseZGC \ -Xms16g -Xmx16g \ -XX:MaxGCPauseMillis=5\ -jar demo.jar 

常用 ZGC 调优参数(生产级):

  • -XX:MaxGCPauseMillis=N:目标最大停顿,默认 10ms,可设 5ms/3ms
  • -XX:ZGCThreads=N:设置 GC 并发线程数,默认基于 CPU 核心自动计算
  • -XX:+ZGenerational:JDK 21+ 分代 ZGC,JDK 17 不支持
  • -Xms-Xmx 建议设为相同,避免动态伸缩带来额外开销
在这里插入图片描述
验证当前使用的 GC

运行后可通过以下命令确认,避免配置不生效:

# 查看进程使用的 GC jinfo -flag UseG1GC <pid> jinfo -flag UseZGC <pid> jinfo -flag UseParallelGC <pid>

只有一个会返回 + 启用状态,其余为 -


(7) 项目选型建议(结合你 JDK 17 实际场景)

根据你目前JDK 17、未使用 module-info.java、传统 Web/微服务架构的情况,给出清晰的选型策略:

场景 A:常规业务,无极端低延迟要求
  • 选择:直接使用 JDK 17 默认 G1 GC
  • 理由:开箱即用,兼容性最好,社区资料最多,满足绝大多数微服务、后台管理、CRUD 类应用
  • 配置:不额外加任何 GC 参数
场景 B:核心链路、支付、订单、秒杀、低延迟 SLA
  • 选择:手动启用 ZGC
  • 理由:消除 GC 毛刺,保证高并发下响应稳定,用户体验与监控指标更平滑
  • 配置:使用 -XX:+UseZGC + 固定堆 + 合理停顿目标
场景 C:离线计算、数据同步、批处理任务
  • 选择:Parallel GC
  • 理由:极致吞吐量,GC 总耗时更低,适合非交互式、CPU 密集型任务
  • 配置:-XX:+UseParallelGC

(8) 最终总结
  1. JDK 8 默认:Parallel GC,G1 为可选方案。
  2. JDK 9 ~ JDK 17 官方默认:G1 GC,这是标准、通用、最稳妥的选择。
  3. JDK 17 中 ZGC 是正式生产级特性,但并非默认,必须通过 -XX:+UseZGC 手动开启。
  4. ZGC 真正成为默认 GC 的版本是 JDK 21+,不要与 JDK 17 混淆。
  5. 对于 JDK 17 项目:
    • 普通业务 → 默认 G1,省心稳定
    • 低延迟核心服务 → 主动切换 ZGC,获得毫秒级停顿
    • 批处理场景 → 可考虑切回 Parallel GC 提升吞吐

3.3 清理历史包袱:向云原生对齐(JDK 17 的隐形优势)

JDK 17 作为长期支持(LTS)版本,一个重要的优化方向是「剔除过时冗余组件、简化运行时依赖」,移除和弃用了一批在 JDK 8 中早已被行业淘汰、不适应云原生/容器化场景的功能。这些变化看似是「减少功能」,实则是让 Java 程序更轻量化、启动更快、内存占用更低,更契合容器化、微服务的部署要求(容器场景对镜像大小、启动速度、资源占用极为敏感)。

下面我们按「彻底移除」和「标记弃用」两类,结合包全名、JDK 8 示例、JDK 17 替代方案进行详细拆解:


1. 彻底移除的过时组件(JDK 8 中已无用,JDK 17 完全删除,相关 API 不可再用)

这类组件在 JDK 8 中已经处于「废弃但仍可使用」状态,由于现代技术的替代方案已经成熟,JDK 17 直接彻底移除了相关 API 和实现,减少 JVM 运行时内存占用和启动开销。

在这里插入图片描述
(1)Applet 相关组件(浏览器端 Java 技术,已被前端技术淘汰)
  • 核心包全名
    • java.applet(核心 Applet 类包)
    • javax.swing.JApplet(Swing 相关 Applet 扩展)
  • JDK 17 中的变化
    彻底移除 java.applet 包和 JApplet 类,上述代码在 JDK 17 中会直接报「包不存在」编译错误,无法编译和运行。
  • 替代方案
    前端场景使用 HTML5、Vue、React、Angular;需要前后端交互的场景,使用「Java 后端(微服务)+ HTTP/HTTPS 接口」的架构。

JDK 8 中的使用示例(已无实际价值,仅作历史参考)
Applet 是早期用于在浏览器中运行 Java 程序的技术,随着 HTML5、JavaScript 的兴起,早已被各大浏览器淘汰(Chrome、Firefox 均已移除 Applet 支持)。

// JDK 8:创建一个简单的 Applet(无法在现代浏览器中运行,仅作语法示例)packagecom.example.deprecated;importjava.applet.Applet;// 核心包:java.appletimportjava.awt.Graphics;// 继承 Applet 类,实现简单绘图publicclassMyTestAppletextendsApplet{// JDK 8 中重写 paint 方法,在浏览器中绘制文本@Overridepublicvoidpaint(Graphics g){ g.drawString("Hello Applet (JDK 8 过时组件)",20,20);}}
(2)Security Manager(老旧安全机制,配置复杂、性能低下)
  • 核心包全名java.lang.SecurityManager(核心类)、java.lang.SecurityException(异常类)
  • JDK 17 中的变化
    彻底移除 SecurityManager 类及其相关 API,System.setSecurityManager() 方法也被移除,上述代码在 JDK 17 中会报「方法不存在」编译错误。
  • 替代方案(云原生场景推荐)
    1. 容器级安全:使用 Docker/K8s 的容器权限限制(如非 root 用户运行、挂载只读目录、限制网络访问);
    2. 操作系统级安全:使用 Linux 账户权限、SELinux/AppArmor 进行系统资源控制;
    3. 应用级安全:使用 Spring Security、Shiro 等框架进行应用内的权限控制(如接口访问、数据权限)。

JDK 8 中的使用示例(老旧安全控制,现已淘汰)
Security Manager 是 JDK 1.0 引入的安全机制,用于限制 Java 程序的系统访问权限(如文件读写、网络连接),但配置复杂(需要编写策略文件 java.policy)、性能开销大,且无法满足现代云原生场景的安全需求。

// JDK 8:使用 Security Manager 限制程序文件读写权限packagecom.example.deprecated;importjava.lang.SecurityManager;// 核心类:java.lang.SecurityManagerimportjava.io.FileWriter;publicclassMyTestSecurityManager{publicstaticvoidmain(String[] args){// 步骤 1:启用 Security Manager(JDK 8 中默认关闭,需手动启用)System.setSecurityManager(newSecurityManager());// 步骤 2:尝试写入文件,会被 Security Manager 拦截(抛出 SecurityException)try(FileWriter writer =newFileWriter("test.txt")){ writer.write("Test Security Manager");}catch(Exception e){ e.printStackTrace();// 会抛出:java.lang.SecurityException: Permission denied}}}

同时需要在 java.policy 文件中配置权限,否则上述代码会被拦截,配置繁琐且难以维护。

(3)CORBA(公共对象请求代理体系,老旧分布式通信机制)
  • 核心包全名
    • org.omg.CORBA(核心 CORBA 规范包)
    • javax.rmi.CORBA(RMI 与 CORBA 集成包)
    • org.omg.CosNaming(CORBA 命名服务包)
  • JDK 17 中的变化
    彻底移除所有 CORBA 相关包(org.omg.*javax.rmi.CORBA),上述代码在 JDK 17 中会报「包不存在」编译错误,无法运行。
  • 替代方案(云原生分布式场景推荐)
    1. 轻量级接口:HTTP/HTTPS + RESTful API(使用 Spring Boot、Spring Cloud 实现);
    2. 高性能跨语言通信:gRPC(支持 HTTP/2,生成跨语言客户端/服务端代码);
    3. 分布式服务治理:Spring Cloud Alibaba、Dubbo(提供服务注册发现、负载均衡、容错等能力)。

JDK 8 中的使用示例(老旧分布式通信,仅作历史参考)
CORBA 是早期用于跨语言、跨进程分布式通信的机制,设计复杂、部署繁琐,现已被 HTTP、gRPC、RESTful API 等现代技术替代。

// JDK 8:CORBA 客户端简单示例(仅作语法参考,需配套 CORBA 服务端,现已无人使用)packagecom.example.deprecated;importorg.omg.CORBA.ORB;// 核心包:org.omg.CORBAimportorg.omg.CosNaming.NamingContextExt;importorg.omg.CosNaming.NamingContextExtHelper;publicclassMyTestCORBAClient{publicstaticvoidmain(String[] args){try{// 步骤 1:初始化 CORBA ORB(对象请求代理)ORB orb = ORB.init(args,null);// 步骤 2:获取命名服务上下文org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");NamingContextExt ncRef =NamingContextExtHelper.narrow(objRef);// 步骤 3:查找远程 CORBA 对象(省略后续调用逻辑)String objName ="MyCORBAService";org.omg.CORBA.Object remoteObj = ncRef.resolve_str(objName);}catch(Exception e){ e.printStackTrace();}}}

2. 标记为弃用的过时特性(JDK 8 中仍可使用,JDK 17 标记为@Deprecated,未来将移除)

这类特性在 JDK 17 中仍可编译和运行,但会被标记为「过时」(编译器会给出警告),且 OpenJDK 官方明确表示「未来版本将彻底移除」,不建议在新代码中使用,已有代码应逐步迁移至替代方案。

在这里插入图片描述
(1)RMI Activation(老旧分布式对象激活机制)
  • 核心包全名java.rmi.activation(核心激活机制包)
  • JDK 17 中的变化
    java.rmi.activation 包下所有类均被标记为 @Deprecated(forRemoval = true)(表示未来将彻底移除),上述代码在 JDK 17 中编译时会出现「过时API」警告,但仍可运行。
  • 替代方案(云原生分布式场景推荐)
    1. 微服务架构:Spring Cloud、Dubbo(提供服务注册发现、按需扩容、容错等能力,无需手动管理对象激活);
    2. 远程方法调用:gRPC、RESTful API(更轻量、更易部署,支持容器化和云原生扩缩容);
    3. 无状态服务设计:将服务设计为无状态,通过 K8s 进行自动扩缩容,替代「对象激活」的需求。

JDK 8 中的使用示例(老旧特性,不适合云原生)
RMI Activation 是 RMI(远程方法调用)的扩展特性,用于「按需激活」远程休眠对象,配置复杂、可靠性低,且不支持容器化部署(依赖固定端口、本地文件系统),现已被现代微服务架构替代。

// JDK 8:RMI Activation 简单示例(仅作语法参考,现已不推荐使用)packagecom.example.deprecated;importjava.rmi.activation.Activatable;// 核心包:java.rmi.activationimportjava.rmi.activation.ActivationID;importjava.rmi.Remote;importjava.rmi.RemoteException;// 步骤 1:定义远程接口publicinterfaceMyRemoteServiceextendsRemote{StringsayHello()throwsRemoteException;}// 步骤 2:实现可激活的远程对象(继承 Activatable)publicclassMyRemoteServiceImplextendsActivatableimplementsMyRemoteService{// 构造方法(用于激活)publicMyRemoteServiceImpl(ActivationID id,java.rmi.MarshalledObject<?> data)throwsRemoteException{super(id,0);// 调用父类构造方法,启用激活机制}// 实现远程方法@OverridepublicStringsayHello()throwsRemoteException{return"Hello RMI Activation (JDK 8 过时特性)";}}
(2)Finalization(老旧对象销毁机制,基于 finalize() 方法)
  • 核心类/方法java.lang.Object.finalize()(所有对象的父类方法,用于对象销毁前的资源清理)
  • JDK 17 中的变化
    Object.finalize() 方法被标记为 @Deprecated(forRemoval = true),上述代码在 JDK 17 中编译时会出现「过时API」警告,且 OpenJDK 官方明确表示「未来将彻底移除该方法」。
    1. 自动资源关闭:使用 try-with-resources 语句(JDK 7 引入,自动关闭实现 AutoCloseable 接口的资源,无需手动关闭);
    1. 手动资源关闭:对于未实现 AutoCloseable 接口的资源,在 finally 块中手动关闭(确保无论是否出现异常,都能执行资源清理);
    2. 框架级资源管理:使用 Spring、MyBatis 等框架的内置资源管理机制(如 Spring 的 @PreDestroy 注解、MyBatis 的 SqlSession 自动关闭)。

替代方案(推荐,安全、可靠、高性能)

// JDK 17 推荐:try-with-resources 自动关闭资源(安全、可靠)packagecom.example.modern;importjava.io.FileInputStream;importjava.io.IOException;publicclassMyTestTryWithResources{publicstaticvoidmain(String[] args){// try-with-resources 自动关闭 FileInputStream(实现了 AutoCloseable 接口)try(FileInputStream inputStream =newFileInputStream("test.txt")){// 业务逻辑:读取文件(省略)}catch(IOException e){ e.printStackTrace();}}}

JDK 8 中的使用示例(性能低下、不可靠,现已不推荐)
Finalization 是 JDK 1.0 引入的对象销毁机制,当 JVM 进行垃圾回收时,会调用待回收对象的 finalize() 方法进行资源清理,但该机制存在严重问题:① 执行时机不确定(依赖 GC 触发,可能长时间不执行);② 性能低下(会延长 GC 停顿时间);③ 不可靠(可能被 JVM 忽略,无法保证一定执行)。

// JDK 8:使用 finalize() 方法进行资源清理(老旧方式,存在诸多问题)packagecom.example.deprecated;importjava.io.FileInputStream;importjava.io.IOException;publicclassMyTestFinalize{privateFileInputStream inputStream;// 构造方法:打开文件流publicMyTestFinalize(String filePath)throwsIOException{this.inputStream =newFileInputStream(filePath);}// 重写 finalize() 方法:尝试关闭文件流(不可靠)@Overrideprotectedvoidfinalize()throwsThrowable{super.finalize();// 尝试关闭文件流,但无法保证该方法一定被执行if(inputStream !=null){try{ inputStream.close();System.out.println("文件流通过 finalize() 关闭(不可靠)");}catch(IOException e){ e.printStackTrace();}}}publicstaticvoidmain(String[] args)throwsIOException{// 创建对象,未手动关闭资源,依赖 finalize() 清理MyTestFinalize test =newMyTestFinalize("test.txt");}}

3. 核心价值(对比 JDK 8,向云原生对齐)

这些「移除」和「弃用」并非简单的「功能删减」,而是 Java 语言向云原生场景转型的重要一步,核心价值体现在 3 点:

  1. 轻量化运行时,适配容器化部署
    移除无用组件(如 Applet、CORBA)后,JVM 运行时内存占用减少、启动速度提升,Java 镜像体积更小(容器镜像可减少数十 MB),更适合在 K8s 等容器编排平台中快速部署、快速扩缩容。
  2. 消除老旧技术债务,提升程序可靠性
    弃用性能低下、不可靠的特性(如 Finalization、RMI Activation),引导开发者使用现代、可靠的替代方案,减少因老旧技术带来的线上问题(如资源泄露、延迟毛刺、部署失败),提升程序的稳定性和可维护性。
  3. 贴合云原生生态,降低转型成本
    移除的老旧技术均有成熟的云原生替代方案,且这些替代方案(如 RESTful API、gRPC、Spring Cloud)已成为行业标准,JDK 17 的清理工作让 Java 与云原生生态的兼容性更好,企业向云原生转型时的技术衔接更平滑,学习成本和迁移成本更低。

总结说明
  1. JDK 17 彻底移除了 java.appletjava.lang.SecurityManagerorg.omg.CORBA 等过时包,相关代码无法在 JDK 17 中编译运行,需迁移至现代替代方案。
  2. JDK 17 标记 java.rmi.activationObject.finalize() 为弃用(未来将移除),新代码应避免使用,优先采用 try-with-resources、微服务等替代方案。
  3. 这些清理工作让 Java 程序更轻量化、更可靠,更契合容器化、云原生场景,是企业转型云原生的重要技术支撑。
  4. 迁移建议:老旧项目升级 JDK 17 时,先排查是否使用了这些过时 API,再逐步替换为推荐的现代技术方案,避免直接升级导致编译或运行错误。

四、JDK 8 迁移到 JDK 17:避坑指南(新手友好,可落地实操)

很多开发者担心「从 JDK 8 迁移到 JDK 17 会有兼容问题、踩坑太多」,其实 JDK 提供了良好的向下兼容,且核心坑点有明确的解决方案。下面是分步、可落地、带示例的迁移指南,新手也能平稳完成迁移。

4.1 第一步:前期准备(打好基础,避免开局踩坑)

迁移前的准备工作是关键,直接决定后续迁移是否顺畅,核心要做好「版本选型」和「环境准备」,附带具体示例和推荐配置。

在这里插入图片描述
1. JDK 版本选型与安装(明确可落地的方案)

迁移的核心是选择稳定、免费、长期支持的 JDK 17 发行版,不推荐使用 Oracle JDK(商业授权收费),优先选择开源免费的社区版。

推荐 JDK 17 发行版优势下载地址
Eclipse Temurin 17(Adoptium)开源免费、长期支持(LTS)、跨平台(Windows/Linux/macOS)、企业级常用、兼容性最好Adoptium 官网
Amazon Corretto 17亚马逊维护、开源免费、优化了云原生场景、兼容 AWS 生态Amazon Corretto 官网
OpenJDK 17(官方)原生开源、无第三方修改、适合对纯净度要求高的场景OpenJDK 官网
实操示例(以 Linux 环境安装 Eclipse Temurin 17 为例)
# 1. 下载 JDK 17 压缩包(x64 架构,根据系统选择)wget https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.9%2B9/OpenJDK17U-jdk_x64_linux_hotspot_17.0.9_9.tar.gz # 2. 解压到指定目录(推荐 /usr/local/jdk/)mkdir -p /usr/local/jdk tar -zxvf OpenJDK17U-jdk_x64_linux_hotspot_17.0.9_9.tar.gz -C /usr/local/jdk/ # 3. 配置环境变量(编辑 /etc/profile,全局生效)vim /etc/profile # 4. 在文件末尾添加以下配置(指定 JDK 17 路径)exportJAVA_HOME=/usr/local/jdk/jdk-17.0.9+9 exportPATH=$JAVA_HOME/bin:$PATHexportCLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar # 5. 使环境变量生效source /etc/profile # 6. 验证安装是否成功(输出 JDK 17 版本即成功) java -version javac -version 
验证结果示例(成功标识)
openjdk version "17.0.9" 2023-10-17 LTS OpenJDK Runtime Environment Temurin-17.0.9+9 (build 17.0.9+9-LTS) OpenJDK 64-Bit Server VM Temurin-17.0.9+9 (build 17.0.9+9-LTS, mixed mode, sharing) 
2. 框架与构建工具版本选型(核心兼容,避免框架报错)

JDK 17 对老旧框架兼容性较差,必须升级到支持 JDK 17 的版本,以下是企业级常用框架的最低兼容版本和推荐版本,附带 Maven/Gradle 配置示例。

技术栈最低兼容版本(支持 JDK 17)推荐版本(稳定、优化多)备注
Spring Boot2.7.x3.1.x / 3.2.x(LTS)Spring Boot 3.x 基于 Spring 6.x,对 JDK 17 优化更彻底
Spring Framework5.3.x6.0.x / 6.1.xSpring 6.x 要求 JDK 17+,是未来主流
MyBatis3.5.93.5.13修复了 JDK 17 反射兼容问题
MyBatis-Plus3.5.33.5.4.1适配 Spring Boot 3.x 和 JDK 17
Maven3.8.13.8.8低版本 Maven 对 JDK 17 编译支持有问题
Gradle7.08.5(LTS)Gradle 8.x 对 JDK 17 构建速度优化明显
实操示例 1:Maven 配置(pom.xml 核心修改)
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><!-- 1. 升级 Spring Boot 父工程(推荐 3.1.x) --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.9</version><relativePath/></parent><!-- 2. 配置项目基本信息 --><groupId>com.example</groupId><artifactId>jdk8-to-17-demo</artifactId><version>1.0.0</version><!-- 3. 核心配置:指定 JDK 17 编译版本(关键,避免编译报错) --><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><mybatis.version>3.5.13</mybatis.version></properties><!-- 4. 依赖配置(以 MyBatis 为例,指定兼容 JDK 17 的版本) --><dependencies><!-- Spring Boot 核心依赖(自动适配 JDK 17) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- MyBatis 依赖(指定推荐版本) --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis.version}</version></dependency></dependencies><!-- 5. 构建配置:指定 Maven 编译插件版本(支持 JDK 17) --><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.11.0</version><!-- 支持 JDK 17 的最低版本 --><configuration><source>17</source><target>17</target><encoding>UTF-8</encoding></configuration></plugin></plugins></build></project>
实操示例 2:IDEA 中配置 JDK 17(避免开发环境报错)
  1. 打开 IDEA,进入「File → Project Structure → Project」;
  2. 「Project SDK」选择已安装的 Eclipse Temurin 17;
  3. 「Project language level」选择「17 - Sealed types, always strict floating-point semantics」;
  4. 进入「File → Project Structure → Modules」,所有模块的「SDK」均选择 JDK 17;
  5. 进入「File → Settings → Build, Execution, Deployment → Compiler → Java Compiler」,所有模块的「Target bytecode version」均选择 17;
  6. 保存配置,重启 IDEA 使配置生效。
3. 前期备份与环境隔离(避免影响线上环境)
  1. 代码备份:迁移前通过 Git 提交所有代码,创建专门的迁移分支(如 feature/jdk8-to-17),避免迁移过程中污染主分支;
  2. 环境隔离:搭建独立的迁移测试环境(与线上环境配置一致,但不对外提供服务),所有迁移操作先在测试环境进行,不直接修改线上环境的 JDK 和框架版本;

依赖缓存清理:清理 Maven/Gradle 本地缓存,避免老旧依赖包干扰迁移,命令如下:

# Maven 清理本地缓存 mvn clean dependency:purge-local-repository # Gradle 清理本地缓存 gradle clean build --refresh-dependencies 

4.2 第二步:分步迁移(从易到难,新手友好)

推荐采用「非核心模块→核心模块」的逐步迁移策略,先从小型、无依赖的非核心模块试点,积累经验后再迁移核心业务模块,降低迁移风险。

在这里插入图片描述
阶段 1:非核心业务模块试点迁移(推荐优先选择工具类模块、监控模块)

这类模块通常功能单一、依赖较少、无复杂业务逻辑,是迁移试点的最佳选择,具体步骤如下:

步骤 1:修改模块的 JDK 编译版本(对应 pom.xml/ build.gradle)

参考 4.1 中的 Maven 配置示例,修改该模块的 pom.xml,指定 JDK 17 编译版本和兼容的框架版本。

步骤 2:排查并替换过时/移除的 API(核心避坑,适配 IDEA )

使用 IDEA 的「代码检查功能」自动排查模块中 JDK 17 不兼容的 API,步骤如下:

  1. 在弹出的窗口中,选择「Whole project」(或仅当前模块),点击「OK」;
  2. 等待代码检查完成,在「Inspection Results」窗口中,展开「Java」分类,找到以下关键检查项(不同 IDEA 版本名称可能略有差异):
    • 「Java language level migration aids」:专门检测「当前 JDK 版本不支持的旧语法/API」,包含 JDK 17 移除/弃用的 API(如 Object.finalize()sun.misc 内部 API);
    • 「Compiler issues」:检测「无法解析的引用」(对应“Unresolved references”),比如 JDK 17 已移除的 java.appletorg.omg.CORBA 等包;
  3. 点击对应检查项,IDEA 会列出所有相关代码位置,逐个处理:
    • 若检查结果显示「Use of API marked for removal」:说明该 API 已被 JDK 17 彻底移除(如 SecurityManager),需直接删除代码并替换为现代方案;
    • 若检查结果显示「Access to an internal API that may be removed in a future release」:说明该 API 是 JDK 内部包(如 sun.misc),需替换为标准 API(如 java.util.Base64);
    • 若检查结果显示「Cannot resolve symbol ‘xxx’」:说明该类/包在 JDK 17 中已不存在(如 java.applet.Applet),需删除相关导入和代码。

「Declaration redundancy」→「Unused declaration」:可辅助排查因 API 移除导致的“无用代码”(比如依赖旧 API 的废弃类/方法);

在这里插入图片描述

右键点击目标模块 → 选择「Analyze → Inspect Code」;

在这里插入图片描述

补充:手动精准搜索(避免遗漏)
若 IDEA 检查未覆盖到所有问题,可通过「全局搜索」主动排查 JDK 17 不兼容的 API:

  1. 按下 Ctrl+Shift+F(Windows)/ Cmd+Shift+F(Mac)打开全局搜索;
  2. 输入以下关键词,逐个搜索并处理:
    • 移除的包/类:java.appletorg.omg.CORBAjava.rmi.activationSecurityManager
    • 弃用的方法:finalize()
    • 内部 API:sun.miscsun.reflectsun.security
实操示例:替换 JDK 内部 API sun.misc.BASE64Encoder 为标准 API
// JDK 8 中使用的老旧内部 API(JDK 17 中无法访问,编译报错)packagecom.example.old;importsun.misc.BASE64Encoder;// JDK 17 中移除,无法导入publicclassOldBase64Util{// 对字符串进行 BASE64 编码publicstaticStringencode(String str){if(str ==null|| str.isEmpty()){return"";}BASE64Encoder encoder =newBASE64Encoder();return encoder.encode(str.getBytes());// 老旧 API,无法在 JDK 17 中运行}}// JDK 17 中推荐的标准 API(兼容、稳定、无访问限制)packagecom.example.modern;importjava.util.Base64;// JDK 8 及以上均支持,标准 APIpublicclassModernBase64Util{// 对字符串进行 BASE64 编码publicstaticStringencode(String str){if(str ==null|| str.isEmpty()){return"";}Base64.Encoder encoder =Base64.getEncoder();return encoder.encodeToString(str.getBytes());// 标准 API,支持 JDK 17}}
步骤 3:编译模块并修复编译错误

使用 Maven/Gradle 编译该模块,排查并修复所有编译错误,命令如下:

# Maven 编译该模块(进入模块目录) mvn clean compile # Gradle 编译该模块(进入模块目录) gradle clean compileJava 

常见编译错误及解决方案:

  1. 「包不存在」:通常是移除的 API(如 java.applet),直接删除相关代码并替换为替代方案;
  2. 「无法访问的内部 API」:通常是 sun.misc 等 JDK 内部包,替换为标准 API;
  3. 「方法不存在」:通常是过时方法被移除,替换为框架提供的替代方法。
步骤 4:单元测试与本地运行验证
  1. 本地运行该模块(如工具类模块,编写测试主类调用核心方法),验证功能是否正常,无运行时异常;
  2. 记录迁移过程中遇到的问题和解决方案,形成迁移文档,为后续核心模块迁移提供参考。

运行该模块的所有单元测试(确保通过率 100%),命令如下:

mvn clean test
阶段 2:核心业务模块迁移(基于试点经验,稳步推进)

核心业务模块(如用户模块、订单模块)通常依赖复杂、业务逻辑繁琐、有大量外部调用,迁移时需更谨慎,具体步骤如下:

步骤 1:升级核心依赖框架(如 Spring Boot、MyBatis)

按照 4.1 中的版本选型,升级核心依赖框架版本,注意:

  • Spring Boot 2.7.x 迁移到 3.x 有部分不兼容变更(如 javax 包改为 jakarta 包),需重点排查;
  • 升级后优先清理无用依赖(如 spring-boot-starter-web 已包含核心依赖,无需重复引入)。
实操示例:解决 Spring Boot 3.x 中 javax 改为 jakarta 的兼容问题

JDK 17 + Spring Boot 3.x 中,Java EE 相关 API(如 javax.servletjavax.persistence)已迁移到 jakarta 包下,需修改相关导入:

// JDK 8 + Spring Boot 2.x 中的代码(javax 包)packagecom.example.old;importjavax.servlet.http.HttpServletRequest;// 老旧包importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;@RestControllerpublicclassOldUserController{@GetMapping("/user")publicStringgetUser(HttpServletRequest request){return"Hello User";}}// JDK 17 + Spring Boot 3.x 中的代码(jakarta 包)packagecom.example.modern;importjakarta.servlet.http.HttpServletRequest;// 新包,兼容 JDK 17importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;@RestControllerpublicclassModernUserController{@GetMapping("/user")publicStringgetUser(HttpServletRequest request){return"Hello User";}}
步骤 2:排查并解决模块化相关问题(无 module-info.java 重点关注反射权限)

如果你的项目未使用 module-info.java(大多数传统项目),重点关注「反射访问限制」问题,常见场景是 Spring、MyBatis 等框架反射扫描注解时权限不足,报错示例:

java.lang.IllegalAccessException: class org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor cannot access a member of class com.example.service.UserService with modifiers "private" 

解决方案:

  1. 升级框架到推荐版本(如 Spring Boot 3.1.x),框架已适配 JDK 17 反射权限;

若仍有报错,添加 JVM 启动参数 --add-opens 手动开放权限,示例:

# 开放 java.base 包给所有匿名模块 java --add-opens java.base/java.lang=ALL-UNNAMED -jar jdk8-to-17-demo.jar 
步骤 3:解决集合相关兼容问题(不可变集合避坑)

JDK 17 中 List.of()Set.of()Map.of() 返回的是不可变集合,无法进行添加、删除、修改操作,若强行操作会抛出 UnsupportedOperationException,这是新手常见坑点。

实操示例:不可变集合避坑与解决方案
// JDK 17 中常见错误:对不可变集合进行修改操作packagecom.example.demo;importjava.util.List;publicclassImmutableCollectionDemo{publicstaticvoidmain(String[] args){// 用 List.of() 创建不可变集合List<String> userList =List.of("张三","李四","王五");// 错误操作:尝试添加元素,会抛出 UnsupportedOperationException userList.add("赵六");// 运行时异常!}}// 解决方案 1:如需可变集合,使用 new ArrayList<>() 等传统方式创建packagecom.example.demo;importjava.util.ArrayList;importjava.util.List;publicclassMutableCollectionDemo1{publicstaticvoidmain(String[] args){// 创建可变 ArrayListList<String> userList =newArrayList<>(); userList.add("张三"); userList.add("李四"); userList.add("王五"); userList.add("赵六");// 正常运行,无异常}}// 解决方案 2:将不可变集合转换为可变集合packagecom.example.demo;importjava.util.ArrayList;importjava.util.List;publicclassMutableCollectionDemo2{publicstaticvoidmain(String[] args){// 先创建不可变集合,再转换为可变 ArrayListList<String> immutableUserList =List.of("张三","李四","王五");List<String> mutableUserList =newArrayList<>(immutableUserList); mutableUserList.add("赵六");// 正常运行,无异常}}
步骤 4:集成测试与性能测试
  1. 集成测试:调用核心业务接口(如用户查询、订单创建),验证端到端功能是否正常,无数据异常、无接口报错;
  2. 性能测试:使用 JMeter、Gatling 等工具进行压测,对比 JDK 8 和 JDK 17 下的接口响应时间、吞吐量、GC 停顿时间,确保迁移后性能不下降(通常 JDK 17 性能会有 10%-30% 提升);
  3. GC 监控:使用 JVisualVM、Prometheus + Grafana 监控 GC 情况,若使用 ZGC 需验证停顿时间是否控制在 10ms 以内。
阶段 3:全项目迁移与整合
  1. 排查跨模块依赖问题(如模块间 API 调用是否正常、依赖是否冲突);
  2. 全项目集成测试,确保所有业务流程正常,无遗漏问题。

所有模块迁移完成后,整合所有模块,进行全项目编译和运行,命令如下:

mvn clean package -Dmaven.test.skip=false 

4.3 第三步:核心坑点汇总与避坑锦囊(新手必看)

迁移过程中最容易踩的坑都在这里,附带明确的解决方案,帮你快速避坑。

坑点类型具体问题解决方案
移除的 API使用 java.appletorg.omg.CORBASecurityManager 等移除的 API,编译报错1. 直接删除相关代码;2. 替换为现代替代方案(如 RESTful API 替代 CORBA、容器安全替代 SecurityManager)
内部 API 访问限制使用 sun.miscsun.reflect 等 JDK 内部 API,编译报错「无法访问」1. 替换为 JDK 标准 API(如 java.util.Base64 替代 sun.misc.BASE64Encoder);2. 若无法替换,添加 JVM 参数 --add-exports 临时开放(不推荐长期使用)
反射权限不足Spring、MyBatis 等框架反射扫描注解时,抛出 IllegalAccessException1. 升级框架到推荐版本;2. 添加 JVM 参数 --add-opens 开放反射权限;3. 避免将业务类设为 private,尽量使用 publicprotected
不可变集合修改使用 List.of() 创建集合后,尝试添加/删除元素,抛出 UnsupportedOperationException1. 如需可变集合,使用 new ArrayList<>()new HashMap<>() 创建;2. 不可变集合转换为可变集合后再修改
javax 包兼容问题Spring Boot 3.x 中,javax.servletjavax.persistence 等包不存在,编译报错1. 替换为 jakarta 包(如 jakarta.servlet.http.HttpServletRequest);2. 升级相关依赖(如 Hibernate 6.x 支持 jakarta 包)
编译版本不匹配IDEA 中配置 JDK 17,但编译时仍使用 JDK 8,抛出版本不兼容错误1. 检查 IDEA 的 Project Structure 和 Java Compiler 配置,确保所有模块均使用 JDK 17;2. 检查 pom.xml/ build.gradle,确保编译版本指定为 17

4.4 第四步:上线与后续优化(平稳落地,持续优化)

  1. 灰度发布:迁移完成后,先进行灰度发布(仅将部分流量切到 JDK 17 环境),监控系统运行状态,无异常后再逐步扩大流量,直至全量切换;
  2. 线上监控:上线后 72 小时内重点监控系统的响应时间、吞吐量、GC 情况、异常率,发现问题及时回滚(回滚方案:切回 JDK 8 环境,使用主分支代码);
  3. 后续优化
    • 逐步使用 JDK 17 新特性(如密封类、记录类、增强的 switch 表达式)优化代码,提升代码简洁性和性能;
    • 对于核心业务模块,尝试启用 ZGC 垃圾收集器,进一步提升系统低延迟性能;
    • 定期清理无用代码和依赖,减少技术债务,为后续迁移到 JDK 21 做准备。
在这里插入图片描述

升级 jdk17 总结

  1. 迁移前需做好「版本选型、环境准备、代码备份」,优先选择 Eclipse Temurin 17 和 Spring Boot 3.x,避免开局踩坑。
  2. 采用「非核心模块→核心模块」的逐步迁移策略,先试点再推广,降低迁移风险,新手易落地。
  3. 核心避坑点是「移除/过时 API 替换、反射权限、不可变集合、javax 包兼容」,有明确的替代方案和配置示例。
  4. 上线后采用灰度发布,重点监控 72 小时,后续可利用 JDK 17 新特性和 ZGC 持续优化系统性能。
  5. 迁移过程中做好文档记录,为后续团队迁移和版本升级提供参考。

五、结语:从 JDK 8 到 JDK 17,是Java的一次优雅演进

十年前,JDK 8 用 Lambda、Stream、Optional 开启了 Java 的现代化大门,让开发者从繁琐的模板代码中解放出来;
五年后,JDK 17 LTS 用密封类、模块化、ZGC 沉淀了 Java 现代化的成果,让 Java 更适合云原生、高并发、低延迟的现代业务场景。

对于开发者来说,升级 JDK 17 不仅仅是「学习新特性」,更是「提升自身竞争力」——在企业招聘中,JDK 17 经验已成为中高级开发者的加分项;
对于企业来说,升级 JDK 17 不仅仅是「版本迭代」,更是「降本增效、向云原生转型」的关键一步。

JDK 8 虽已退出主流舞台,但它的设计思想依然影响着 Java 的演进;JDK 17 虽已是当前主流,但它也不是终点——Java 正在以「优雅、稳定、高效」的节奏,迈向更光明的未来。

最后,送给所有 Java 开发者一句话:「技术的进步,从来不是颠覆过去,而是在过去的基础上,让未来变得更美好。」


总结

  1. LTS 是长期支持版本,JDK 17 支持到 2029 年,是企业级应用的首选,对比 JDK 8 具备更优的稳定性和安全性。
  2. JDK 17 语法特性以「简化编码」为核心,密封类解决继承混乱、模式匹配简化类型转换、文本块告别字符串拼接,对比 JDK 8 大幅提升开发效率。
  3. JDK 17 平台特性以「提升性能和可维护性」为核心,模块化系统告别类路径地狱、支持 ZGC 实现毫秒级 GC 停顿,对比 JDK 8 更适配云原生场景。
  4. 从 JDK 8 迁移到 JDK 17 需注意框架版本兼容,优先在非核心模块试点,可平稳完成升级。

希望这篇文章,能成为你技术升级路上的一份可靠参考。

Read more

C++中的继承

继承是 C++ 面向对象三大特性(封装、继承、多态)的核心,核心价值是代码复用和层次化类设计。本文全面覆盖继承的语法、对象模型、构造析构、同名成员处理、多继承及菱形继承等关键知识点,并附注意事项和示例。 一、继承的基本语法 1. 语法格式 // 基类(父类):被继承的类class 基类名 {// 成员(属性、方法)};// 派生类(子类):继承基类class 派生类名 : 继承方式 基类名 {// 子类扩展的成员(可新增/重写)}; 2. 核心概念 基类(父类):提供通用属性 / 方法的类(如Person); 派生类(子类):复用基类成员,同时扩展自身功能的类(如Student); 继承的本质:子类拥有基类的所有成员(private成员虽不可直接访问,但仍占用内存)

By Ne0inhk
C++之模版详解(进阶)

C++之模版详解(进阶)

目录 1. 非类型模板参数 2. 类模板的特化 2.1 函数模板特化 2.2 类模版特化 3. 模板的分离编译 1. 非类型模板参数 模版参数有两种,一种叫类型模版参数,一种叫做非类型模版参数。今天我们来讲讲非类型模版参数。 template <int N> 中的 int N 就是典型的非类型模板参数。这里的 int 是参数的类型,而 N 是参数名,它接收的是一个具体的常量值,而非像普通类型模板参数(如 template <typename T>)那样接收一个 “类型”。 两者核心区别就是: * 类型模板参数:传递 “类型”(如 T

By Ne0inhk
C++ 模板再升级:非类型参数、特化技巧(含全特化与偏特化)、分离编译破解

C++ 模板再升级:非类型参数、特化技巧(含全特化与偏特化)、分离编译破解

✨ 孤廖:个人主页 🎯 个人专栏:《C++:从代码到机器》 🎯 个人专栏:《Linux系统探幽:从入门到内核》 🎯 个人专栏:《算法磨剑:用C++思考的艺术》 折而不挠,中不为下 文章目录 * 前言 * 正文 * 1. 非类型模板参数 * 2. 模板的特化 * 2.1 概念 * 2.2 函数模板特化 * 2.3 类模板特化 * 2.3.1 全特化 * 2.3.2 偏特化 * 2.3.3 类模板特化应用示例 * 3 模板分离编译 * 3.1 什么是分离编译 * 3.2 模板的分离编译

By Ne0inhk