高级Java面试:谈谈你对类加载机制的理解

文章绝大部分内容都是可以从周志明老师的《深入理解JVM虚拟机》一书获得,有需要深入了解各方面细节的读者可以自行前往阅读。
本文致力于用口语化的形式回答标题所提到的问题,做一个面试题目的参考答案为广大求职者提供一些微薄之力。

一句话总结答案

Java虚拟机将描述类数据的class文件加载到虚拟机中,并对数据进行检验、转换解析和初始化,最终形成虚拟机可以直接使用的类型。这个过程被称为虚拟机的类加载机制。

详细描述

一个类在虚拟机完整的生命周期分为加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证、准备和解析统称为连接。加载、验证、准备、初始化和卸载这五个阶段顺序是固定的,他们会按部就班的开始(按部就班的开始而不是进行或者完成,这是因为这些阶段是相互交叉混合的进行的,会在一个阶段执行过程中激活、调用另一个阶段,具体会在后面讲解加载阶段的时候聊一聊)。而解析阶段为了支持Java 运行时绑定的特性,某些情况下会在初始化后执行。(准备一下什么是运行时绑定?)

初始化的时机

虚拟机没有规定什么时候执行类加载过程的第一个阶段“加载”,但是对初始化的时机有明确的规定

概括的说就是主动引用时会立刻触发类的初始化

  1. new一个对象时
  2. 访问一个类的静态方法时
  3. 访问一个类的静态属性时。(如果通过子类访问父类的静态属性,虚拟机规范规定了只有定义静态属性的类才必须初始化父类,而子类是否需要初始化没有做出明确规定,交由具体的虚拟机实现决定hotSpot会初始化子类)。
  4. 通过反射访问这个类时
  5. 初始化一个类时,如果他的父类没有初始化会先初始化他的父类(引出经典题目,父子类中的属性和代码块的加载顺序)

不会触发初始化的情况有

  1. 定义一个数组类型,如 DemoClass[] demols = new DemoClass(),因为针对数组类型,虚拟机会自动创建一个包装类,并对包装类初始化而不是该类
  2. A类中访问B类的常量(final static修饰的属性)时,B类不会被初始化。因为编译阶段的常量传播优化会将B类的常量加入到A的常量池中,所以B类的常量并没有被访问,B类也就不会初始化。
加载

类加载第一个阶段,“加载”,也是开发人员可以自定义的一个阶段,主要完成一下三件事

  1. 通过类的全限定名找到定义此类的二进制字节流
  2. 将这个字节流的静态存储结构转换成方法区中的运行时数据结构
  3. 生成对应的Java.lang.class对象,作为该类在方法区的访问入口

加载阶段可以使用默认的引导类加载器进行类加载,也可以使用自定义的类加载器,重写findClass和loadClass方法,根据自己想法的赋予程序获取运行时代码的动态性。典型的例子就是破坏双亲委派模型。

还有一点就是一个类是与类加载器结合在一起确定唯一性的。如果有一个A类分别通过两个类加载器加载等到两个对象,那么这两个类的instanceOf等都会返回false。

验证

接下来是介绍一下验证阶段,主要任务就是

  1. 验证是否魔数开头,版本信息。(读取的是字节流,通过以后字节流才会进入到方法区中,后面几个阶段都是直接访问方法区)
  2. 检查是否继承了Object,父类是否为final,是否实现了父类的抽象方法,或者接口的所有方法,不符合要求的方法重载(如方法签名一样,但是返回类型不一样)
  3. 验证方法体中的代码是否符合规范,如类型转换是否正确,是否非法调用不可以访问的方法
  4. 符号引用验证:检查符号引用是否正确,这个阶段出现异常会抛出方法没有找到异常,属性没有找到异常,非法访问异常
准备

然后是准备阶段的主要作用,准备类的静态属性的零值。
比如类中声明了static int i = 123在准备阶段i会等于0,如果声明为静态常量(fianl)该阶段会赋予声明的值,如i = 123

解析

解析阶段将符号引用转变成直接引用,

初始化

最后介绍初始化阶段做的几件事

  1. 初始化的时候会收集类变量和静态代码块生成方法,并且虚拟机会保证父类的方法先执行,如果类中没有静态属性和静态方法就不会生成方法。接口不会先执行接口的方法
  2. 虚拟机保证了一个类的方法在多线程环境下被正确的加锁同步,所以要避免在静态代码块中执行耗时长的方法
类加载器

聊完类的生命周期,再来讲一下类加载器

Java虚拟机团队有意将“通过类的全限定名获取描述该类的二进制字节流”的动作放到虚拟机外部实现,以便让应用系统决定如何去获取所需要的类,而实现这个动作的代码实现就是“类加载器”

Java中几个默认的类加载器

  1. 启动类加载器bootstrap classload,负责加载Java_home/lib下的类,通过名称识别,即使程序员把自己编写的类放到这个目录下,引导类加载器也不会加载
  2. 扩展类加载器,负责加载扩展路径(java_home/lib/ext)下的类
  3. 应用程序类加载器,负责加载classpath路径下的代码,如果没有自定义类加载器,一般来说这个就是默认类加载器

Java中默认的使用双亲委派模型将这几个类加载器组合一起使用。

双亲委派模型

什么是双亲委派模型呢?双亲委派模型要求除了启动类加载器之外所有的类加载器都有父类加载器,这里的父类是指通过组合的形式复用代码实现。然后在一个类加载器收到加载请求的时候,首先是将请求委派给它父类加载器来看看能不能完成这个加载请求,如果无法搜索范围内完成加载才会让子类加载器尝试加载。这样就保证所有的类都是首先尝使用启动类加载器加载。
这样做的好处有几点,第一就是保证类的优先级,比如自己在classpath中写了一个java.lang.object类的话,因为双亲委派模型,所有类都尝试使用启动类来加载,所以这个自定义的Object就不会加载运行,从而保证了Java 最基础的行为安全。

破坏双亲委派模型

但是不是所有代码都适用双亲委派模型的,比如JDBC破坏双亲委派模型的案例(举例讲一个,最好都了解,因为面试官对你的举例不了解的话,会问你其他的。
Serviceload、Tomcat、Osgi热部署、线程上下文加载器)

这里举例JDBC来说明使用线程上下文类加载器的场景

JDBC基本原理是利用Java提供的SPI技术,将具体实现类全限定名放入到META-INFO/services中以Driver命名的文件中。
当代码首次通过DriverManage获取数据库连接的时候,会加载该类并执行它静态代码块中的初始化代码,主要做了两件事情,一是解析services中的文件获取类名,二是通过ClassLoad.forName加载实现类后进行初始化。
需要使用线程上下文类加载器的场景就出现在第二步,因为Driver位于Java_Home/jar中,所以使用是启动类加载器,而根据Java规定,加载依赖类的时候如果没有指定类加载器时会用默认使用当前类的加载器。具体的实现类是在用户代码classpath当中,启动类加载器无法加载。根据双亲委派模型,上层无法直接委派下层进行加载。为了解决这个问题开发团队引入了线程上下文文类加载器。
这个类加载器可以通过java.lang.Thread类的setContextClassLoader指定,如果没有指定会继承父线程的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值