☰
JDK、JRE、JVM与Java运行流程全解析:从字节码到跨平台执行
2026/10/15 5:48:02 网站建设 项目流程

如果你刚开始接触Java,大概率会被三个名词绕晕:JDK、JRE、JVM到底谁包含谁?写的Java代码是怎么从一个.java文件变成正在运行的程序的?以及,Java凭什么敢说“一次编写,到处运行”?我在带新人的时候发现,哪怕工作了两三年的同学,也未必能把Java运行流程、JDK/JRE/JVM的边界这几个概念串成一条完整的链路。这篇文章我就把这些东西一次性讲透,顺带把我实际配置环境和排查问题时踩过的坑也一并分享出来。

无论你是刚学Java的学生,还是想系统补基础的在职开发,读完之后你至少能回答三个问题:字节码是怎么跑起来的?JVM在整条链路里到底干了多少活?怎么搭建一套靠谱的Java运行环境?

1. Java程序从源码到执行的完整旅程

1.1 第一步编译:javac把.java变成字节码

先看最基础的流程。我们在项目里写了一个HelloWorld.java文件,内容大概是这样的:

public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }

要让它跑起来,第一步是用javac命令编译:

javac HelloWorld.java

命令执行完,当前目录多出来一个HelloWorld.class文件。很多人以为编译就是把Java源码变成机器码,这是最经典的误解。javac干的事是:把.java源码翻译成JVM能够识别的字节码,而不是CPU能直接识别的机器码。

这就像是你写了一份英文说明书,先翻译成一份“国际通用语言”版本,再交给不同国家的人去翻译成本地语言。字节码就是这个“国际通用语言”。你可以用任意文本工具打开.class文件看一眼,或者用javap -c HelloWorld命令反编译看看里面的字节码:

javap -c HelloWorld

会看到一堆诸如invokestatic、getstatic、return这样的指令,阅读起来确实不友好,但这就是JVM的“母语”。

编译阶段做的事情远不止翻译。词法分析、语法分析、语义分析、常量池生成、字节码指令生成,这些都在javac内部完成。有一件很反直觉的事情:javac几乎不做性能优化,甚至很多“聪明的优化”反而是留给运行期的JIT编译器去做的。所以编译阶段只保证“正确性”,不追求“极致性能”。

1.2 类加载:JVM怎么把class文件变成内存里的类

编译出.class文件之后,下一步是启动JVM并加载这个类。很多人以为java HelloWorld是把整个文件一次性读进内存,实际上JVM采用了一种“按需加载”的策略。

java HelloWorld

这条命令做的事情,远远不只是启动应用。它会先创建JVM环境,然后通过类加载器(ClassLoader)找到HelloWorld.class,完成“加载、验证、准备、解析、初始化”这一整套流程,这才算把类真正准备好。

  • 加载:从磁盘、网络或内存中找到类的二进制字节流,并生成对应的Class对象。
  • 验证:检查字节码格式是否合法,防止恶意或错误代码伤害JVM。比如cafe babe开头这种魔数校验就是在这里做的。
  • 准备:为类的静态变量分配内存并赋默认值。比如static int count;这一步会先把count设为0。
  • 解析:把常量池中的符号引用替换为直接引用。简单理解就是“这个类里提到的其他类,具体内存地址在哪”。
  • 初始化:真正执行静态代码块和静态变量的赋值逻辑。这个阶段可能会触发父类初始化。

类加载器本身也不是单一个体,而是有层次结构的。启动类加载器负责加载JDK核心类,扩展类加载器负责加载扩展包,应用类加载器负责加载项目classpath下的类。一个类从父类加载器一层层往下查找,类不会重复加载,这就是常说的“双亲委派模型”。

我做性能分析时经常遇到ClassNotFoundException,十有八九就是“加载”阶段没找到类,而不是类文件损坏。这种问题多数跟classpath配置有关,后面我会专门展开讲。

1.3 运行时数据区:类加载完以后内存里长什么样

类加载完成之后,JVM要划分出一块区域来运行程序。这块区域被叫做运行时数据区,它并不是一整块大内存,而是分工明确的几个子区域:

  • 程序计数器:当前线程正在执行的字节码指令地址,线程私有。真正实现多线程切换的关键。
  • 虚拟机栈:每个线程一个栈,栈里存着“栈帧”。每调用一个方法,就压入一个新栈帧。局部变量、方法返回值、部分运算结果都在栈帧里。
  • 本地方法栈:为native方法服务。
  • 堆:几乎所有对象实例都在这里分配。这是GC(垃圾回收)的主要战场。
  • 方法区:存储类元数据、常量池等。JDK 8之后改用“元空间”,并且使用本地内存。

