你写的每一行Java代码,从保存文件到真正跑起来,中间隔着7个精密环节。理解它们,才能解释为什么你的服务启动慢、为什么内存会炸、为什么同样的代码第二次运行更快。
编译:从人话到字节码
![]()
javac 做的不是"翻译",而是"降维"。
它把你的Java源码变成字节码(bytecode)——一种平台无关的中间格式,存成 .class 文件。这些文件可以打包进JAR,或者按Java 9之后的模块系统组织。
关键认知:字节码不是机器码。它跑不了,只能被JVM"解释"或"编译"后才能执行。这是Java"一次编写,到处运行"的根基,也是性能争议的源头。
加载:类加载器的层级博弈
类加载器子系统采用双亲委派模型(parent delegation)。不是"先自己找",而是"先问爸爸"。
三层分工:
• 启动类加载器(Bootstrap):加载JDK核心类,java.lang.* 这些。用C++实现,Java代码里看不见。
• 平台类加载器(Platform):加载扩展类,JDK的 lib/ext 目录或模块路径。
• 系统类加载器(System):加载你的应用代码,CLASSPATH指定的那些。
为什么要"先问爸爸"?安全。防止你写一个 java.lang.String 把JDK的替换掉。也防止重复加载——父加载器加载过的,子加载器不再加载。
但打破双亲委派也有场景。Tomcat给每个Web应用独立类加载器,就是为了让同名的Servlet类可以隔离共存。OSGi、SPI机制(Service Provider Interface)也是破坏者。
链接:验证、准备、解析
类加载进来后,进入链接阶段,三步走:
验证(Verify):JVM检查字节码是否合法。操作数栈不会溢出、类型转换安全、跳转指令不越界。这是安全沙箱的第一道防线。
准备(Prepare):为类的静态变量分配内存,并赋予默认值。static int count = 10,这时候 count = 0。真正的10还没来。
解析(Resolve):把符号引用变成直接引用。你的代码里写 new UserService(),编译后是个字符串符号。解析阶段找到UserService类在内存里的实际地址,把指针填进去。
解析可以延迟。用到的时候才解析,节省启动时间。
初始化:静态世界的诞生
这是类加载的最后一步,也是程序员最能感知的一步。
静态变量被赋予真正的初始值,static 代码块按顺序执行。这个过程是线程安全的——JVM保证一个类的初始化只会发生一次,多线程竞争时只有一个能执行,其他的阻塞等待。
触发初始化的时机:new实例、访问静态变量/方法、反射调用、子类初始化触发父类初始化、main方法所在类。被动引用不会触发,比如只定义父类的静态字段引用。
很多"诡异"的类加载问题,根源在这里。循环依赖的静态初始化、静态块里的耗时操作,都会在这一步暴露。
内存布局:线程共享与隔离
JVM内存不是一块大平地,而是严格分区的建筑。
线程共享区:
• 堆(Heap):所有对象实例和数组。GC的主战场。新生代、老年代、永久代/元空间,都在这里。
• 方法区(Method Area):类信息、常量池、静态变量、JIT编译后的代码缓存。JDK 8之前叫永久代(PermGen),之后叫元空间(Metaspace),移到了本地内存。
线程私有区:
• JVM栈(Stack):每个线程一个栈,栈帧对应方法调用。局部变量表、操作数栈、动态链接、方法返回地址。
• 程序计数器(PC Register):当前线程执行的字节码行号指示器。线程切换时靠它恢复位置。
• 本地方法栈(Native Method Stack):给JNI调用的C/C++方法用的栈。
直接内存(Direct Memory)不在JVM规范里,但NIO的 ByteBuffer.allocateDirect 用它。绕过堆,减少一次拷贝,但不受GC直接管理,容易内存泄漏。
执行引擎:解释与编译的拉锯战
字节码终于要被"执行"了。JVM有两种武器:
解释器(Interpreter):逐条读字节码,边解释边执行。启动快,执行慢。适合跑一两次的代码。
JIT编译器(Just-In-Time Compiler):把热点代码编译成本地机器码,存到代码缓存(Code Cache)。后续直接执行机器码,速度接近C++。但编译本身耗时,触发需要"预热"。
热点检测基于方法调用计数器和回边计数器。方法被调用多少次,循环执行多少轮,达到阈值就进编译队列。
C1编译器(Client Compiler):快速编译,优化少,适合桌面应用快速启动。C2编译器(Server Compiler):深度优化,激进编译,适合服务端长期运行。Java 7引入的分层编译(Tiered Compilation),先C1后C2,平衡启动和峰值性能。
Graal编译器是C2的替代者,用Java写编译器,支持AOT(Ahead-Of-Time)编译,让Java也能像Go一样快速冷启动。
垃圾回收:自动化的代价与选择
堆内存的回收是JVM最复杂的子系统。对象存活判定、回收算法、收集器实现,决定了你的服务是丝滑还是卡顿。
判定算法:
• 引用计数:Python用,JVM不用。循环引用解决不了。
• 可达性分析:从GC Roots(栈帧局部变量、静态变量、常量、JNI引用等)出发,能走到的对象存活,走不到的回收。
回收算法:
• 标记-清除(Mark-Sweep):先标记存活,再清除死亡。碎片多。
• 复制(Copying):内存分两半,存活对象复制到另一半,整片清空。无碎片,但内存利用率50%。新生代用这个,因为大部分对象朝生夕死,复制成本低。
• 标记-整理(Mark-Compact):标记后把存活对象往一端移动,清理边界外。老年代用这个,对象存活率高,复制不划算。
收集器演进:
Serial/Serial Old:单线程,STW(Stop-The-World)长。现在只配做客户端默认。
Parallel/Parallel Old:多线程并行,吞吐优先。JDK 8默认。
CMS(Concurrent Mark Sweep):并发低停顿,但碎片严重,JDK 14已移除。
G1(Garbage First):Region化内存,预测停顿时间,平衡吞吐和延迟。JDK 9+默认。
ZGC/Shenandoah:亚毫秒级停顿,TB级堆,染色指针、读屏障。JDK 15+生产可用。
选收集器不是越新越好。ZGC的吞吐量比G1低10%-20%,如果你的服务是批处理而非在线服务,可能反而更慢。
本地接口:JNI的双刃剑
Java代码调用C/C++库,走Java本地接口(JNI,Java Native Interface)。
流程:Java声明native方法 → 生成头文件 → C/C++实现 → 编译成动态链接库 → System.loadLibrary加载。
JNI打破了JVM的安全边界。C代码的内存泄漏、段错误,会直接搞崩整个JVM进程。字符串、数组在JNI里要手动管理内存,Get/Release配对。局部引用表有限,大量对象要及时DeleteLocalRef。
Netty的零拷贝、TensorFlow Java API、各种数据库驱动,底层都依赖JNI。用得好是性能利器,用不好是稳定性的噩梦。
实战:从启动慢到诊断工具链
理解JVM结构后,常见的性能问题有了排查路径。
启动慢:检查类加载数量(-verbose:class),看是不是加载了太多jar。用AppCDS(Application Class-Data Sharing)把类元数据缓存到文件,下次直接映射,省掉解析验证。
内存溢出:堆溢出看对象存活周期,用MAT(Memory Analyzer Tool)分析dump文件。元空间溢出检查动态生成类(CGLIB、反射)是否泄漏。直接内存溢出,看NIO的Buffer有没有手动释放。
卡顿:打印GC日志(-Xlog:gc*),看STW时间。用async-profiler做火焰图,定位热点方法。JIT编译本身也会暂停,C2编译复杂方法时的去优化(deoptimization)可能引发抖动。
工具链:
• jps:看Java进程
• jstat:监控GC、类加载、编译统计
• jmap:生成堆dump
• jstack:打印线程栈
• arthas:阿里巴巴开源,在线诊断,反编译、热更新、trace方法耗时
• JFR(Java Flight Recorder):低开销持续记录,JDK商业特性开源后的利器
为什么这7步值得刻进脑子
云原生时代,JVM的"重"被反复诟病。启动慢、内存大、容器里表现诡异。
但理解这7步,你会发现优化空间:
• GraalVM的AOT编译,把初始化阶段提前到构建时,冷启动进到毫秒级
• CRIU(Checkpoint/Restore In Userspace)冻结预热后的JVM状态,恢复时跳过前面6步
• Quarkus、Micronaut框架,编译期做依赖注入,减少运行时反射和类加载
JVM不是黑箱。从字节码到机器码的旅程,每一步都有设计取舍,也都有调优杠杆。下次面试被问到"对象怎么创建的"、线上遇到"Metaspace OOM"、争论"Java到底慢不慢",这7步就是你的底层操作系统。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.