编译期是javac将.java翻译为.class的过程,仅做静态检查;运行期是java命令加载字节码并执行的阶段,才发生内存分配、方法调用和异常抛出。

javac 把 .java 翻译成 .class 的那一刻,就是编译期;而 java 命令真正把字节码加载进内存、执行方法、分配对象、抛出 NullPointerException 的那一段,才是运行期——两者不是时间先后关系,而是职责完全不同的两个阶段。
编译期只做“静态检查”,不碰内存也不执行逻辑
编译器 javac 就像一个严苛的语文老师:它逐行扫描词法、语法、类型声明,但不会运行你写的任何一行代码。它能发现 int x = "hello";(类型不匹配)、System.out.println(y);(变量 y 未声明)、new FileInputStream("a.txt"); 没加 try-catch(受检异常未处理)这类错误,但对下面这些完全无感:
-
String s = null; s.length();→ 编译通过,运行时才崩 -
int[] arr = {1}; arr[5];→ 编译不报错,运行抛ArrayIndexOutOfBoundsException -
List list = new ArrayList(); list.add("x"); String x = (String) list.get(0);→ 编译期擦除泛型,不检查实际类型
关键点:编译期生成的 .class 文件里,只有“怎么分配内存”的指令模板(比如字段偏移量、栈帧大小),没有真实内存地址,更不会真的分配堆或栈空间。
运行期才真正“活起来”,所有动态行为在此发生
JVM 启动后,从类加载开始,到方法调用、对象创建、GC 回收,整个过程都属于运行期。此时才会暴露编译期无法捕获的问题:
- 类找不到:
ClassNotFoundException(.class文件缺失或 classpath 错误) - 类加载失败:
ExceptionInInitializerError(静态块里抛了异常) - 多态绑定:
Animal a = new Dog(); a.makeSound();→ 编译期只认Animal有该方法,运行期才决定调用Dog的实现 - 反射调用:
Class.forName("NonExistent").newInstance()→ 编译期完全无法验证类是否存在
注意:final、static、private 方法在编译期就绑定(静态绑定),而普通实例方法默认是运行期绑定(动态绑定),这是多态和框架(如 Spring AOP)能工作的底层前提。
立即学习“Java免费学习笔记(深入)”;
常见混淆点:编译期“分配内存”其实是假命题
很多资料说“编译期分配内存”,这是严重误导。真实情况是:
- 编译期只计算字段大小、方法栈帧所需局部变量槽数量,并写进
.class的常量池和属性表中 - 真正的内存分配(堆上新建对象、栈上压入帧、方法区加载类结构)全部发生在运行期,由 JVM 在类加载的
准备阶段和初始化阶段完成 - 比如
static int x = 10;:编译期记录“这个字段初始值是 10”,运行期类加载的准备阶段给x分配内存并设默认值 0,初始化阶段再执行赋值为 10
所以,当你看到 OutOfMemoryError: Java heap space,这一定是运行期问题,跟 javac 一点关系都没有。
实战建议:如何快速定位问题是编译期还是运行期?
看错误出现时机和错误类型最直接:
- 用
javac Main.java报错 → 编译期(错误信息含error:,如cannot find symbol) - 用
java Main启动后崩溃 → 运行期(错误以Exception in thread "main"开头,带完整堆栈) - IDE 里红色波浪线 + 提示“Unresolved symbol” → 编译期检查提前拦截
- 单元测试跑通但线上偶发
NullPointerException→ 典型运行期逻辑缺陷(比如依赖外部服务返回 null)
真正难的不是区分阶段,而是理解:编译期能拦住的只是冰山一角;大量业务逻辑错误、并发问题、资源泄漏、NPE,全藏在运行期黑盒里——它们不会在 javac 那儿亮红灯,只能靠日志、监控、断点和经验去挖。