拿我们的HelloWorld举例:执行main方法时,JVM创建一个线程,往虚拟机栈里压入main对应的栈帧;执行System.out.println时,又会压入println的栈帧。对象"Hello, World!"这个字符串则在堆中分配。

新手经常把“栈”和“堆”搞混。我用一句话总结:栈管运行,堆管存储。方法执行的一层层嵌套调用,对应栈帧的一层层入栈出栈;不断new出来的对象,对应堆里不断扩大的存活空间。栈溢出往往发生在递归太深,堆溢出则多半是对象堆积太多、来不及回收。

1.4 执行引擎与JIT:解释执行和编译执行的博弈

类加载完成、内存分配好了,代码怎么真正跑起来?这一步属于执行引擎。

早期的JVM采用“逐字节码解释执行”的方式:每条字节码指令都翻译成一组机器指令,然后执行。这种做法的优点是JVM实现简单,但缺点是同样的代码每次运行都要翻译一遍,性能自然不好。

现代JVM早就不是“纯解释执行”了。JVM会统计哪些代码执行频率非常高,这些被叫作“热点代码”,比如一个循环几百万次的方法体。对于热点代码,JVM会使用JIT编译器直接把它编译成本地机器码,然后缓存起来。后面再执行时,直接调用编译好的机器码,速度翻倍。

这个过程就是“解释执行优先,编译执行兜底”。有一个阶段叫作分层编译,根据阈值动态从解释模式升级到C1编译,再升级到C2编译,平衡启动速度和峰值性能。

我举个形象的例子:解释执行就像同声传译,说一句翻一句;JIT编译就像提前把演讲稿翻译并打印好,会议开始时直接照着念,自然更快。

从JDK 9开始还引入了AOT编译,可以提前在编译期把字节码编成机器码,减少启动时间。但AOT对动态特性的支持不完美,所以目前主力还是JIT。

最后把整条链路串起来:

源码 → javac编译 → 字节码 → 类加载 → 运行时数据区分配 → 执行引擎(解释/JIT) → 操作系统 → 硬件

这条链路是理解后面所有内容的基础。

2. JDK、JRE、JVM:一场持续多年的混淆

2.1 三者的包含关系和职责

网上看到很多教程画三层同心圆:JDK在最外层,包含JRE;JRE在中间,包含JVM。直觉上没问题,但容易误导人。我更喜欢用“职责分工”来理解:

  • JVM(Java Virtual Machine):负责让字节码能在不同操作系统上运行,是整条执行链路的“运行时承载者”。
  • JRE(Java Runtime Environment):JVM加上Java核心类库和运行时依赖,负责“让已经编译好的Java程序能跑起来”。
  • JDK(Java Development Kit):在JRE基础上再加开发工具,比如javac、jar、javadoc、jshell等,负责“开发 + 运行一体化”。

给出一个对比表格会更清楚:

组件包含内容核心职责开发代码时需要吗
JDKJRE + 开发工具编译、调试、运行需要
JREJVM + 核心类库运行已编译程序不需要
JVM独立的执行引擎、内存管理、类加载解释或编译字节码,调用本地系统能力必须

注意,JRE并不是为了帮助开发,而是为了部署。如果你只写了一个可运行的jar包,要在服务器上跑,这时才考虑“只装JRE”。但现实情况是,服务器上装完整的JDK更省心,因为很多运行场景其实需要用到JDK自带的监控工具和诊断命令。

2.2 JDK安装目录里到底有哪些东西

我在给新人讲环境配置时,经常推荐他们去翻一下JDK安装目录,把它当一次“体检”。

JDK 8安装目录下的jre文件夹很容易吸引注意力。很多人判断“JDK里包含JRE”就是从这里来的。但如果你打开的是JDK 11或17的安装目录,会发现根本没有独立的jre目录,取而代之的是bin、conf、legal、lib、include等目录。

  • bin:存放所有可执行文件,包括java、javac、jar、jcmd、jhsdb等。
  • lib:存放运行时类库、模块文件等。JDK 9之后这里会有modules文件。
  • include:存放需要用JNI和本地代码交互的C/C++头文件。
  • conf:存放运行时配置文件。

