从一次Java方法调用说起
Java虚拟机的执行流程是一条从字节码加载、校验、解析、初始化到执行、优化、回收、卸载的完整链路,核心机制可概括为“类加载子系统负责把.class文件搬进内存,执行引擎负责把字节码变成机器指令,运行时数据区负责在中间承接所有活数据”。
如果把JVM比作一家工厂,类加载器就是原料采购部门,执行引擎是车间里的老技工,垃圾回收器则是负责清场和回收报废零件的后勤组,整条链路环环相扣,任何一个环节卡住,程序就会原地罢工,下面顺着原料进厂到成品报废的顺序,把这套流程拆开来看。
Java虚拟机执行流程和JVM内存模型的区别在哪?
业内专家指出,这两个概念经常被混着说,其实分工差别很大,执行流程讲的是“时间线上的动作”:类怎么进来、字节码怎么被翻译、方法怎么被调用、对象怎么被回收,JVM内存模型讲的是“空间上的布局”:方法区、堆、虚拟机栈、本地方法栈、程序计数器各放什么,执行流程是动词,内存模型是名词,理解执行流程,必须先把运行时数据区当作舞台背景,否则字节码在哪儿跑、栈帧怎么压都说不清楚。
从.class到机器码:JVM类加载过程面试题常考环节
JVM类加载过程面试题出现频率很高,核心就三个阶段:加载、链接、初始化。
- 加载:类加载器根据全限定名找到.class文件,读成一个二进制字节流,然后在方法区生成这个类的运行时数据结构,在堆里生成一个Class对象作为访问入口。
- 链接:分三步走,验证阶段检查字节码格式、语义、符号引用是否合法,防止恶意代码混进来;准备阶段给静态变量分配内存并设零值,注意是零值,不是代码里写的初始值;解析阶段把常量池里的符号引用替换成直接引用。
- 初始化:执行类构造器
<clinit>(),给静态变量赋真实值,跑静态代码块,这一步由JVM保证线程安全,父类先于子类初始化。
面试里常会追问双亲委派模型,启动类加载器、扩展类加载器、应用类加载器层层向上问,先让父加载器尝试,父加载器加载不了才自己动手,这样做的目的是保证核心类库不被篡改。
实操验证可以用javap看字节码:
javap -v -p Hello.class
输出里能看到常量池、方法签名和<clinit>的位置,想看类加载过程可以加参数:
java -verbose:class -cp . Hello
JVM字节码执行引擎工作原理详解
字节码进内存之后,真正干活的是执行引擎,它有两种工作模式:解释执行和即时编译。
解释执行像逐字逐句读稿子,读一条字节码执行一条,启动快但长期跑效率一般。即时编译把热点代码翻译成本地机器码再执行,运行效率高,但编译本身要花时间和内存,行业共识认为,现代JVM默认走混合模式:先解释执行,跑一会儿统计到热点方法后触发JIT编译,把热代码换成机器码,分层编译进一步把JIT分成C1和C2,C1编译快、优化少,C2编译慢、优化深,两者配合兼顾启动和峰值性能。
执行方法时,JVM会在当前线程的虚拟机栈里压入一个栈帧,结构包括:
- 局部变量表:放方法参数和局部变量,以变量槽为单位分配。
- 操作数栈:执行字节码时临时存取数据,压栈弹栈。
- 动态链接:指向运行时常量池中该方法的符号引用。
- 方法返回地址:记录方法执行完后回到哪。
方法调用有解析和分派两条路,静态方法、私有方法、构造器这类在编译期就能确定版本的,走解析,普通实例方法走动态分派,根据调用者的实际类型在方法区里查虚方法表。invokedynamic则是给Lambda和动态语言用的,把调用点绑定延迟到运行时。
想看JIT编译情况可以打开编译日志:
java -XX:+PrintCompilation -cp . Hello
输出会逐行显示哪些方法被编译、用了哪层编译器。
JVM对象从加载到卸载的完整过程
对象的一生从new开始,到GC判断不可达结束,沿时间线看:
- 类加载完成后,执行
new指令时JVM在堆里分配内存,常用指针碰撞或空闲列表两种方式。 - 分配完内存设零值,再执行构造方法完成初始化。
- 对象从新生代Eden区开始,经过Minor GC存活后进入Survivor区,年龄够大或Survivor装不下时晋升老年代。
- 老年代发生Full GC时若对象仍不可达,被回收。
卸载比回收更严格,类卸载要同时满足三个条件:该类所有实例都被回收、加载该类的ClassLoader被回收、该类的Class对象没有引用,多数情况下只有自定义类加载器加载的类才有机会卸载,系统类加载器加载的核心类基本常驻。
JVM卸载类加载器和卸载类的区别是什么?
这两个动作经常被合并理解,但顺序和条件不同。
| 对比项 | 卸载类加载器 | 卸载类 |
|---|---|---|
| 触发条件 | 加载器实例不可达,被GC回收 | 类实例全回收、ClassLoader被回收、Class对象无引用 |
| 影响范围 | 该加载器加载的所有类一起失去归属 | 单个类的元数据从方法区清除 |
| 常见场景 | OSGi、热部署、插件容器 | 动态生成类、反射生成的临时类 |
| 系统类加载器 | 基本不卸载 | 基本不卸载 |
可以理解为:加载器是“户口本”,类是“户口本上的成员”,户口本销毁了,上面的成员自然就没了;单独把一个成员迁走,户口本还在,热部署框架能做到代码热替换,靠的就是用新的自定义类加载器重新加载类,旧加载器连同旧类一起被回收。
观察类卸载可以加参数:
java -XX:+TraceClassUnloading -cp . Hello
Q&A:关于Java虚拟机执行流程的常见追问
Q:Java虚拟机执行流程中类加载和对象创建是同一回事吗?
A:不是,类加载是把类的元数据搬进方法区、在堆里生成Class对象,属于类级别的一次性动作,对象创建是在类加载完成后,执行new时在堆里分配实例内存并初始化,属于实例级别、可重复发生的动作。
Q:字节码为什么不直接翻译成机器码,还要经过解释执行这一层?
A:解释执行是启动阶段的低成本方案,如果全部靠JIT编译,启动时编译开销大、内存占用高,短生命周期程序会得不偿失,混合模式让程序一边跑一边收集热点信息,把编译资源花在真正高频的方法上,整体性价比更高。
Q:Java虚拟机执行流程和垃圾回收是什么关系?
A:垃圾回收是执行流程里的后勤环节,字节码执行过程中不断创建对象、方法返回后栈帧弹出,堆里的对象逐渐变为不可达,GC根据可达性分析找出这些对象并回收,回收后类加载器若也不可达,类才可能进入卸载阶段,整条流程从加载到卸载,GC始终在后半程承上启下。