【面试真题拆解05】Java类加载过程+双亲委派机制

四季读书网 2 0
【面试真题拆解05】Java类加载过程+双亲委派机制

什么是类加载?

简单来说,类加载就是把.class文件(编译后的字节码)从磁盘/网络加载到JVM内存里,并生成对应的Class对象的过程。

Java类加载的5个核心阶段

类加载不是一步到位的,而是分成了5个阶段:加载→验证→准备→解析→初始化,其中加载、验证、准备、初始化的顺序是固定的,解析可能在初始化之后(为了支持动态绑定)。

1. 加载(Loading)

这是类加载的第一步,举个例子:假如你要加载User类,JVM会先找到User.class文件,把它的信息存到方法区,然后在堆里生成一个Class对象,以后你要访问User类的信息,都通过这个Class对象。

2. 验证(Verification)

这一步是为了保证JVM的安全,防止恶意或不规范的.class文件搞破坏,主要验证四个方面:

  1. 文件格式验证:检查.class文件的魔数(开头的CAFEBABE)、版本号(JDK版本兼容)等;
  2. 元数据验证:检查类的继承关系(比如不能继承final类)、方法重写是否合法等;
  3. 字节码验证:检查字节码指令是否安全(比如不会跳转到非法指令、不会访问越界的数组);
  4. 符号引用验证:检查符号引用(比如类名、方法名)是否能找到对应的类/方法。

3. 准备(Preparation)

这一步是在方法区里为类的静态变量static修饰的变量)分配内存,并设置初始默认值

需要注意的是:不是代码里写的初始值,而是以下这些初始值:

数据类型
默认值
int
0
long
0L
float
0.0f
double
0.0d
boolean
false
引用类型
null

4. 解析(Resolution)

这一步是把常量池里的符号引用(比如类名com.example.User、方法名getUser),转换成直接引用(比如内存地址、指针)。

解析的话,主要针对的是类、接口、字段、方法等符号引用。

举个通俗一点的例子:

符号引用就是一个“代号”,比如你用“张三”指代一个人,不管他在哪,“张三”这个代号都能用;

直接引用就是“具体的地址”,比如张三现在在“XX小区XX号楼XX室”,你可以直接找到他。

5. 初始化(Initialization)

这是类加载的最后一步,也是真正执行程序员写的代码的阶段。

这一步只做一件事:执行类构造器<clinit>()方法

<clinit>()方法是编译器自动生成的,它会把类里的静态变量赋值语句静态代码块static {})按顺序合并起来执行。

举个例子

publicclassUser{staticint num = 10// 静态变量赋值static { // 静态代码块        num = 20;        System.out.println("静态代码块执行");    }}

以上代码在初始化阶段,JVM会执行<clinit>()方法:

先把num设为10,再设为20,然后打印“静态代码块执行”。

什么时候会触发类的初始化?

有以下6种情况:

  1. 创建类的实例(new User());
  2. 访问类的静态变量或静态方法(User.numUser.getUser());
  3. Class.forName("com.example.User")反射加载类;
  4. 初始化子类时,会先初始化父类;
  5. JVM启动时,会先初始化包含main()方法的主类;
  6. JDK 7+的动态语言支持,MethodHandle解析结果为REF_getStatic/REF_putStatic/REF_invokeStatic的句柄,且对应的类未初始化。

双亲委派机制(Parent Delegation Model)

什么是类加载器?

类加载器(ClassLoader)就是负责加载类的工具,JVM内置了三种类加载器:

  1. 启动类加载器(Bootstrap ClassLoader)

用C++写的,不是ClassLoader的子类,开发者无法直接获取它的引用。

负责加载JDK核心类库(比如java.lang.Objectjava.lang.String),这些类在jre/lib/rt.jar里。

  1. 扩展类加载器(Extension ClassLoader)

ClassLoader的子类,开发者可以获取它的引用,负责加载JDK扩展类库(比如jre/lib/ext目录下的类)。

  1. 应用程序类加载器(Application ClassLoader)

ClassLoader的子类,也是默认的类加载器。负责加载用户自己写的类(比如com.example.User),也就是classpath下的类。

什么是双亲委派机制?

这里的“双亲”不是指“父母”,而是指“父类加载器”,是翻译的问题,理解成“父委派机制”更准确一点。

双亲委派机制的核心规则是:“向上委托,向下加载”

  1. 当一个类加载器收到加载类的请求时,它不会自己先加载,而是把请求委托给父类加载器去加载;
  2. 如果父类加载器能加载这个类,就由父类加载器加载;
  3. 如果父类加载器加载不了(比如在它的加载范围内找不到这个类),才会向下交给子类加载器自己加载。

举个例子

比如说:加载java.lang.Object

  1. 应用程序类加载器收到加载Object的请求,它不自己加载,委托给扩展类加载器;
  2. 扩展类加载器也不自己加载,委托给启动类加载器;
  3. 启动类加载器在rt.jar里找到了Object类,直接加载它;
  4. 加载完成,返回Class对象,不会再向下交给子类加载器。

为什么需要双亲委派机制?

双亲委派机制的目的是保证Java程序的安全性和稳定性,主要解决了两个问题:

  1. 避免类的重复加载

如果没有双亲委派,用户自己写一个java.lang.Object类,应用程序类加载器就会加载它,导致JVM里有两个Object类,程序就乱了。

  1. 保证核心类库的安全

防止用户恶意篡改核心类库(比如写一个恶意的java.lang.String类),因为核心类库只会由启动类加载器加载,用户写的同名类不会被加载。

破坏双亲委派机制的情况

常见的破坏双亲委派机制的场景:

  1. JDBC的DriverManager

JDBC需要加载不同数据库的驱动(比如MySQL驱动),但驱动类是用户写的,启动类加载器加载不了,所以DriverManager用了线程上下文类加载器(Thread Context ClassLoader),打破了双亲委派,让启动类加载器能委托应用程序类加载器加载驱动。

  1. Tomcat的类加载器

Tomcat需要隔离不同Web应用的类(比如两个应用都用了不同版本的Spring),所以它有自己的类加载器结构,每个Web应用有独立的类加载器,优先加载自己的类,而不是向上委托。

抱歉,评论功能暂时关闭!