也就是说,从JDK 9开始,官方不再单独对外发布“JRE安装包”了。当你安装一个完整JDK时,JVM、核心类库、开发工具全部都在里面。所以现在回答“JRE和JDK有什么区别”,一个很简略的答案是:JDK就是增强版的JRE,但现代JDK不再明确区分出独立JRE。

2.3 LTS版本怎么选:我从Java 8用到Java 21的体会

每次说起版本,总有人问:到底该用Java 8还是Java 17,或者Java 21?

我的建议很明确:优先选择LTS(长期支持)版本。Java 8、Java 11、Java 17、Java 21都属于LTS。下面是一些关键版本的核心变化:

版本大版本亮点我实际体感
Java 8Lambda、Stream、新日期时间API依然是很多老企业系统的主力
Java 11局部变量类型推断、ZGC出现、移除Java EE模块性能提升明显,推荐替换8
Java 17密封类、统一日志、switch增强当前我认为最稳的LTS
Java 21虚拟线程、Record Pattern、字符串模板并发编程体验大幅提升

如果是在新项目里,我会选择Java 17起步,因为它既稳定,生态支持也成熟。Java 21尽管有很多吸引人的特性,但不少第三方框架对21的适配还在打磨。选择版本时还要考虑一件关键事:JDK发行版的供应商和许可模式。过去用Oracle JDK容易担心商业授权问题,现在多数团队直接使用OpenJDK系发行版,比如大家常说的某发行版或社区版,都可以放心商用。

这里有一个我反复强调的细节:不要在生产环境单独装一个所谓“精简JRE”。很多部署事故就是因为少装了几个模块,程序启动时报模块找不到,排查半天还不如装完整JDK。

3. Java语言特性:为什么它能在企业级开发中独当一面

3.1 跨平台的真相:不是魔法,是标准化和规范化的结果

“一次编写,到处运行”是Java最著名的口号。但这并不是说Java程序能在所有设备上不加区分地运行,而是指:只要目标平台有对应的JVM实现,同一份字节码就能跑。

这在技术层面是怎么做到的呢?因为JVM对上层提供了一套统一的约定:字节码指令集、内存管理、线程模型、异常处理机制,全部有规范。每个平台的JVM实现负责把字节码“翻译”成对应平台的系统调用和机器指令。Windows上的JVM和Linux上的JVM内部实现不同,但对外提供的JVM规范是统一的。

所以当你的程序报错时,错误信息里往往带着平台相关信息,比如“Unable to open socket”或“Cannot allocate memory”,这些最终都是底层操作系统能力经过JVM暴露出来的。跨平台是特性,但不是免死金牌,最终真正落地时仍要考虑文件路径分隔符、字符编码、操作系统权限差异。

3.2 自动内存管理:程序员和GC的相爱相杀

Java最吸引人的一个特性是自动内存管理。在C/C++里,你new了一个对象,必须记得delete或free;Java里你只需要new,不需要关心对象什么时候释放,JVM内置的垃圾收集器会处理。

GC的基本思路是:从一组“根对象”出发,遍历所有可达对象并标记为存活,然后回收所有不可达对象占用的内存。这个过程叫标记-清除或标记-复制,不同收集器有不同的算法。

JVM堆通常被分成新生代和老年代:新生代里存放短命的临时对象,老年代里存放长期存活的对象。绝大多数对象生命周期很短,因此在新生代被回收的效率很高。

但GC不是完全没有代价。有一项指标叫STW(Stop-The-World),意思是必须暂停业务线程才能安全地回收内存。如果你在日志里看到GC pause,那就是STW的体现。现代G1和ZGC一直在努力缩短STW时间,但“暂停”只是变短,不会完全消失。

我在线上调优时遇到过系统内存充足但老年代持续增长的情况,最终原因是瑕疵代码里有一个大集合被某个静态变量引用,GC永远无法回收。自动内存管理不等于“不会内存泄漏”,它只保证你忘了释放也能回收,但如果你让对象一直被引用,数组越攒越大,OOM迟早会来。

3.3 多线程与并发:从Thread到虚拟线程

Java从语言层面就支持多线程。你写一个new Thread(() -> System.out.println("run")),线程就启动了。背后是操作系统线程,Java线程通常直接映射到平台原生线程。

