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

1.3 为什么企业优先选择 JDK 17?(对比 JDK 8)
对于基础开发者来说,升级的核心动力是「更简洁的语法、更低的编码出错率」;对于企业来说,升级的核心动力是「降本增效、降低风险」,具体体现在 3 点:
- 长期支持,无后顾之忧:支持到 2029 年,无需频繁升级版本,减少版本迭代带来的适配成本,对比 JDK 8 即将停止全面支持,更具可持续性;
- 性能飙升,运维更轻松:ZGC 垃圾收集器默认可用(毫秒级停顿)、启动速度更快、内存占用更低,对比 JDK 8 的 G1 GC,在高并发、大内存场景下优势显著;
- 安全性增强,抵御外部威胁:默认启用强封装、移除过时不安全组件、新增多种加密算法,对比 JDK 8 的老旧安全机制,能更好地抵御现代网络攻击;
- 语法简化,开发效率更高:密封类、模式匹配、简化的集合操作等特性,对比 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};}
适用场景
- 领域模型设计(如支付方式、订单状态);
- 状态机开发(如流程节点、消息类型);
- 协议解析(如请求类型、响应格式)。
性能 & 安全价值
- 编译期安全:新增子类时,所有 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));}}核心特性与注意事项
- 不可变性:Record 类的成员变量默认是
private final,创建对象后无法修改字段值,线程安全,适合作为 DTO、VO 等数据载体; - 无需手动编写样板代码:编译器自动生成
getter(无get前缀)、equals()、hashCode()、toString(),大幅减少开发工作量; - 可自定义方法:Record 类中可以添加自定义方法(如业务校验、数据转换),增强灵活性;
- 不可继承: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 包,解决了旧 Date、Calendar 类的线程不安全、设计混乱等问题,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 个让人头疼的问题:
- 类冲突(最常见):不同 Jar 包里有同名的类(比如两个不同版本的工具包都有
com.example.utils.StringUtils),JVM 加载时不知道该选哪个,直接抛出ClassNotFoundException或NoClassDefFoundError,排查起来要翻遍所有 Jar 包,耗时耗力; - 隐式依赖(维护噩梦):项目里用了某个 Jar 包的类,但没有明确声明「我依赖这个 Jar 包」,后续其他人接手项目、或部署到新环境时,很容易漏掉这个依赖,导致项目启动失败;
- 启动缓慢(微服务痛点):JVM 启动时,会把类路径下所有的类都加载一遍,哪怕有些类从头到尾都没被使用过(比如某个 Jar 包的冷门功能),不仅启动速度慢,还占用大量内存,在微服务场景下,这个问题会被放大(很多微服务需要快速启动、快速扩容);
- 无强封装(耦合过高):只要类是
public修饰的,其他任何代码都能访问它,哪怕这个类是某个框架的内部实现类、不希望被外部调用,这就导致项目之间耦合过高,后续修改内部代码时,很容易影响到外部调用者。

JDK 17 怎么写?(模块化系统实战)
模块化系统的核心是 module-info.java 文件——它是每个模块的「身份证」和「说明书」,放在每个模块的 src/main/java 根目录下(和你的业务包(如 com.example.demo)同级)。
下面我们以「一个简单的 Spring Boot 项目」为例,一步步教你搭建模块化项目。

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

