JDK、JRE和JVM之间是层层嵌套的包含关系:JDK是Java开发工具包,里面装着JRE;JRE是Java运行环境,里面装着JVM;JVM是虚拟机,负责把字节码翻译成机器能懂的指令,简单概括:你用JDK写代码,用JRE跑代码,JVM在JRE肚子里干活。
很多人在Oracle官网下载Java时,看着JDK和JRE两个选项一脸茫然,明明自己只想运行一个.jar文件,为什么还要下载一个几百兆的JDK?安装完成后,又发现目录里藏着一个叫jre的文件夹,这套三层嵌套的结构,如果不拆开看,确实容易让人绕晕。
JDK和JRE有什么区别?先理清包含关系
JDK(Java Development Kit)和JRE(Java Runtime Environment)的头号区别,不是谁比谁厉害,而是谁装着谁。 JDK是给开发人员用的完整工具包,JRE是给普通用户用的运行环境,JDK的体积通常是JRE的2到3倍,多出来的空间几乎全给了开发工具。
拿OpenJDK 17的目录结构举例,JDK里除了bin、conf、lib这些常规目录,还专门带了一个jmods目录存放模块化库文件,JRE则精简得多,核心组件就是bin和lib两个目录。
从下载到安装,JRE藏在JDK里
你从Oracle官网下载的JDK安装包,装完之后会得到一个完整的jdk目录,进入该目录,你会发现里面嵌套着一个jre子目录,这就是标准的包含关系:JDK自带一份完整的JRE,这份JRE主要用于运行开发过程中产生的临时程序。
但JRE可以独立于JDK单独安装,早期Java 8时代,Oracle官网提供独立的JRE下载链接,Java 9开始模块化改革后,官方不再提供独立的JRE安装包,转而推荐用jlink命令定制最小运行环境,如果你写了一个Java桌面程序要分发给客户,用jlink裁剪出的精简运行时体积可能只有40MB左右,比完整JRE小一半以上。
三者的职责边界
| 组件 | 适用人群 | 是否包含JVM | |
|---|---|---|---|
| JDK | JRE + javac编译器 + jdb调试器 + javap反编译工具 + jmods模块库 | 开发人员 | 包含 |
| JRE | JVM + 核心类库(rt.jar、java.lang等) + 运行必要文件 | 普通用户 | 包含 |
| JVM | 类加载器 + 运行时数据区 + 执行引擎 | 无直接面向用户 | 本身是JRE的一部分 |
在这条链条里,JVM是最内层的执行者,JRE是给JVM提供生存土壤的运行环境,JDK则是上游的生产工具,三者各司其职,缺一不可。
JVM跨平台原理:一次编译到处运行是怎么做到的
Java的口号叫“一次编写,到处运行”,这句口号能够成立,靠的就是JVM这层中间翻译官。你在Windows上写的Java代码编译成.class字节码,拿到Linux上照样能跑,前提是目标机器装了对应平台的JRE。
为什么字节码能做到平台无关?因为字节码不针对任何具体CPU指令集,它是一套抽象的中间表示,JVM负责把字节码逐条翻译成当前操作系统和硬件能识别的机器码,Windows版JVM把字节码变成x86指令,Mac版JVM把它变成ARM指令,代码本身一个字都不用改。
类加载机制:第一次运行时的三道关卡
Java程序启动的一瞬间,JVM的类加载器开始工作,具体流程分三步:
- 加载:从classpath或jar包中找到.class文件,读入内存,生成Class对象
- 链接:验证字节码合法性,分配静态变量内存,解析符号引用
- 初始化:执行static代码块,给静态变量赋初值
这段流程是从JVM规范里提炼的行业共识,类加载器的双亲委派模型值得一提:你写的java.lang.String类永远不会覆盖JDK自带的java.lang.String,因为引导类加载器优先加载核心类库。 这种设计保证了Java核心类的安全性,防止开发者写出冒名顶替的系统类。
运行时数据区:JVM内部的内存分工
JVM把运行时内存划分为几个区域,各管各的事:
- 程序计数器:记录当前线程执行到哪一行字节码
- 虚拟机栈:每个方法调用时压入一个栈帧,局部变量、操作数栈都存在这里
- 堆:所有new出来的对象都分配在堆上,堆是垃圾回收的主战场
- 方法区:存放类元信息、常量池、静态变量
- 本地方法栈:执行native方法时使用
绝大多数Java性能调优的焦点都集中在堆上,因为对象都在这里创建和销毁,你听过的OOM(内存溢出)错误,大部分也发生在堆区域。
JVM内存参数设置:排查内存溢出还是性能调优
JVM的设计者们很清楚,没有一种参数组合适合所有业务场景,于是他们开放了一大堆可调参数,让运维人员在启动脚本里按需配置。JVM内存参数设置是Java应用上线前必须做的一次体检,默认参数更像是一个兜底方案,远谈不上最优解。
常用参数速查表
| 参数 | 作用 | 典型场景 |
|---|---|---|
| -Xms | 初始堆大小 | 设为物理内存的1/4 |
| -Xmx | 最大堆大小 | 设为物理内存的1/4,与-Xms一致时避免动态扩容 |
| -Xmn | 新生代大小 | 占堆总量的1/3到1/2 |
| -XX:MaxMetaspaceSize | 元空间上限 | 防止类加载过多导致OOM |
| -XX:+PrintGCDetails | 打印GC日志 | 排查内存泄漏时开启 |
配置示例,在启动命令中这样写:
java -Xms2g -Xmx2g -Xmn1g -XX:+PrintGCDetails -jar myapp.jar
业界比较常见的设置思路是让-Xms和-Xmx相等,JVM启动时直接分配固定大小的堆,好处是没有扩容和收缩的开销,GC行为更可预测,坏处是如果实际用量很低,浪费了物理内存。
遇到的OutOfMemoryError,从哪下手
堆内存溢出时,第一件事不是调大-Xmx,而是分析堆快照,具体操作路径如下:
- 在启动参数中添加-XX:+HeapDumpOnOutOfMemoryError,JVM在抛OOM时自动生成.hprof文件
- 用jmap命令手动导出快照:jmap -dump:format=b,file=dump.hprof <进程ID>
- 用VisualVM或MAT打开快照,查看对象占用排行
多数情况下,OOM不是内存容量不足,而是内存泄漏,某个集合对象无限增长,强引用没释放,占满了堆空间,盲目调大-Xmx只是把出问题的时间往后拖,根源还在那里,排查泄漏重点看GC后老年代是否持续增长,如果每次Full GC之后老年代占用率不降反升,基本可以断定有对象泄漏了。
JDK哪个版本好用?LTS版本怎么选
选择JDK版本,本质上是选择冒险还是求稳。每隔三年发布的LTS(长期支持)版本是主流选择,非LTS版本适合尝鲜,不建议生产环境使用。 Oracle官方明确承诺对LTS版本提供至少8年的更新,非LTS版本每半年发一个,支持期只有6个月。
出镜率最高的几个LTS版本
- JDK 8(2014年发布):Lambda表达式、Stream API横空出世,至今仍有相当一部分老旧项目跑在它上面
- JDK 11(2018年发布):LTS版本的转折点,Oracle正式推出用Java开发的企业级应用
- JDK 17(2021年发布):Spring Framework 6和Spring Boot 3强制要求的最低版本
- JDK 21(2026年发布):引入虚拟线程,高并发场景的重磅利器
选择版本时建议配合框架要求判断。Spring Boot 3.x系列只支持JDK 17及以上,如果你的项目要从Spring Boot 2升级到3,JDK版本就得同步跳级。 新项目直接上JDK 17或21是当前行业共识,JDK 8适合维护存量系统。
环境变量配置是区分三者关系的试金石
配置JAVA_HOME时你最直接地感受到三者关系:
- JAVA_HOME指向JDK安装目录,例如D:Program FilesJavajdk-21
- PATH中添加%JAVA_HOME%bin,让javac和java命令全局可用
- CLASSPATH部分版本已不需要手动设置,Java 9以后JVM会自动加载lib目录
验证安装是否成功,命令窗口输入java -version,如果输出类似openjdk version “21.0.2”的字样,说明JRE和JVM都在正常运转,再输入javac -version,如果也正常返回,说明JDK的编译器也没有问题。
JVM垃圾回收机制:程序运行背后的清理工
JVM管理层堆内存时,有一套精密的回收机制,统称为垃圾回收GC。对象被判定为“垃圾”的标准只有一个:从根对象出发,沿着引用链走一遍,走不到的对象就是垃圾。 这个判定方法叫作可达性分析,比传统引用计数法更可靠,不会出现循环引用无法回收的死锁问题。
堆内存分区与晋升路径
堆内部按对象存活时长分成几个物理区域:
- Eden区:新建对象统一分配到这里,空间不足时触发Minor GC
- Survivor区:Minor GC后存活下来的对象从Eden移入S0,再下一次GC移到S1,反复交换
- 老年代:对象在Survivor区来回交换了15次(默认阈值)之后,晋升到老年代
老年代空间也不够时,JVM发动Full GC,对整个堆进行一整轮清扫,Full GC通常会触发STW(Stop The World),用户线程全线停顿,所以它发生的频率是衡量应用健康程度的关键指标。
在JDK 8时代,G1收集器在服务端应用里占据了主导位置,JDK 11开始G1成为默认垃圾回收器,JDK 17之后,ZGC和Shenandoah这类低延迟收集器逐渐成熟,把STW时间控制在了毫秒级甚至微秒级,对于某个Java服务来说,GC停顿时间从几百毫秒缩减到几毫秒,对用户体验的改善是直观的。
JVM类加载器的双亲委派,Java安全模型的基石
开发者在排查类冲突问题时,总会碰到类加载器这个概念,双亲委派模型不是JVM实现细节,而是Java平台安全机制的重要一环,保证了核心类不会被恶意替换。你自定义一个java.lang包下的类,JVM直接拒绝加载,原因就在于引导类加载器优先加载了JDK自带的版本。
类加载器从上到下分为四层:
- 引导类加载器:C++实现,负责加载
/lib目录下的核心类 - 扩展类加载器:Java实现,加载jre/lib/ext目录下的类
- 应用类加载器:加载classpath环境变量指定的类
- 自定义类加载器:继承ClassLoader类,实现热部署、加密解密等特殊需求
Tomcat这类Web容器会用多个自定义类加载器各自管理不同web应用的类,应用之间互不干扰,这也是为什么两个部署在同一个Tomcat里的Java应用就算引用了同一个Jar包的不同版本,也能各自正常启动。
常见问题
JDK、JRE和JVM的区别是什么?
JDK包含JRE,JRE包含JVM,JDK面向开发人员,提供编译器、调试器;JRE面向运行用户,提供运行时环境和类库;JVM是执行引擎,负责将字节码翻译成机器指令,三者是包含关系,不是并列关系。
安装JDK后还需要单独安装JRE吗?
不需要,JDK安装包内自带完整的JRE,名为jre子目录,你编译和运行Java程序都可以直接使用这个内嵌运行环境,如果客户机器只运行你的程序,可以单独装JRE,或者用jlink定制一个更精简的运行时。
JVM内存设置多大合适?
取决于物理内存和应用负载。核心原则:堆最大值不要超过物理内存的一半,保留给操作系统和JVM自身使用的空间。 先按默认参数跑起来,观察GC日志,如果GC频繁且回收效果不佳,逐步调大堆,同时留意是否出现Full GC,没有万能的参数值,一切以实际压测结果为准。
JDK、JRE和JVM不是三选一的关系,它们是同一条生产链上的上下游,理解这套嵌套结构,无论是排查内存溢出、解决版本冲突,还是优化启动参数,你都能从编译原理和运行时机制两个维度找到根源,记住一句话:开发向JDK要工具,运行向JRE要环境,性能向JVM要解释。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/641317.html





