面试题:类加载机制的原理
面试官考察点
考察目标:了解面试者对JVM的理解,属于面试八股文系列。
考察范围:工作3年以上。
技术背景知识
在回答这个问题之前,我们需要先了解一下什么是类加载机制?
类加载机制简述
什么是类加载机制?
简单来说:类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
经过类加载这个过程后,我们才能在程序中构建这个类的实例对象,并完成对象的方法调用和操作。
基本的工作原理下图所示。
文章配图我们编写的.java后缀的原始代码,通过JVM编译之后得到.class文件。
类加载机制,就是把.class文件加载到JVM中,我们知道JVM的运行时数据区又分为堆内存、虚拟机栈、元空间、本地方法栈、程序计数器等空间,当类被加载后,会根据JVM内存规则,把数据保存到对应区域内。
了解类加载器
大家想想,在实际开发中,运行一个程序,有哪些地方的类需要被加载?
从本地系统直接加载,如JRE、CLASSPATH。
通过网络下载.class文件
从zip,jar等归档文件中加载.class文件
从专有数据库中提取.class文件
将Java源文件动态编译为.class文件(服务器)
由于类加载器是负责这些和系统运行有关的所有类的加载行为,而针对不同位置的类,JVM提供了三种类加载器:
启动类加载器,BootStrapClassLoadr,最顶层的加载类,主要加载核心类库,也就是我们环境变量下面%JRE_HOME%\lib下的rt.jar、sourcs.jar、charsts.jar和class等,还可以通过启动jvm时指定-Xbootclasspath和路径来改变BootstrapClassLoadr的加载目录。扩展类加载器,ExtClassLoadr,加载目录%JRE_HOME%\lib\xt目录下的jar包和class文件。还可以加载-Djava.xt.dirs选项指定的目录应用类加载器,AppClassLoadr,也称为SystmAppClass。加载当前应用的classpath的所有类和jar包
从上述三个类加载器的描述来看,不同的加载器代表了不同的加载职能。当我们自己定义的一个类,要被加载到内存中时,类加载器的工作原理如下图所示。
文章配图从Java2开始,类加载过程采取了双亲委派模型(PantsDlgationModl),PDM更好的保证了Java平台的安全性。在该机制中,JVM自带的BootStrapClassLoadr是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。
PDM只是Java推荐的机制,并不是强制的。可以继承java.lang.ClassLoadr类,实现自己的类加载器。如果想保持PDM,就重写findClass(nam);如果想破坏PDM,就重写loadClass(nam)。JDBC使用线程上下文加载器打破了PDM,原因是JDBC只提供了接口,并没有提供实现。
类加载器的演示
通过下面这段代码演示一下类所使用的加载器。
publicclassClassLoadrExampl{publicstaticvoidmain(String[]args){ClassLoadrloadr=ClassLoadrExampl.class.gtClassLoadr();Systm.out.println(loadr);//cas1Systm.out.println(loadr.gtPant());//cas2Systm.out.println(loadr.gtPant().gtPant());//cas3}}
Cas1所示的代码,表示ClassLoadrExampl这个类是被那个类加载器加载的。Cas2所示的代码,表示ClassLoadrExampl的父加载器Cas2所示的代码,表示ClassLoadrExampl的祖父加载器
运行结果如下:
sun.misc.LaunchrAppClassLoadr
18b4aac2sun.misc.LaunchrExtClassLoadrf44null证明了,ClassLoadrExampl是被AppClassLoadr加载。
最后一个应该是Bootstrap类加载器,但是这里输出为null,原因是BootStrapClassLoadr是一个使用C/C++编写的类加载器,它已经嵌入到了JVM的内核之中。当JVM启动时,BootStrapClassLoadr也会随之启动并加载核心类库。当核心类库加载完成后,BootStrapClassLoadr会创建ExtClassLoadr和AppClassLoadr的实例,两个Java实现的类加载器将会加载自己负责路径下的类库,这个过程可以在sun.misc.Launchr中看到。
为什么要设计PDM
Java中为什么要采用PDM方式来实现类加载呢?有几个目的
防止内存中出现多份同样的字节码。如果没有PDM而是由各个类加载器自行加载的话,用户编写了一个java.lang.Objct的同名类并放在ClassPath中,多个类加载器都能加载这个类到内存中,系统中将会出现多个不同的Objct类,那么类之间的比较结果及类的唯一性将无法保证,同时,也会给虚拟机的安全带来隐患。双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。这样可以保证系统库优先加载,即便是自己重写,也总是使用Java系统提供的Systm,自己写的Systm类根本没有机会得到加载,从而保证安全性。
类的加载原理
一个类在加载过程中,到底做了什么?它的实现原理是什么呢?
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们的顺序如下图所示:
文章配图其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
每个阶段的所执行的工作,如下图所示。
文章配图下面详细分析一下类加载器在每个阶段的详细工作流程。
加载
”加载“是”类加机制”的第一个过程,在加载阶段,虚拟机主要完成三件事:
(1)通过一个类的全限定名来获取其定义的二进制字节流
(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
(3)在堆中生成一个代表这个类的Class对象,作为方法区中这些数据的访问入口。
验证
验证的主要作用就是确保被加载的类的正确性。也是连接阶段的第一步。说白了也就是我们加载好的.class文件不能对我们的虚拟机有危害,所以先检测验证一下。他主要是完成四个阶段的验证:
(1)文件格式的验证:验证.class文件字节流是否符合class文件的格式的规范,并且能够被当前版本的虚拟机处理。这里面主要对魔数、主版本号、常量池等等的校验(魔数、主版本号都是.class文件里面包含的数据信息、在这里可以不用理解)。
(2)元数据验证:主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求,比如说验证这个类是不是有父类,类中的字段方法是不是和父类冲突等等。
(3)字节码验证:这是整个验证过程最复杂的阶段,主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对数据类型做出验证后,这个阶段主要对类的方法做出分析,保证类的方法在运行时不会做出危害虚拟机安全的事。
(4)符号引用验证:它是验证的最后一个阶段,发生在虚拟机将符号引用转化为直接引用的时候。主要是对类自身以外的信息进行校验。目的是确保解析动作能够完成。
对整个类加载机制而言,验证阶段是一个很重要但是非必需的阶段,如果我们的代码能够确保没有问题,那么我们就没有必要去验证,毕竟验证需要花费一定的的时间。当然我们可以使用-Xvrfity:non来关闭大部分的验证。
准备
准备阶段主要为类变量分配内存并设置初始值。这些内存都在方法区分配。在这个阶段我们只需要注意两点就好了,也就是类变量和初始值两个关键词:
(1)类变量(static)会分配内存,但是实例变量不会,实例变量主要随着对象的实例化一块分配到java堆中,
(2)这里的初始值指的是数据类型默认值,而不是代码中被显示赋予的值。比如publicstaticintvalu=1;,在这里准备阶段过后的valu值为0,而不是1。赋值为1的动作在初始化阶段。
在上面valu是被static所修饰的准备阶段之后是0,但是如果同时被final和static修饰准备阶段之后就是1了。我们可以理解为staticfinal在编译器就将结果放入调用它的类的常量池中了。
解析
解析阶段主要是虚拟机将常量池中的符号引用转化为直接引用的过程。什么是符号应用和直接引用呢?
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好,就好比在班级中,老师可以用张三来代表你,也可以用你的学号来代表你,但无论任何方式这些都只是一个代号(符号),这个代号指向你(符号引用)直接引用:直接引用是可以指向目标的指针、相对偏移量或者是一个能直接或间接定位到目标的句柄。和虚拟机实现的内存有关,不同的虚拟机直接引用一般不同。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化
一个类在以下情况下,会被初始化。
创建类的实例,也就是nw一个对象
访问某个类或接口的静态变量,或者对该静态变量赋值
调用类的静态方法
反射(Class.forNam("