(2)官方依据与版本变迁表
下面是 OpenJDK 官方确定的、跨版本默认 GC 变迁,无任何争议:
| JDK 版本 | 官方默认垃圾收集器 | 是否支持 ZGC | ZGC 状态 |
|---|---|---|---|
| JDK 8 | Parallel GC(Parallel Scavenge + Parallel Old) | 不支持 | 未引入 |
| JDK 9 | G1 GC | 不支持 | 未引入 |
| JDK 10 | G1 GC | 不支持 | 未引入 |
| JDK 11 | G1 GC | 是 | 实验特性(Linux x64),需 UnlockExperimentalVMOptions |
| JDK 12 | G1 GC | 是 | 实验特性 |
| JDK 13 | G1 GC | 是 | 实验特性 |
| JDK 14 | G1 GC | 是 | 实验特性 |
| JDK 15 | G1 GC | 是 | 正式特性,不再是实验版 |
| JDK 16 | G1 GC | 是 | 正式特性 |
| JDK 17 | G1 GC | 是 | 正式生产可用,非默认 |
| JDK 18 | G1 GC | 是 | 正式可用 |
| JDK 19 | G1 GC | 是 | 正式可用 |
| JDK 20 | G1 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 时,默认垃圾收集器是高频误区,我们先以官方标准明确结论:
- JDK 17 中的 ZGC:正式可用,需手动开启
ZGC 从 JDK 15 开始结束实验状态,成为正式生产特性。
JDK 17 中 ZGC 功能完整、跨平台支持(Linux / Windows / macOS)、稳定性极高,是低延迟场景的首选,但必须手动配置启用,不属于默认行为。 - 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) 最终总结
- JDK 8 默认:Parallel GC,G1 为可选方案。
- JDK 9 ~ JDK 17 官方默认:G1 GC,这是标准、通用、最稳妥的选择。
- JDK 17 中 ZGC 是正式生产级特性,但并非默认,必须通过
-XX:+UseZGC手动开启。 - ZGC 真正成为默认 GC 的版本是 JDK 21+,不要与 JDK 17 混淆。
- 对于 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 中会报「方法不存在」编译错误。 - 替代方案(云原生场景推荐):
- 容器级安全:使用 Docker/K8s 的容器权限限制(如非 root 用户运行、挂载只读目录、限制网络访问);
- 操作系统级安全:使用 Linux 账户权限、SELinux/AppArmor 进行系统资源控制;
- 应用级安全:使用 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 中会报「包不存在」编译错误,无法运行。 - 替代方案(云原生分布式场景推荐):
- 轻量级接口:HTTP/HTTPS + RESTful API(使用 Spring Boot、Spring Cloud 实现);
- 高性能跨语言通信:gRPC(支持 HTTP/2,生成跨语言客户端/服务端代码);
- 分布式服务治理: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」警告,但仍可运行。 - 替代方案(云原生分布式场景推荐):
- 微服务架构:Spring Cloud、Dubbo(提供服务注册发现、按需扩容、容错等能力,无需手动管理对象激活);
- 远程方法调用:gRPC、RESTful API(更轻量、更易部署,支持容器化和云原生扩缩容);
- 无状态服务设计:将服务设计为无状态,通过 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 官方明确表示「未来将彻底移除该方法」。- 自动资源关闭:使用
try-with-resources语句(JDK 7 引入,自动关闭实现AutoCloseable接口的资源,无需手动关闭);
- 自动资源关闭:使用
- 手动资源关闭:对于未实现
AutoCloseable接口的资源,在finally块中手动关闭(确保无论是否出现异常,都能执行资源清理); - 框架级资源管理:使用 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 点:
- 轻量化运行时,适配容器化部署
移除无用组件(如 Applet、CORBA)后,JVM 运行时内存占用减少、启动速度提升,Java 镜像体积更小(容器镜像可减少数十 MB),更适合在 K8s 等容器编排平台中快速部署、快速扩缩容。 - 消除老旧技术债务,提升程序可靠性
弃用性能低下、不可靠的特性(如 Finalization、RMI Activation),引导开发者使用现代、可靠的替代方案,减少因老旧技术带来的线上问题(如资源泄露、延迟毛刺、部署失败),提升程序的稳定性和可维护性。 - 贴合云原生生态,降低转型成本
移除的老旧技术均有成熟的云原生替代方案,且这些替代方案(如 RESTful API、gRPC、Spring Cloud)已成为行业标准,JDK 17 的清理工作让 Java 与云原生生态的兼容性更好,企业向云原生转型时的技术衔接更平滑,学习成本和迁移成本更低。
总结说明
- JDK 17 彻底移除了
java.applet、java.lang.SecurityManager、org.omg.CORBA等过时包,相关代码无法在 JDK 17 中编译运行,需迁移至现代替代方案。 - JDK 17 标记
java.rmi.activation、Object.finalize()为弃用(未来将移除),新代码应避免使用,优先采用try-with-resources、微服务等替代方案。 - 这些清理工作让 Java 程序更轻量化、更可靠,更契合容器化、云原生场景,是企业转型云原生的重要技术支撑。
- 迁移建议:老旧项目升级 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 Boot | 2.7.x | 3.1.x / 3.2.x(LTS) | Spring Boot 3.x 基于 Spring 6.x,对 JDK 17 优化更彻底 |
| Spring Framework | 5.3.x | 6.0.x / 6.1.x | Spring 6.x 要求 JDK 17+,是未来主流 |
| MyBatis | 3.5.9 | 3.5.13 | 修复了 JDK 17 反射兼容问题 |
| MyBatis-Plus | 3.5.3 | 3.5.4.1 | 适配 Spring Boot 3.x 和 JDK 17 |
| Maven | 3.8.1 | 3.8.8 | 低版本 Maven 对 JDK 17 编译支持有问题 |
| Gradle | 7.0 | 8.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(避免开发环境报错)
- 打开 IDEA,进入「File → Project Structure → Project」;
- 「Project SDK」选择已安装的 Eclipse Temurin 17;
- 「Project language level」选择「17 - Sealed types, always strict floating-point semantics」;
- 进入「File → Project Structure → Modules」,所有模块的「SDK」均选择 JDK 17;
- 进入「File → Settings → Build, Execution, Deployment → Compiler → Java Compiler」,所有模块的「Target bytecode version」均选择 17;
- 保存配置,重启 IDEA 使配置生效。
3. 前期备份与环境隔离(避免影响线上环境)
- 代码备份:迁移前通过 Git 提交所有代码,创建专门的迁移分支(如
feature/jdk8-to-17),避免迁移过程中污染主分支; - 环境隔离:搭建独立的迁移测试环境(与线上环境配置一致,但不对外提供服务),所有迁移操作先在测试环境进行,不直接修改线上环境的 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,步骤如下:
- 在弹出的窗口中,选择「Whole project」(或仅当前模块),点击「OK」;
- 等待代码检查完成,在「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.applet、org.omg.CORBA等包;
- 「Java language level migration aids」:专门检测「当前 JDK 版本不支持的旧语法/API」,包含 JDK 17 移除/弃用的 API(如
- 点击对应检查项,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),需删除相关导入和代码。
- 若检查结果显示「Use of API marked for removal」:说明该 API 已被 JDK 17 彻底移除(如
「Declaration redundancy」→「Unused declaration」:可辅助排查因 API 移除导致的“无用代码”(比如依赖旧 API 的废弃类/方法);

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

补充:手动精准搜索(避免遗漏)
若 IDEA 检查未覆盖到所有问题,可通过「全局搜索」主动排查 JDK 17 不兼容的 API:
- 按下
Ctrl+Shift+F(Windows)/Cmd+Shift+F(Mac)打开全局搜索; - 输入以下关键词,逐个搜索并处理:
- 移除的包/类:
java.applet、org.omg.CORBA、java.rmi.activation、SecurityManager; - 弃用的方法:
finalize(); - 内部 API:
sun.misc、sun.reflect、sun.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 常见编译错误及解决方案:
- 「包不存在」:通常是移除的 API(如
java.applet),直接删除相关代码并替换为替代方案; - 「无法访问的内部 API」:通常是
sun.misc等 JDK 内部包,替换为标准 API; - 「方法不存在」:通常是过时方法被移除,替换为框架提供的替代方法。
步骤 4:单元测试与本地运行验证
- 本地运行该模块(如工具类模块,编写测试主类调用核心方法),验证功能是否正常,无运行时异常;
- 记录迁移过程中遇到的问题和解决方案,形成迁移文档,为后续核心模块迁移提供参考。
运行该模块的所有单元测试(确保通过率 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.servlet、javax.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" 解决方案:
- 升级框架到推荐版本(如 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:集成测试与性能测试
- 集成测试:调用核心业务接口(如用户查询、订单创建),验证端到端功能是否正常,无数据异常、无接口报错;
- 性能测试:使用 JMeter、Gatling 等工具进行压测,对比 JDK 8 和 JDK 17 下的接口响应时间、吞吐量、GC 停顿时间,确保迁移后性能不下降(通常 JDK 17 性能会有 10%-30% 提升);
- GC 监控:使用 JVisualVM、Prometheus + Grafana 监控 GC 情况,若使用 ZGC 需验证停顿时间是否控制在 10ms 以内。
阶段 3:全项目迁移与整合
- 排查跨模块依赖问题(如模块间 API 调用是否正常、依赖是否冲突);
- 全项目集成测试,确保所有业务流程正常,无遗漏问题。
所有模块迁移完成后,整合所有模块,进行全项目编译和运行,命令如下:
mvn clean package -Dmaven.test.skip=false 4.3 第三步:核心坑点汇总与避坑锦囊(新手必看)
迁移过程中最容易踩的坑都在这里,附带明确的解决方案,帮你快速避坑。
| 坑点类型 | 具体问题 | 解决方案 |
|---|---|---|
| 移除的 API | 使用 java.applet、org.omg.CORBA、SecurityManager 等移除的 API,编译报错 | 1. 直接删除相关代码;2. 替换为现代替代方案(如 RESTful API 替代 CORBA、容器安全替代 SecurityManager) |
| 内部 API 访问限制 | 使用 sun.misc、sun.reflect 等 JDK 内部 API,编译报错「无法访问」 | 1. 替换为 JDK 标准 API(如 java.util.Base64 替代 sun.misc.BASE64Encoder);2. 若无法替换,添加 JVM 参数 --add-exports 临时开放(不推荐长期使用) |
| 反射权限不足 | Spring、MyBatis 等框架反射扫描注解时,抛出 IllegalAccessException | 1. 升级框架到推荐版本;2. 添加 JVM 参数 --add-opens 开放反射权限;3. 避免将业务类设为 private,尽量使用 public 或 protected |
| 不可变集合修改 | 使用 List.of() 创建集合后,尝试添加/删除元素,抛出 UnsupportedOperationException | 1. 如需可变集合,使用 new ArrayList<>()、new HashMap<>() 创建;2. 不可变集合转换为可变集合后再修改 |
javax 包兼容问题 | Spring Boot 3.x 中,javax.servlet、javax.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 第四步:上线与后续优化(平稳落地,持续优化)
- 灰度发布:迁移完成后,先进行灰度发布(仅将部分流量切到 JDK 17 环境),监控系统运行状态,无异常后再逐步扩大流量,直至全量切换;
- 线上监控:上线后 72 小时内重点监控系统的响应时间、吞吐量、GC 情况、异常率,发现问题及时回滚(回滚方案:切回 JDK 8 环境,使用主分支代码);
- 后续优化:
- 逐步使用 JDK 17 新特性(如密封类、记录类、增强的 switch 表达式)优化代码,提升代码简洁性和性能;
- 对于核心业务模块,尝试启用 ZGC 垃圾收集器,进一步提升系统低延迟性能;
- 定期清理无用代码和依赖,减少技术债务,为后续迁移到 JDK 21 做准备。

升级 jdk17 总结
- 迁移前需做好「版本选型、环境准备、代码备份」,优先选择 Eclipse Temurin 17 和 Spring Boot 3.x,避免开局踩坑。
- 采用「非核心模块→核心模块」的逐步迁移策略,先试点再推广,降低迁移风险,新手易落地。
- 核心避坑点是「移除/过时 API 替换、反射权限、不可变集合、
javax包兼容」,有明确的替代方案和配置示例。 - 上线后采用灰度发布,重点监控 72 小时,后续可利用 JDK 17 新特性和 ZGC 持续优化系统性能。
- 迁移过程中做好文档记录,为后续团队迁移和版本升级提供参考。
五、结语:从 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 开发者一句话:「技术的进步,从来不是颠覆过去,而是在过去的基础上,让未来变得更美好。」
总结
- LTS 是长期支持版本,JDK 17 支持到 2029 年,是企业级应用的首选,对比 JDK 8 具备更优的稳定性和安全性。
- JDK 17 语法特性以「简化编码」为核心,密封类解决继承混乱、模式匹配简化类型转换、文本块告别字符串拼接,对比 JDK 8 大幅提升开发效率。
- JDK 17 平台特性以「提升性能和可维护性」为核心,模块化系统告别类路径地狱、支持 ZGC 实现毫秒级 GC 停顿,对比 JDK 8 更适配云原生场景。
- 从 JDK 8 迁移到 JDK 17 需注意框架版本兼容,优先在非核心模块试点,可平稳完成升级。
希望这篇文章,能成为你技术升级路上的一份可靠参考。