一个 .class 文件是怎么被 JVM 跑起来的?类加载机制 + 双亲委派一篇讲透
你写的 Java 代码编译成.class之后,JVM 到底对它做了什么,才能让它真正跑起来?
为什么静态代码块只执行一次?为什么你自己写一个java.lang.String不会生效?JDBC 加载驱动为什么非得用Class.forName?
这些问题的答案,全都指向同一个东西——类加载机制。今天咱们就把它彻底讲明白。
一、类的一生:7 个阶段
一个类从进 JVM 到被卸载,要走 7 个阶段:
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
其中验证、准备、解析合起来叫"连接"。
打个比方:加载就像把原材料运进工厂,验证是质检,准备是先把货架摆好(放上空盒子),解析是把"说明书上的零件名"翻译成"仓库里的具体位置",初始化才是真正开工组装。
有个细节:解析阶段的顺序不固定,它可以拖到初始化之后,这是为了支持多态(运行时才知道具体调哪个方法)。
二、加载:把字节流搬进内存
加载阶段类加载器干 3 件事:
- 通过类的全限定名,拿到这个类的二进制字节流(不一定是 .class 文件,也能来自 jar、网络、甚至动态生成)
- 把字节流的静态结构,转换成方法区里的运行时数据结构
- 在堆里生成一个代表这个类的
java.lang.Class对象,以后反射、new 都靠它当入口
三、连接三阶段
验证:先过安检
验证就是防止一份恶意的、损坏的字节流搞崩虚拟机。要查四样:文件格式(开头魔数是不是0xCAFEBABE)、元数据(有没有乱继承 final 类)、字节码(方法里的逻辑合不合法)、符号引用(引用的东西找不找得到)。
准备:静态变量先给零值(高频考点)
准备阶段给静态变量(static)分配内存,先设成零值,这时候一行你的代码都还没执行。
publicstaticintvalue=123;准备阶段结束后,value是0,不是 123。赋值成 123 这个动作,得等到初始化阶段。
各种类型的零值:int 是 0、boolean 是 false、引用类型是 null、char 是\u0000。
但有个特例——static final编译期常量,准备阶段就直接赋好值:
publicstaticfinalintCONSTANT=123;这种情况下准备阶段CONSTANT就已经是 123 了。
解析:符号引用变直接引用
解析就是把常量池里的"符号引用"换成"直接引用"。符号引用只是个名字(比如类的全限定名),跟内存无关;直接引用是真正的内存指针、偏移量,能直接定位到目标。
四、初始化:真正执行你的代码
初始化阶段 JVM 会执行一个叫<clinit>()的方法,它是编译器把你写的所有静态变量赋值 + 静态代码块自动合并出来的。
两条关键规则:
- 执行子类的
<clinit>前,保证父类的先执行 - JVM 会给
<clinit>加锁,保证多线程下一个类只初始化一次(这就是静态内部类单例线程安全的原理)
初始化顺序实测
classParent{staticinta=1;static{System.out.println("父类静态代码块 a="+a);}{System.out.println("父类构造代码块");}Parent(){System.out.println("父类构造方法");}}classChildextendsParent{staticintb=2;static{System.out.println("子类静态代码块 b="+b);}{System.out.println("子类构造代码块");}Child(){System.out.println("子类构造方法");}}第一次new Child()的真实输出:
父类静态代码块 a=1 子类静态代码块 b=2 父类构造代码块 父类构造方法 子类构造代码块 子类构造方法第二次再new Child(),静态代码块不执行了(类只初始化一次),只剩构造代码块和构造方法。
什么时候会触发初始化?
记住 6 种主动引用:new 对象、读写非 final 静态字段、调用静态方法、反射Class.forName、初始化子类时父类先初始化、启动时含 main 方法的主类。
五、类加载器:三兄弟
| 加载器 | 负责加载 | 特点 |
|---|---|---|
| Bootstrap 启动类加载器 | lib 核心库(java.lang.*) | C++ 写的,Java 里拿到的是 null |
| Platform 平台类加载器 | lib/ext 扩展库 | JDK9 前叫 Extension |
| Application 应用类加载器 | classpath 你的类 | 默认加载器 |
为什么String.class.getClassLoader()返回 null?因为 String 是 Bootstrap 加载的,Bootstrap 是 C++ 实现、没有对应的 Java 对象,所以用 null 表示。
六、双亲委派模型(核心中的核心)
它是怎么工作的?
一个类加载器收到加载请求,自己不先动手,而是一路向上委托给父加载器,直到 Bootstrap。父加载器说"我这边找不到",子加载器才会自己去加载。
为什么非得这么绕?
两个好处:
- 避免重复加载、保证类唯一:同一个类不管被谁请求,最终都由同一个加载器加载,不会出现两个"一样的类"
- 安全:你要是自己写了个
java.lang.String想搞事情,请求会向上委托给 Bootstrap,加载的还是 JDK 自带的那个真 String,你的假的根本不会被加载
七、谁打破了双亲委派?
| 场景 | 怎么打破的 |
|---|---|
| JDBC 等 SPI | java.sql.Driver接口在 Bootstrap,但厂商驱动实现在 classpath,Bootstrap 看不见,于是用线程上下文类加载器反向加载 |
| Tomcat | 每个 webapp 一个独立类加载器,先自己加载再委托,实现应用之间类隔离 |
| OSGi / 热部署 | 网状加载器,模块能动态装卸 |
八、两个高频追问
Class.forName 和 ClassLoader.loadClass 有啥区别?
Class.forName默认会初始化类(执行静态代码块),所以加载 JDBC 驱动时能顺便完成驱动注册;ClassLoader.loadClass只加载、不初始化,把初始化推迟到真正使用。
静态内部类单例为什么线程安全?
外部类加载时不会马上加载内部类,第一次调用才加载;而初始化阶段 JVM 自动加锁保证实例只建一次,所以既线程安全又懒加载,还没有 synchronized 的开销。
九、口诀总结
| 口诀 | 意思 |
|---|---|
| 加载验证准备解析初始化 | 类生命周期,解析可延后 |
| 准备零值,初始化真值 | static 变量准备阶段是 0 |
| static final 准备就赋值 | 编译期常量特例 |
| 静态先行父先子 | 父类静态块先执行,只一次 |
| 收到请求先向上委托 | 双亲委派 |
| 双亲委派两好处:唯一+安全 | 不重复、防伪造核心类 |
| SPI 用线程上下文加载器 | 打破双亲委派典型 |
到这里,JVM 类加载机制就讲完了。加上前面的内存结构和垃圾回收,JVM 三大核心你已经拿下两块半。
下一篇咱们聊JVM 调优实战——OOM、CPU 飙高、频繁 Full GC 到底怎么排查。觉得有用点个赞关注一下,跟着系列走,面试不慌。
#Java #JVM #类加载 #双亲委派 #java面试 #后端开发