JVM常量折叠主要由JIT编译器在运行时完成,javac仅对编译期可完全求值的字面量表达式(如2+3*4、"a"+"b")做有限折叠;final局部变量参与运算可折叠,非final变量或含方法调用则不折叠;反编译器可能误导,应以javap -c查看真实字节码。

JVM 不会做你想象中的“常量折叠”,至少不是在字节码生成阶段由 javac 主动完成的;真正发生折叠的位置是 JIT 编译器(运行时),而 javac 只对部分简单、无副作用的字面量表达式做有限折叠 —— 这点直接决定你反编译看到的字节码是不是“干净”的。
javac 什么时候会折叠常量?只看字面量和编译期可确定的表达式
javac 的常量折叠非常保守:仅限于基本类型字面量(int、long、String 等)之间的纯运算,且整个表达式必须在编译期完全可求值、无任何方法调用或变量引用。
-
int x = 2 + 3 * 4;→ 折叠为int x = 14;(字节码中直接是BIPUSH 14) -
String s = "a" + "b" + "c";→ 折叠为String s = "abc";(LDC "abc") -
final int a = 5; int y = a + 10;→ 折叠(因为a是static final或局部final且已初始化) -
int b = someMethod() + 1;→ 不折叠(有方法调用) -
String t = s + "d";(s是非 final 变量)→ 不折叠,生成StringBuilder调用
为什么反编译看不到折叠?可能你没关掉调试信息或用了错误工具
即使 javac 折叠了,某些反编译器(如旧版 JD-GUI)会“还原”逻辑,把 LDC "abc" 显示成 "a" + "b" + "c",造成“没折叠”的错觉。真实字节码才是依据。
- 用
javap -c看最可靠:javap -c MyClass | grep 'LDC\|BIPUSH'直接查加载指令 - 确保编译时没加
-g(虽然不影响折叠,但冗余调试信息会让javap输出变长) - 避免用 IDE 内置反编译器判断——它们倾向“可读性优先”,不是字节码忠实映射
-
final String s = "x"; String t = s + "y";:如果s是类字段且非static,javac通常不折叠(因涉及字段读取)
JIT 才是真正的折叠主力,但你看不见它生成的机器码
运行时,HotSpot 的 C2 编译器会对反复执行的代码做激进优化,包括常量传播、死代码消除、甚至把 Math.abs(-5) 直接替换成 5。但这发生在本地机器码层面,javap 完全看不到。
- 这种折叠依赖运行时 profile(比如方法被调用上万次后才触发 C2 编译)
- 开启
-XX:+PrintCompilation可观察哪些方法被 JIT 编译 - 用
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly(需 hsdis)才能看汇编码,但输出极长且平台相关 - 别指望靠反编译验证 JIT 行为——它不改字节码,只替换执行路径
真正容易被忽略的是:常量折叠是否发生,不仅取决于表达式本身,还取决于变量声明位置(局部 vs 字段)、修饰符(final 是否足够“强”)、以及你用什么工具看结果。信 javap -c,不信反编译器的“漂亮输出”。