多线程并发带来的最大挑战是共享资源的竞争。Java提供了synchronized关键字和java.util.concurrent下的各种工具:ReentrantLock、Semaphore、CountDownLatch、ConcurrentHashMap。这些工具在运行期都依赖于JVM内置的锁机制和内存屏障。

Java线程和操作系统线程一一对应带来的问题是:线程数量受系统资源限制。你的服务器上开几千个线程往往就撑不住了,因为每个线程要占独立的栈内存(默认大约1MB)。

JDK 21引入的虚拟线程改变了这个局面。虚拟线程由JVM调度,不是一对一映射到系统线程,它更像是一种“轻量级线程”。你用虚拟线程能轻松创建几十万个并发任务,底层只占用少数平台线程。对于大量IO等待场景,这个特性堪称革命性。

我给一个实际建议:并发编程里不要总用synchronized,先考虑java.util.concurrent包里的工具类。synchronized简单但粗粒度,在运行流程里会让其他线程阻塞等待,锁竞争激烈时性能会明显下降。

3.4 面向对象、泛型与函数式编程:语言演进如何影响运行流程

Java是一门口语化的面向对象语言。类、接口、继承、多态这些核心概念,最终都会映射到JVM的类加载、方法派发和栈帧调用流程中。

比如多态里最典型的场景:父类引用指向子类对象,调用子类重写的方法。JVM用的是“方法表”和“动态分派”。运行时会根据实际对象类型找到对应的方法入口,这比C++的虚函数表稍显笨重,但换来的是更灵活的动态扩展能力。

泛型是Java程序员绕不开的语法。有意思的是,Java的泛型在编译期检查、运行期擦除。你写的List<String>在编译后会被擦除成List,JVM运行期并不知道原来保存的是String。这也是为什么你用反射拿到List时,无法直接知道它泛型参数的原因。

Java 8引入的Lambda和Stream,让函数式编程成为习惯。Lambda表达式编译后通常转化为invokedynamic指令,这是JVM为了动态语言支持而设计的。运行期会通过“引导方法”生成实际调用逻辑。当时看到这条指令时,我才真正意识到:语言特性在变,JVM指令集也在持续演进。

3.5 异常处理机制:运行流程里的“安全网”

Java的异常处理采用“抛出-捕获”模型。你写的try-catch-finally,到了编译期会被编进Class文件的“异常表”中。当JVM执行方法时,每执行一条可能抛出异常的指令,都要查一下异常表,找到匹配的处理器,然后跳转过去。

异常处理不是零成本,throw new一套异常往往伴随着线程栈快照的采集,非常昂贵。我在性能排查时经常见到有人用异常控制业务流程,比如把“缓存不存在”当成Exception抛出去,再在顶层捕获。这在业务规模小时没问题,QPS一上去就会发现吞吐量骤降。

正确的做法是:异常只用于真正异常的情况。对于可预期的分支,比如缓存未命中、用户未登录、参数非法,应该使用返回值或结果对象,而不是异常。

4. 从零到一:搭建环境并让代码跑起来

4.1 选择并安装JDK:一种比较省心的方式

如果你是新开始学Java,就不要纠结那么多发行版。最稳妥的做法是:去OpenJDK社区或主流的发行版站点下载一个LTS版本的JDK,比如Java 17。

安装时记住两个原则:

  1. 不要安装在带空格的路径下,路径越短越好,比如C:\jdk-17。
  2. 最好不要让它自动帮你添加一堆系统PATH,手动配置会让你更清楚自己在做什么。

下载下来的安装包解压或安装后,打开命令行验证一下:

java -version

能看到类似:

openjdk version "17.0.9" 2023-10-17 OpenJDK Runtime Environment ... OpenJDK 64-Bit Server VM ...

这三行说明JVM、运行时、发行版都正常。

4.2 环境变量配置:JAVA_HOME和PATH的前世今生

新手经常听到必须配置JAVA_HOME和PATH,但很少有人讲清楚为什么。

JAVA_HOME:告诉系统和某些构建工具,JDK安装在哪个目录。比如Maven、Gradle、Tomcat都要靠这个变量找到JVM。

PATH:让操作系统能够直接找到java和javac命令。如果你不配PATH,每次运行都得输入完整路径C:\jdk-17\bin\java HelloWorld,非常别扭。

