资讯动态

JDK、JRE、JVM详解:Java版本演进与跨平台原理一次讲透

发布时间:2026/10/20 17:32:42 来源:尧图企业网站定制
如果你刚开始学Java或者已经在用Java做项目但一直没把基础概念彻底理清楚那我猜“JDK、JRE、JVM”这三个词大概率是你最早遇到也最容易混淆的一组成语式名词。我第一次接触Java的时候也很头大教材上写着“JDK包含JREJRE包含JVM”我背是背下来了但完全想不明白为什么要包来包去更搞不懂Java各种版本号之间到底差在哪。后来真正进入项目开发亲手装环境、调GC、处理过好几次版本不兼容的线上问题才慢慢把这些概念串成一条线。这篇文章我想从Java的特性、版本演进、以及JDK/JRE/JVM三者的实际分工说起把每个概念背后“为什么存在”的逻辑讲透。不管你是刚入门的新手还是已经能写业务代码但一直没系统整理过这些基础的同学这篇内容都可以当一份比较完整的学习笔记来看。1. 先搞清楚三个“J”到底是什么关系1.1 一句话给结论JDK包含JREJRE包含JVM先把结论挂在前面JDK是一个完整的Java开发工具包JRE是Java程序的运行环境JVM是Java虚拟机。它们之间的包含关系很简单JDK里带着JREJRE里装着JVM。很多教材和教程上来就甩这个结论但很少解释为什么必须这样嵌套。用生活里的东西类比一下就很好懂了。把JVM比作一台洗衣机内部的核心电机和控制板它决定了“程序到底怎么运转”但光有电机你没法直接洗衣服你得给它接好水管、配好电源、放在一个能排水的地方这套“装好并可以运行的洗衣房”就是JRE。而JDK相当于在这个洗衣房旁边又多了一个工具间里面有各种维修工具、说明书、检测仪表——也就是编译器、调试工具、打包工具这些开发用的东西。所以如果只是想运行别人写好的Java程序你只需要JRE如果你想自己写Java代码并把它编译成可运行的程序那你必须装JDK。1.2 “跨平台”背后到底发生了什么Java最大的招牌就是“一次编写到处运行”。这几年各种新语言层出不穷但这句口号依然是Java在服务端领域站稳脚跟的重要原因之一。那它到底是怎么实现的很简单你用Java写出来的.java源文件并不会像C或者C那样被直接编译成针对某个操作系统和CPU的机器码。javac会先把源码编译成一个中间产物——.class字节码文件。字节码并不是某个真实CPU能直接执行的指令而是专门给JVM看的一套指令。你把这个.class文件放到任意一台安装了JVM的操作系统上JVM会逐条解释或者通过即时编译把字节码转换成当前平台真正认识的机器码来执行。整个过程可以翻译成你用世界语写了一封邮件每个地方都有一位翻译官JVM把它翻译成当地的语言所以你只需要写一次信就能寄往各个国家。翻译官不同但邮件内容是一样的。这就是Java跨平台的本质不是Java本身懂所有系统而是每个平台上都有一个懂Java字节码的虚拟机。1.3 Java语言的核心特性到底有哪些很多新人背Java特性时喜欢背“面向对象、多线程、分布式、健壮性、安全性”这一串名词但背下来之后并不知道它们在真实开发中到底意味着什么。我这里换一种讲法不说教条直接说这些特性让你写代码的时候实际感受到的差别。第一是强类型和编译期检查。Java的变量类型一旦声明就不能随便乱变很多低级错误在编译阶段就会被拦住而不是运行到线上才炸。这一点对于大型多人协作项目非常重要编译器相当于一个最严格的代码评审员。第二是面向对象带来的代码组织方式。封装让你不用关心内部实现继承和接口让模块之间可以按约定协作。Java把类和对象作为最基本的组织单元这种设计让几百万行的代码仓库依然能被拆成边界清晰的模块。你写业务时会发现和数据库打交道的类、处理逻辑的类、暴露接口的类天然就能按职责切分开。第三是自动内存管理和垃圾回收。Java里你不需要像写C语言那样亲手malloc一块内存再用完释放你只管new对象JVM会在合适的时候把不再使用的对象清理掉。这大幅降低了内存泄漏的概率也让开发效率提升很多。但自动不等于完全不用管如果对象创建速度大于回收速度堆内存照样会爆后面我会专门讲内存参数和排查。第四是原生的多线程支持。Java语言层面就提供了线程和锁的API加上后来的并发工具包写多线程程序不需要依赖某个框架。服务端一次处理大量请求背后基本都离不开线程池、并发容器、原子变量这些东西。第五是极丰富的标准库和生态。从集合、IO、网络到数据库连接、XML解析、加密、日志JDK自带的类库覆盖了大部分常见需求。再加上社区里积累的各类开源框架Java开发者在实现一个功能时通常不需要从零造轮子。第六是异常处理机制。Java强制要求受检异常必须处理或者向上抛这个设计虽然经常被吐槽繁琐但它养成了一个好习惯错误路径和正常业务路径被分开处理程序遇到意外情况时不会直接崩溃得无声无息。第七是反射和动态能力。Java是静态语言但通过反射可以在运行时获取类的结构、调用方法、操作属性。很多框架底层就是靠反射完成依赖注入和动态代理的。你平时写业务可能很少直接用反射但你用的每一个主流框架都在频繁用它。这些特性单独看好像都很基础但组合起来就让Java成了那种“不一定最优雅、但综合能力极强”的语言。它不会让天才飞起来但它绝对不让千行代码的项目烂到底。2. Java版本演进从1.0到21这些年都在改什么2.1 早期版本从1.0到1.4把地基打牢Java的版本历史是一个特别容易让人糊涂的领域因为它的早期版本号带了“1.”前缀。Java 1.0其实就是Java 1Java 1.2后来被冠名为Java 2但版本号还是1.2结果就是“JDK 1.2”和“Java 2”这种叫法并存了很长一段时间。现在大家说Java 8本质上是指JDK 1.8只是后来官方去掉了“1.”从Java 9开始才恢复了整数的命名方式。第一个阶段从Java 1.0到1.4是给整个语言打地基的时期。1.0正式推出标志着Java从实验室走向公开市场。1.2引入了真正的集合框架那时候List、Set、Map三件套开始成型还加入了JIT编译器让Java程序的执行速度有了明显提升。1.4版本加入了正则表达式、NIO和日志API这些在今天看来都是基本功但在当时是关键拼图。那个阶段Java最打动人的地方在于“写一次随便跑”所以它在Web开发的浪潮里迅速成了企业应用的主流语言。你可能现在觉得“跨平台”已经不值钱了但在二三十年前能在一台开发机上写好程序然后部署到另一台服务器上直接用是一件节省大量时间的事。2.2 两次划时代升级Java 5和Java 8Java 5是第一次真正意义上改变开发者写代码方式的版本。它带来的泛型、自动装箱拆箱、枚举、注解、可变参数、新增的for-each循环还有配套的并发工具包等于把整个工业界Java开发者的日常写法重写了一遍。举几个具体的例子泛型让集合在编译期就能检查类型不用再到处强转注解后来成了Spring全家桶的基石并发工具包解决了过去需要自己手动管理线程池的痛点。Java 5的发布让Java从“能用”变成了“好用”。但要说影响最深、至今依然统治着无数生产环境的版本还得是Java 8。Java 8引入了Lambda表达式、Stream流式API、接口默认方法、Optional建议容器以及一套全新的日期时间API。这里面最颠覆性的变化就是Lambda和Stream。在Java 8之前你处理一个列表通常要写外部循环定义一个集合for循环遍历每个元素做判断然后在循环体里收集结果。Java 8之后你可以直接写出类似“从学生列表里筛选出成绩大于90分的同学再按名字排序取出前十个”这种链式代码。Stream的惰性求值和内部迭代让过滤、映射、聚合这些操作变得像流水线一样清晰。我那时候第一次看到这种代码是真有“被打开新世界大门”的感觉代码量少了一大截而且逻辑一眼就能看懂。日期时间API也是Java 8的一个痛点修复。旧版的java.util.Date不仅命名反人类还存在可变性和线程安全问题。Java 8推出的LocalDate、LocalDateTime、Instant这组新API用起来清爽多了之后再也没人愿意回头用旧的日期类。另外Java 8还做了一项后来影响深远的决定从它开始官方提供了LTS长期支持版本。也就是说Java 8在发布后的很多年里官方都在持续维护更新安全补丁和稳定性修复一直没停。这也是为什么直到今天很多老项目仍然安心地停在Java 8上不升级。2.3 Java 9以后模块化与半年一版的快节奏如果说Java 8是对开发体验的全面升级那Java 9以后的版本就是在底层架构上动手术。Java 9最重头的特性是模块化系统。JDK本身被拆分成一系列相互依赖的模块应用也可以自己定义模块显式声明依赖了哪些模块、对外暴露哪些包。这套东西的初衷是解决大型应用“类路径地狱”的问题让依赖关系在编译期就变得清晰可控。代价是学习曲线陡增生态兼容性也经历了一段阵痛期。很多老库用了非公开的JDK内部API在模块化之后直接就跑不起来了。Java 9还带来了一个对学习者和日常调试非常友好的工具JShell。它是一个交互式REPL你可以在里面直接敲Java语句马上看到结果不用再经历“写类、写main方法、编译、运行”一整套流程。我有时候要验证一个小API用法懒得开IDE直接在JShell里试一下效率高很多。Java 10引入了局部变量类型推断用var声明局部变量编译器根据初始值推断类型。这个特性被很多人称赞也被很多人吐槽但整体来说它让部分代码更简洁了。Java 11是一个新的LTS版本同时做了一些瘦身工作删掉了Java 9之前遗留的Java EE模块还引入了ZGC垃圾收集器的初步版本。到了Java 12到16这段时间更像是一个高频迭代期。Switch表达式从预览走向正式写“根据枚举类型执行不同逻辑”这类代码时可以不用再写一堆breakinstanceof模式匹配让“先判断类型再强转”的写法简化成了“判断类型的同时绑定新变量”record关键字用来定义只携带数据没有行为的类写DTO和值对象的时候省了大量样板代码。Java 14还正式引入了NVM友好的垃圾回收器ZGC。Java 15、16继续打磨这些预览特性。Java 17是又一个重磅LTS版本。它把很多之前在预览阶段被反复验证过的特性转正包括密封类也就是明确一个类可以被哪些类继承让继承关系变得可穷举。从Java 17开始大家终于有了一个既能享受Java 8以来所有新特性、又能安心用于生产环境的长期支持版本。最新的Java 21是又一个长期支持版本最受关注的当属虚拟线程正式落地。虚拟线程是一种轻量级线程由JVM调度而不是操作系统线程一一切换可以以极低的内存成本创建成千上万个并发任务。这直接解决了过去Java高并发应用“线程池大小难调、阻塞IO浪费线程”的经典难题写高并发代码的模式也随之发生变化。2.4 生产环境到底怎么选版本现在生产环境里最常见的Java版本就是8、11、17这三档以及越来越多的21。我给你的建议很简单老项目能不动就不动新项目优先选最新的LTS。原因也很实际。Java 8到现在依然有大量跑了几年的老系统这些系统的框架、中间件、第三方库都是基于Java 8时代的技术栈验证过的贸然升级可能带来的兼容性风险远大于收益。Java 11作为中间过渡版确实也不少企业用但它离Java 8太近也没有特别明显的升级动力很多团队会选择直接跳到17。Java 17和21是目前比较理想的起点不仅拥有所有现代语法糖而且性能、GC和并发能力都比Java 8时代强很多。有一件事要特别注意很多新特性在刚发布时是预览状态用的时候要加编译参数功能本身可能还不太稳定。选择版本的时候不要只看功能多少更要看这个版本是否处于长期支持状态以及社区的迁移经验是否已经积累得足够多。下面这个表可以帮你快速对比一下几个LTS版本的核心差异版本关键特性适合场景迁移注意点Java 8Lambda、Stream、新日期API存量老化系统、稳定优先依赖生态成熟不需要动Java 11var、ZGC预览、模块化成熟需要较新API但不想追太快的团队老库可能用了JDK内部API需要检查Java 17Switch表达式、record、密封类、强封装JDK内部新项目首选、老项目可以规划升级反射和内部API访问是最大兼容性障碍Java 21虚拟线程、record模式匹配等高并发服务、新系统建设虚拟线程使用习惯需要学习但收益明显3. 深入拆解JDK、JRE、JVM的组成3.1 JDK装完之后目录里都是些什么每当你下载并解压一个JDK安装包之后会发现里面有很多文件夹很多人从不关心它们各自是干嘛的。这里挑几个关键的讲。bin目录存放的是可执行工具。java用来启动Java程序javac用来编译Java源文件jar用来打包javadoc用来生成文档javap可以用来反编译查看字节码。初学者最常用到的是前两个到了排查线上问题的时候jstack、jmap、jstat、jcmd这些监控诊断工具就派上大用场了。lib目录存放核心类库和编译运行所需的库文件。include目录放的是与本地方法相关的头文件如果你不写JNI调用C/C代码一般用不到。conf目录存放一些全局配置文件比如安全管理策略和net属性配置。旧版本的JDK里还有单独的jre目录现在新版本已经没有了原因后面说。3.2 为什么JDK 9之后不再单独提供JRE安装包以前你要运行一个Java程序可以去官网单独下载一个JRE安装包体积比JDK小很多装完就能跑。但Java 9模块化之后官方调整了分发方式不再单独发布独立的JRE安装包而是让你基于模块化JDK用jlink工具按需生成一个精简的运行时镜像。这样做的理由很直接过去独立JRE里带了整个JDK公共类库哪怕你的程序只用到了其中一小部分系统也得装一个完整的运行时浪费磁盘和内存。模块化之后运行环境可以按需裁剪只包含你的应用真正依赖的模块体积自然就小很多。所以现在的开发机、服务器大多直接装JDK运行程序靠JDK里自带的运行时即可不需要再单独装一个JRE。这里顺便提醒一下网上有些旧教程让你“下载JRE安装”这个过程在今天已经不合时宜了。新项目不要再费力去找独立JRE包直接下载对应版本的JDK就行。3.3 JVM内部到底在做什么JVM是一个相当复杂的软件但可以按功能拆成三大块来看类加载子系统、运行时数据区还有执行引擎。类加载子系统负责把.class字节码文件加载到JVM内存里并进行验证、准备、解析和初始化。验证阶段会检查字节码格式是否合法防止非法指令捣乱准备阶段会给静态变量分配内存并设置默认值解析阶段把常量池里的符号引用替换为直接引用初始化阶段才是真正执行静态变量的赋值和静态代码块。这套流程从你启动一个Java类的时候就开始运行所以Java程序有类加载机制也让后面出现ClassNotFoundException时你有迹可循。运行时数据区是JVM在运行过程中使用到的内存区域主要分为几块。堆是所有线程共享的用来存放对象实例也是垃圾回收的主战场虚拟机栈是线程私有的每调用一个方法就会往栈里压入一个栈帧方法结束后栈帧出栈元空间存放类的元数据和静态变量程序计数器记录当前线程正在执行的字节码指令地址。如果你遇到过堆溢出、栈溢出其实就是这些区域撑爆了。执行引擎是真正“干活”的部分。它先以解释方式执行字节码指令同时监控哪些方法是热点方法然后使用即时编译器把这些方法编译成平台相关的机器码让后续执行速度大幅提升。JVM的启动速度和长期运行后的执行效率差异很大程度上就是解释执行与即时编译结合的功劳。3.4 分代回收与常见GC参数JVM里的垃圾回收并不是每次都扫描全堆的而是按照对象存活时间做了分代设计。大多数对象刚创建时分配在年轻代年轻代里的对象经历几次垃圾回收后仍然存活就被晋升到老年代。年青代GCMinor GC频率高、速度快老年代GCMajor GC / Full GC频率低但停顿时间更长。不同垃圾收集器的取舍点不一样。老一些的并行收集器关注吞吐量适合后台批量计算G1是JDK 9之后的默认收集器把堆划分为多个Region能够在控制停顿时间的同时保持不错的吞吐量适合大多数服务端应用ZGC则面向超低停顿目标适合大内存、对延迟敏感的分布式系统。实际开发中你会经常和这几个JVM参数打交道-Xms设置堆初始大小-Xmx设置堆最大大小-XX:UseG1GC指定使用G1收集器-XX:PrintGCDetails打印GC日志。一个简单的经验是-Xms和-Xmx建议设成一样大避免运行期堆大小反复伸缩带来的额外开销。但具体是多少要结合你的服务内存预算和业务峰值流量来定不能照抄别人的配置。4. 一步步搭好开发环境JDK安装与配置4.1 下载哪个发行版很多新手在“下载JDK”这个环节就卡住了因为一搜JDK会出现一大堆发行版。我建议你根据实际场景从下面三类中挑一个。第一类是官方提供的商业JDK版本它提供完整的商用授权和技术支持适合企业内部正规项目使用。第二类是各种开源社区编译构建的版本以OpenJDK为基础的免费开源发行版许可宽松更新速度也比较快个人学习和中小项目完全够用。第三类是各云厂商或系统厂商提供的构建版通常针对自家云平台或操作系统做过额外优化适合部署在特定环境里的场景。选择标准只有三条看版本号是不是LTS看支持周期能不能覆盖你的项目计划看是否允许在你的使用场景中免费商用。个人开发和学习选一个开源LTS版本就好公司项目按公司的合规要求来确定。4.2 Windows环境安装与JAVA_HOME配置Windows安装JDK有两条路线一条是用安装程序点点点一条是用zip包手动解压配置。zip包路线更可控一些也方便以后一键切换版本我讲这一种。先把下载到的JDK压缩包解压到一个不含中文和空格的路径比如D:\java\jdk-17。然后在系统环境变量里新建一个JAVA_HOME值就填D:\java\jdk-17。接着在系统变量Path里新增一行%JAVA_HOME%\bin。注意Windows配置系统环境变量时Path中当前用户的变量和系统变量会叠加如果本机以前装过别的JDK你可能会在列表里看到旧的JDK路径把它删掉或者移到最后都行。这里提示一个细节不要偷懒直接把JDK的完全路径写进Path而不设置JAVA_HOME。虽然这样java命令也能生效但很多构建工具、IDE、脚本都要读取JAVA_HOME你写死了路径以后换版本会非常痛苦。设置JAVA_HOME之后再引用它是一种把配置集中管理的习惯。配置完成后打开一个新的命令行窗口依次执行下面两个命令验证java -version javac -version如果两个命令都能正确打印版本号说明环境变量配置成功了。如果提示“不是内部或外部命令”先检查Path里是否真的包含了%JAVA_HOME%\bin再检查变量名是否写错。4.3 macOS和Linux的配置macOS和Linux上装JDK通常也分两种方式一种是直接下载压缩包后配置环境变量一种是用系统包管理器安装。用包管理器安装的好处是会自动帮你配好路径但有时你并不知道装到哪去了。手动配置的方式是在用户级shell配置文件中添加几行导出语句。以Linux和macOS默认的shell为例编辑~/.bashrc或者~/.zshrc文件加入export JAVA_HOME/opt/jdk-17 export PATH$JAVA_HOME/bin:$PATH改完文件后执行source ~/.bashrc让配置立即生效。macOS系统上可以借助系统自带的/usr/libexec/java_home命令来获取当前默认JDK的路径你可以把它作为JAVA_HOME的值这样以后用java_home命令切换默认版本时环境变量也能自动跟随。新手只要记住一条规则哪里配置了PATH哪里就要同步配置JAVA_HOME。4.4 验证安装结果与多版本切换版本验证不只是打两个命令看个响就结束要会看输出内容。java -version会显示三行信息第一行是Java运行时版本号第二行是JVM版本号第三行是运行模式比如Java HotSpot(TM) 64-Bit Server VM。如果你把它看习惯之后你就能判断当前运行的Java是哪个大版本、哪个发行商、运行在什么模式下。多版本共存的场景很常见比如你手头同时维护着Java 8的老项目和Java 17的新项目。Windows上最笨也最可靠的办法是用两个不同的JAVA_HOME需要哪个就改环境变量再重启命令行窗口。Linux/macOS上可以借助一些命令行版本管理工具来在不同JDK之间切换。无论用哪种方式切换完一定要重新执行java -version确认当前生效的是你要的那一个。因为有些IDE、脚本会缓存环境变量切换之后一定要让所有终端重新加载配置否则你编译的时候还是旧版本。5. 实战中绕不开的坑与排查方法5.1 “java不是内部或外部命令”怎么定位新人遇到的环境问题一多半都发生在环境变量配置环节。看到“不是内部或外部命令”这句话先别慌按顺序检查三步。第一步确认JAVA_HOME指向的目录确实存在并且路径没写错尤其注意分号、斜杠方向、目录名大小写。第二步确认Path环境变量或PATH环境变量中真的包含%JAVA_HOME%\bin修改环境变量后必须重新打开命令行窗口有些旧窗口不会自动刷新。第三步直接在命令行输入echo %JAVA_HOME%或者echo $JAVA_HOME看变量是否已经被正确读取。很多时候你排查完会发现不是JDK没装好而是修改完环境变量之后忘了开新窗口。如果是在IDE里启动了Java命令但提示找不到还要看一下IDE自己安装的JRE路径配置。IDE默认可能带了一个它自带的JDK或者读取的是你全局环境变量之外的另一个路径。5.2 ClassNotFoundException和NoClassDefFoundError的区别这两个错误长得很像但原因完全不同。ClassNotFoundException发生在类加载阶段是JVM找遍了整个类路径都没有找到指定的类。通常是依赖的JAR包没放进classpath或者JAR包本身的META-INF清单里写的依赖信息有问题。排查方法很直接看启动命令里的-classpath参数是否包含了该依赖的完整路径或者检查是不是漏加了某个第三方库。NoClassDefFoundError则是一个更棘手的问题类之前已经被加载过但运行过程中无法再访问到它的定义通常是因为依赖缺失、静态初始化失败、或者类被放进了多个位置导致版本冲突。它往往不是因为你少写了什么而是因为运行时真正加载依赖的顺序和编译时不一样。遇到这个错误建议优先检查是否有多版本JAR包冲突。比如某个传递依赖把同一个类的不同版本都塞进了classpath导致加载器加载了错误的一个。5.3 编译版本与运行版本不一致当一个类的版本比你运行的JVM高时会报UnsupportedClassVersionError并告诉你一个数字比如“class file version 61.0”。这个数字对应着Java版本从Java 5开始有映射关系45对应Java 1.149对应Java 552对应Java 855对应Java 1161对应Java 1765对应Java 21。看到这类报错先检查编译时生成的class文件用的是哪个JDK再检查运行时用的是哪个JDK。比较推荐的做法是编译的时候用--release参数而不是单独指定-source和-target。-source和-target只是限制了源码语法和class文件版本但编译时仍然可能链接到当前JDK里的新API导致运行时在新版本上才报错。--release参数会同时约束源码、字节码和系统API这三个维度保证编译出来的产物可以在指定版本上运行。举个例子你用JDK 17开发要兼容Java 11运行时就加--release 11这样编译器的行为会被限制在Java 11的规范内。5.4 控制台中文乱码问题Java程序在控制台打印中文出现乱码绝大多数原因是源文件编码、运行时默认字符编码、和控制台显示编码三个环境不一致。你在Windows上最容易遇到这个问题因为中文Windows的控制台默认使用GBK编码而你的源文件如果以UTF-8保存运行时就可能出现乱码。解决办法分两步。第一步确保源文件以UTF-8保存并且编译时指定编码javac -encoding UTF-8。如果是构建工具做的编译要看工具的编码配置。第二步运行程序时显式指定-Dfile.encodingUTF-8让JVM的输出使用UTF-8编码。如果依然乱码就到系统终端设置里把控制台代码页改成UTF-8。这里也提醒一下Java 18开始默认字符集已经改为UTF-8如果你搞不定旧版本乱码问题最简单的办法就是把运行环境切到Java 18以上。5.5 常见内存问题堆溢出、栈溢出、元空间异常内存问题是Java线上事故里的常客。最常见的报错是OutOfMemoryError: Java heap space说明堆空间满了要么是存活对象太多要么是存在内存泄漏。排查第一步用jstat -gc pid看各个代的使用情况第二步用jmap -dump:formatb,fileheap.bin pid导出堆快照然后放进分析工具里看哪个对象的实例数量异常庞大。线上环境如果不方便用这些命令可以在启动参数里加上-XX:HeapDumpOnOutOfMemoryError让程序自动在发生OOM时把堆快照落盘。StackOverflowError通常是递归过深或者方法调用链太长造成的。默认线程栈大小按平台不同一般是512KB到1MB遇到确实需要更深栈的场景可以用-Xss调大但调大栈的代价是每个线程占用的内存会变多线程数多的时候不划算所以根治方案通常是检查业务代码有没有写出不合理的递归。还有一种OutOfMemoryError: Metaspace是元空间撑爆了常见于动态生成大量类的场景比如热部署、反射、字节码增强用得太狠。这时候先看是不是有类加载器泄漏也就是旧的类加载器始终被引用无法释放然后适当调大-XX:MaxMetaspaceSize作为缓冲。6. 从老版本迁移到新版JDK的实践经验6.1 迁移前先做一次版本审计老项目升级JDK最忌讳的就是“换一个JDK然后编译跑一下”这种简单粗暴的做法。版本升级牵扯到的不只是JDK自身还包括你项目里的所有依赖库、框架、构建插件、容器镜像、IDE配置。我建议你先花几天时间做一次依赖审计把所有第三方库列出来对照各库官方文档确认它在目标JDK版本上是否兼容尤其要考虑那些年久失修、已经很久不更新的老库。审计项目里有没有用到JDK非公开API也很关键。Java 9引入模块化之后JDK内部包被加强了封装很多以前能直接访问的sun.*、com.sun.*类现在会直接报错。项目里如果大量使用反射去访问JDK内部实现迁移前一定要把相关代码改成公开API方案。可以用jdeps命令行工具对构建产物做静态分析看看它到底依赖了哪些JDK内部模块。6.2 按依赖、编译、测试、上线四步走迁移过程我建议分成四步顺序不能乱。第一步先处理依赖兼容性把所有第三方库升级到支持目标JDK的版本有些老库可能需要大版本升级这里要做充分回归测试。第二步才是改项目配置把编译插件的Java版本参数改到新版本用--release替换掉旧的-source和-target。第三步跑自动化测试先跑单元测试保证核心业务逻辑没被破坏再跑集成测试重点覆盖文件编码、反射、动态代理、序列化这些和JVM底层交互比较密切的部分。第四步才是灰度上线先让少量机器切到新版本观察日志、监控、GC表现和线程使用情况稳定一两天再全量。这个过程中最容易翻车的是各种框架的字节码增强逻辑。比如某些老版本的ORM框架、缓存框架使用CGLib生成代理类在模块化之后可能因为反射访问受限而直接启动失败。遇到这种情况别硬扛老老实实按框架方给出的升级路线走。6.3 把版本信息固定成项目的“体检指标”最后分享一个我个人的习惯我会在每个Java项目的启动脚本里固定打印一行版本与环境信息包括java -version的输出、JAVA_HOME路径、关键JVM参数。这样任何一次环境变更都能在日志里留下痕迹排查线上问题时可以一眼看出“这台机器跑的是什么版本、什么参数”不用到处猜。这个习惯救过我很多次。有一回一个服务突然出现明显的性能退化排查半天找不到原因最后在启动日志里看到JAVA_HOME指向的JDK路径被误改了。如果没有这个日志可能要花更久才能定位。我强烈建议你也把这个“启动信息固定化”的动作养成习惯尤其是团队里有多个开发、多个部署环境的时候它能省掉大量低效的沟通成本。另外还想多说一句不要被新版本眼花缭乱的特性和新语法带偏节奏。升级JDK的目的应该是让项目更稳定、更高效、更安全而不是单纯追新。如果项目在Java 8上跑得好好的业务也稳定那升级的优先级就往后放如果新项目或者你准备解决高并发和内存瓶颈那选一个较新的LTS版本从头开始就是最佳时机了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