本文已参与「新人创作礼」活动,一起开启掘金创作之路。
行到水穷处,坐看云起时
类加载过程也是面试中比较常见的问题,也是需要JVM的重点部分,你值得拥有~
在冯·诺依曼定义的计算机模型中,任何程序都需要加载到内存才能跟CPU进行交流。
诺,就是这位大佬。
字节码.class文件同样也需要先加载到内存中,才可以实例化类。
ClassLoader的使命就是提前加载.class文件到内存中,在加载时,使用的是双亲委派模型(Parents Delegation Model),双亲委派模型在接下来会讲到,莫急莫急~
首先呢,说一下这个Java的类加载器(ClassLoader),了解这个之前我们先要知道JVM的组成部分:
JVM主要部分 类加载器(ClassLoader)+ 执行引擎(excution engine)+ 运行时数据区
所以说呢,类加载器也是JVM中的一份子,不是什么特殊的东西,别紧张…
这样咱们就有了一个清晰的了解,简单的说,类加载器负责提前加载.class文件到内存中,执行引擎负责将字节码转化为对应平台上可运行的机器码。
类加载详细过程
来了,重点来了…知识点懂不懂~ 快拿小本本记好
许多面试官在问你这个问题的时候,当你回答出下面三个过程是基本过关(没有luan用)的,所以,你需要了解的更详细一点,起码在这个问题你得拖个三分钟吧。
别急,我们来看看如何慢条斯理有逻辑的拖个三分钟。类加载器将类加载到内存中的过程主要有三个阶段,分别是加载(Load)、链接(Link)、初始化(Init)。
- 加载(Load)阶段:读取类文件产生二进制流,并转化为特定的数据结构(这里我理解为转化为JVM需要的数据结构,如方法区运行时数据结构),初步校验cafebabe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的实例。
- 链接(Link)阶段:这个阶段分为三个部分,分别是验证、准备、解析
验证:(详细校验)比Load阶段更为详细的校验,如final是否合规、类型是否正确、静态变量是否合理等。
准备:(分配内存设默认值)为静态变量分配内存,并设定默认值。
解析:(解析确保引用正确)解析类和方法,确保类与类之间的相互引用的正确性,完成内存结构布局。 - 初始化(Init)阶段:(赋值)执行类构造器(接下来会解释)中的< clinit>方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。
好,到这里差不多两分钟过去了,那么剩下一分钟呢?那就是可能刚刚在上文大家就存在疑惑的:类构造器方法< clinit>。
面试官大大,别急别急,让我来聊一聊我了解的(告诉你)什么是类构造器方法< clinit>,与它容易混淆的是实例构造器方法< init>,接下来我们就来简单说一下两者的区别。
类构造器方法< clinit> :JVM第一次加载类的时候在初始化阶段调用,类级别的,只加载一次,编译过程中编译器会自动收集类中所有的类静态变量和静态语句,从而生成类构造器方法< clinit>。
实例构造器方法< init> :在实例创建出来的时候调用,包括new操作符、反射创建实例对象(newInstance())、clone()方法等。
这里关于类加载中需要注意的是:全小写的class是关键字,用来定义类,而首字母大写的Class,它是所有class的类。
这句话或许有些抽象,也就是说我们通常定义的类,它属于Class类,这也就是为什么说类加载过程是将一个.class字节码文件实例化为一个Class对象并进行初始化的过程。为了大家更清晰的了解,我贴上一段代码:
public class ClassTest {
private static int[] array = new int[3];
private static int length = array.length;
//任何小写的class定义的类都有一个魔法属性:class,来获取此类的大写class类对象
private static Class<One> one = One.class;
public static void main(String[] args) throws Exception{
//通过newInstance()方法创建One的对象
One oneObject = one.newInstance();
oneObject.call();
//通过one这个大写的Class对象来获取私有成员属性对象Field
Field privateFieldInOne = one.getDeclaredField("inner");
//设置私有对象可以访问和修改
privateFieldInOne.setAccessible(true);
privateFieldInOne.set(oneObject,"hello world!");
//成功修改私有属性inner的值
System.out.println(oneObject.getInner());
}
class One{
private String inner = "innerInit";
public void call(){
System.out.println("OneCall");
}
public String getInner() {
return inner;
}
}
}
运行的结果大概是这样:
怎么样,看到这里,应该懂了这个类中之王Class了吧。
那么回到类加载中,类加载器是如何定位到具体的类文件并且读取的呢?
类加载器呢,就类似于一个原始的部落,存在权利制度:
是的是的,就是等级制度…
最高一层的是威望最高的Bootstrap,在JVM启动时创建,是最根基的类加载器,负责装载最核心的Java类,如Object、System、String等
第二层是在jdk9中称为Platform ClassLoader,平台类加载器,用以加载一些扩展的系统类,比如XML、加密、压缩相关的功能类等,jdk9之前的加载器是Extension ClassLoader
第三层是Application ClassLoader 应用类加载器,主要加载的是用户自定义的CLASSPATH路径下的类。
第二、三层类加载器为Java语言实现,第一层的类加载器Bootstrap是通过C/C++实现的,并不存在与Java体系内,所以下面的代码打印出来的c2为null。类加载器具有严格的等级制度,但并非是继承关系。
查看类加载器的方法:
//正在使用的类加载器
ClassLoader c = TestDemo.class.getClassLoader();
//该加载器的上层加载器(也就是平台类加载器)
ClassLoader c1 = c.getParent();
//该加载器的上层加载器(Bootstrap加载器,因为不在Java体系内,所以输出为null)
ClassLoader c2 = c.getParent();
那接下来就是类加载中的重点啦 霍霍~
双亲委派模型
先来张图,呃,没啥美术功底,大家将就一下…
那具体流程是怎么样的呢,怎么向上询问,怎么向下加载呢?
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个位置类的话,要非常礼貌的向上询问:“师傅(爸爸),请问这个类已经被加载过了吗?”
被询问的高层次类加载器听到这个问题后,首先会思考一个问题:“我是不是已经加载过这个类了?”。
如果已经加载过这个类,就回复下层:“乖徒儿(儿子),师傅(爸爸)已经加载过这个类了,你不用加载了。”。
如果没有加载过这个类,那么他就会判断他有没有师傅(爸爸)。如果有,就还会非常礼貌的向上询问:“师傅(爸爸),请问这个类已经被加载过了吗?”,如果没有,就表示他已经是最高层的类加载器了,就会判断自己是否能够加载这个类,如果可以就加载,如果不可以,就严肃的向下层命令:“乖徒儿(儿子),师傅(爸爸)年迈了,加载不了这个类了,这点小事你去做就好了。”,于是就让下层去加载这个类。那如果都加载不了,则通知发起加载请求的当前类加载器,准予加载。
顺便提一句,Bootstrap加载的路径可以追加,但不建议修改或者删除原有的加载路径。如果需要追加,则可以在JVM启动参数中加入相关的启动参数即可。
那我为什么要用双亲委派模型呢?有什么好处呢?
这里应该很好理解,如果自己定义了一个java.lang.String类,那如果没有双亲委派模型,那岂不是String类的所有方法都不能用了?只能用我自己定义的String类?那不玩儿完了?我可是个弟弟…所以,这里是双亲委派模式的第一个优点:避免核心类被篡改。还有一个优点就是可以避免重复加载,上层类加载器已经加载类,下层加载器就不用再去加载了。
自定义类加载器
这里有人就要说了,我就觉得自己牛B,我就要自己重写一个java.lang.String类,不行吗?
行!当然可以了,双亲委派模型并非是强制模型,用户可以自定义类加载器,那首先说一下在什么情况下需要自定义类加载器呢?
- 隔离加载类:在某些框架内进行中间件与应用模块的隔离,把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。
- 修改类加载方式:除了Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
- 扩展加载源:比如从数据库,网络甚至是电视机机顶盒进行加载。
- 防止源码泄露:Java代码容易编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。
那该如何自定义一个类加载器呢?
实现一个自定义类加载器的步骤:继承ClassLoader,重写findClass()方法,调用defineClass()方法。如下代码实现一个简单的类加载器:
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException{
try {
byte[] result = getClassFromCunstomPath(name);
if(result == null){
throw new FileNotFoundException();
}else{
return defineClass(name,result,0,result.length);
}
}catch (Exception e){
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCunstomPath(String name){
//从自定义路径中加载指定类
}
}
由于中间件一般都有自己的依赖jar包,在同一个工程内引用多个框架时,往往会被迫进行类的仲裁。
按照某种规则jar包的版本会被统一制定,所以这样就会引起类冲突,导致应用程序出现异常,主流的容器类都会自定义类加载器,实现不同中间件之间的隔离,有效避免了类冲突。
关于JVM的类加载机制暂时就写这么多啦
后记
同时也希望大家可以关注我的公众号【类似程序员】,会定期与大家分享我的一些想法与学习感悟,谢谢各位!