在Windows上,系统属性 → 高级系统设置 → 环境变量中添加:

JAVA_HOME=C:\jdk-17 PATH=%JAVA_HOME%\bin;%PATH%

在Linux/macOS上,可以往~/.bashrc或~/.zshrc里追加:

export JAVA_HOME=/opt/jdk-17 export PATH=$JAVA_HOME/bin:$PATH

这里我不建议再用CLASSPATH。远古时代写Java需要手动指定classpath,JVM默认从当前目录找类。现在有了-cp参数和构建工具,直接配CLASSPATH反而容易引发“这里有一个类,那里有一个类”的混乱局面。

4.3 用命令行跑通第一个程序

配好变量后,我们不要急着打开IDE,先在命令行验证一遍完整链路。我在教新人的时候,坚持让他们先不使用IDE,真正理解“编译-运行”发生了什么。

写一个Main.java:

public class Main { public static void main(String[] args) { System.out.println("Java version: " + System.getProperty("java.version")); } }

执行:

javac Main.java java Main

正常情况下会输出:

Java version: 17.0.9

带上包结构时也简单,目录结构为com/example/App.java,编译时用:

javac -d out com/example/App.java java -cp out com.example.App

这里-cp out指定classpath为out目录。你会发现这个思路贯穿所有Java程序,只不过IDE把这一步藏起来了。

如果你想验证“字节码跨平台”这件事,可以在一台机器上用javac编译出.class文件,然后把.class文件传到另一台装了JVM的机器上直接运行。前提是两边的Java版本兼容,这个操作很简单,但很多人从来没试过。

4.4 常用JVM参数与性能监控命令

写真实项目时,很少有人只用默认参数跑JVM。下面这几个参数是面试高频、实战刚需:

JVM参数作用示例
-Xms初始堆内存大小-Xms512m
-Xmx最大堆内存大小-Xmx2g
-XX:+UseG1GC使用G1垃圾收集器-XX:+UseG1GC
-Xlog:gc输出GC日志(JDK9+)-Xlog:gc:gc.log
-Xss设置线程栈大小-Xss1m

启动一个Java应用时:

java -Xms512m -Xmx2g -XX:+UseG1GC -jar myapp.jar

为什么要设置-Xms和-Xmx?如果初始堆太小、最大堆很大,JVM启动后可能频繁扩容,性能和稳定性都会受影响。一般建议两者设置相同,避免运行时堆大小震荡。

监控命令列表:

jps # 查看本机Java进程 jstat -gc <pid> # 查看GC情况 jstack <pid> # 查看线程栈 jcmd <pid> help # 万能诊断工具

有一次我为了定位再一个“突然卡死”的服务,用jstack打印线程栈,立刻看到有几个线程卡在锁等待上。没有这些命令,解决这种问题只能靠重启碰运气。

4.5 多JDK版本管理:一台机器同时装Java 8和Java 17

开发老项目时需要Java 8,新项目用Java 17,怎么办?不需要反复卸载重装,用版本管理工具更省心。

在Linux/macOS上非常推荐SDKMAN,安装完直接用命令切换:

curl -s "https://get.sdkman.io" | bash source ~/.sdkman/bin/sdkman-init.sh sdk list java sdk install java 17.0.9-tem sdk install java 8.0.392-tem sdk use java 8.0.392-tem

Windows上可以直接使用jEnv或手动切换JAVA_HOME。其实切换的原理都一样:修改JAVA_HOME环境变量指向哪个JDK目录,然后在PATH里优先使用%JAVA_HOME%\bin。只要没人把某个版本的绝对路径硬编码到PATH中,切换版本就能生效。

多版本共存最容易踩的坑是:命令行输入java -version显示的是A版本,但javac -version显示B版本。原因往往是PATH中两条路径的优先级顺序不对,先匹配到了某个老目录。查看完整解析路径,用:

which java which javac

把两处路径指向同一个JDK的bin目录就能解决。

5. 高频问题与排查技巧实录

5.1 一张表看懂高频异常

我在实际工作里遇到过各种各样的Java运行问题,下面这五个是最常见的代表,每个都值得记到笔记里。

异常/错误出现场景可能原因解决思路
ClassNotFoundException反射加载类classpath里没有这个类检查jar包依赖和classpath
NoClassDefFoundError类已定义但初始化失败缺失依赖或验证失败看是否有版本冲突
UnsupportedClassVersionError运行低版本JVM加载高版本class编译版本和运行版本不一致统一JDK版本
OutOfMemoryError: Java heap space对象堆积,堆内存满内存泄漏/堆太小dump堆内存分析
StackOverflowError递归无终止条件栈帧无限入栈检查递归方法和调用深度

这里要强调ClassNotFoundException和NoClassDefFoundError的区别:前者是“我想找这个类,根本没找到”,后者是“这个类之前出现过,但加载时出了问题,比如包含它的依赖冲突”。排查后者往往比前者更恶心。

还有一些经典错误比如GC overhead limit exceeded,意思是JVM已经花了一大堆时间做GC但没有任何空间回收,基本属于代码或配置有问题,要赶紧往下看GC日志。

5.2 排查思路:版本、类路径、资源占用三步走

遇到任何Java运行异常,我习惯按下面三步排查。

第一步,确认版本一致。执行:

java -version javac -version mvn -version

如果项目是用Maven构建的,还要看pom.xml里maven.compiler.source/target属性。出现UnsupportedClassVersionError时,往往是编译用的JDK 17生成class,运行时却用JDK 8。

第二步,确认classpath里到底有什么。可以使用:

java -cp myapp.jar SomeClass -verbose:class

这个参数会打印JVM实际加载了哪些类,来源是哪个jar。一旦看到加载路径不对,就知道哪个依赖出了问题。

第三步,确认资源占用。如果程序疑似内存溢出,先执行:

jstat -gcutil <pid> 1000

观察E、O区使用率以及FGC次数。如果老年代不断增长,Full GC频繁,基本就是堆空间不够或对象泄漏。想要更精细的分析,用jmap -dump:format=b,file=heap.hprof <pid>导出堆快照,然后用MAT工具分析。

我也遇到过一种情况:程序在IDE里跑得好好的,换到服务器就启动失败。最后发现是服务器PATH里没有配JAVA_HOME,IDE自带了一个内部JDK,但服务器命令行用的是系统默认旧版本。这个坑特别容易踩,所以配置完环境变量一定要先用命令行验证一遍。

5.3 关于“要不要深入学JVM”的个人看法

很多人问:我平时写业务代码,真的需要学JVM吗?我的回答是:你可以不成为一个JVM调优专家,但你不能完全不懂JVM。

如果你只会写for循环和if分支,那确实不懂JVM也能完成业务需求。可一旦遇到线上服务CPU飙高、接口延迟突增、偶发OOM,不懂JVM的人只能重启服务、加机器、摆烂。懂JVM的人可以先用jstack看线程在做什么,用jstat看GC情况,再决定是优化代码还是调整参数。

从学习路径来看,先理解我在第一节讲的运行流程,再关注JDK/JRE/JVM的区别,接着去了解Java特性对应的运行机制,最后实际动手用监控工具排查一两个问题。你会发现知识是成体系的,而不是零散记命令。

学JVM不需要背一堆参数,先掌握五个最有用的命令就够起步了:jps、jstat、jstack、jmap、jcmd。我每次带人排查,最多再配一个jvisualvm图形化工具看一眼内存曲线,大部分问题都能定位到方向。

5.4 最后分享一个我反复踩过的小坑

有一次我接手一个老项目,代码能正常编译,但运行时总是报NoSuchMethodError,看异常栈指向的是标准类库里的一个方法。查了半天,发现是项目里引了一个非常老的第三方库,它把某个核心类打进了包里,导致类加载时优先加载了那个老版本。这种问题最隐蔽,因为它不是编译错误,而是运行期才爆雷。

遇到这种“明明代码没毛病但运行诡异”的问题,直接用-verbose:class打印类加载日志,会看到某个类实际来自哪个jar包。一旦发现异常,移除冲突依赖通常能解决。这也是为什么我强调要理解类加载机制,它不只是面试题,而是实打实的排错利器。

写到最后,我想说,Java这套体系之所以看起来复杂,是因为它把“开发”和“运行”分得很开。理解JDK/JRE/JVM的区别,不是靠背定义,而是靠亲手敲一遍命令、观察一遍文件、排查一遍报错。你按照本文的链路从编译到字节码再到JVM参数配置,完整跑通一个项目之后,以后再看到类似异常,心里就不会发慌了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询