本文首发于微信公众号【程序员江湖】

作者黄小斜,斜杠青年,某985硕士,阿里 Java 研发工程师,于2018 年秋招拿到 BAT 头条、网易、滴滴等 8 个大厂 offer

个人擅长领域 :自学编程、技术校园招聘、软件工程考研(关注公众号后回复”资料“即可领取 3T 免费技术学习资源)


classloader顾名思义就是类加载器,负责将class加载到jvm中。

事实上,classloader除了能将class加载到jvm中,还有一个重要的作用就是会审查每个类应该由谁来加载,它是一种父优先的等级加载机。

classloader除了上述两个作用以外还有一个作用就是将class字节码重新解析成jvm统一要求的对象格式(说明class文件并不是加载进来就能用)

classloader类结构分析

classloader主要有几个方法

1 defineclass
defineclass主要负责将byte字节流解析成JVM能够识别的Class对象。比如通过网络获取字节流,然后转化为Class对象。

defineclass生成的Class对象还么有resolve,只会对象真正实例化时才会resolve(resolve是解析连接的过程,经过这一步Class对象才能真正被访问到)

2 findclass
defineclass和findclass方法通常是一起使用的,我们通过直接覆盖classloader父类的findclass方法来实现家在规则。

如果想在类加载到JVM时就被连接,那么亦可以在findclass中再次调用resolveclass方法来进行连接。

3 loadclass
loadclass按照双亲委派模型进行加载,加载不到才会调用findclass。
如果我们要在运行时加载一个类,直接调用this.getClass().getClassLoader().loadClass()方法就可以完成类加载了。

classloader是个抽象类,如果要实现自己的classloader,一般可以继承uriclassloader,因为这个类已经帮我们实现了大部分工作。

classloader的等级加载机制

整个jvm平台提供三层classloader

1 bootstrapclassloader
这个classloader只服务于JVM自身,主要加载JVM自身工作所需要的类,是JVM自己控制的,用户访问不到,这个类加载器也不遵守双亲委派模型,没有子类。

负责加载lib目录下面的类

2 extclassloader
extclassloader虽然是jvm的一部分,但是不是jvm实现的,它负责加载lib/ext下面的类

3 appclassloader专门为用户服务,父类是extclassloader。服务于classpath下的类加载。

如果我们要实现自己的类加载,最终都要把appclassloader作为父加载器。

事实上,bootstrapclassloader不是ext的父加载器,他也没有子类。

jvm加载class文件的两种方式:

1 隐式加载
是指jvm自动加载类的方式,不用显示调用classloader加载

2 显示加载
在代码中使用classloader加载。

如何加载class文件

classloader加载一个class文件到JVM时经过的过程

class文件 ——> findclass ——> class规范验证,准备,解析 ——> 类属性初始化赋值 ——> class对象

1 第一个阶段是把class文件把这个文件包含的字节码加载到内存

2 第二个阶段又可以分为三个步骤,分别是字节码验证、class类数据结构分析以及相应的内存分配和最后的符号表的链接。

3 第三个阶段是类中静态属性和初始化赋值,以及静态块的执行等。

加载字节码到内存(执行defineclass)

其实在抽象类classloader中并没有定义如何去加载

1 如何去找到指定类并且把它的字节码加载内存需要的子类中去实现,也就是要实现findclass方法。

2 我们来看一下子类urlclassloader是如何实现findclass的,在urlclassloader中通过一个urlclasspath帮助取的要加载的class文件字节流,而这个urlclasspath定义了到哪里去找这个class文件。

3 如果找到了这个class文件,再读取它的byte字节流,通过defineclass方法来创建类对象。

实际上,ext把默认的搜索路径指定为环境变量配置的lib/ext了。而appclassloader则指向classpath的环境变量。

验证与解析(执行verify,prepare和resolve)

1 字节码验证,类装入器对于类的字节码要做许多检测,以保证格式正确,行为正确。

2 类准备,在这个阶段准备代表每个类中定义的字段,方法和实现接口所必须的数据结构。

3 解析,在这个阶段装入器装入类所引用的其他所有类,可以用许多方式引用类,比如超类,接口,字段等。

初始化class对象(初始化类中的值)

在类中包含的静态初始化器都被执行,静态字段也被初始化为默认值。

常见加载类错误分析

classnotfoundexception

这个异常主要是因为找不到对应类的class字节码,说明classpath下没有指定文件存在。

noclassdefounderror

这个异常在用java命令行执行java类时可能会用到,因为指定类名时如果没写完整,就会出现这个问题,

unsatisfiedlinkerror

少见

classcastexception

类型转换失败,可能出现在不能转换的类型发生转换时。

exceptioninitializeerror

少见

常用的classloader分析

web应用中的一个servlet是如何加载类的,我们看一下它的classloader,发现层级关系是extclassloader -> appclassloader -> standardclassloader -> webappclassloader

tomcat在何时创建这些classloader:

1 standardclassloader在bootstrap类(tomcat的一个类)的initclassloaders方法中创建的。

2 standardclassloader创建成功时,会被设置为整个tomcat的根classloader

3 tomcat使用standardclassloader加载catalina并创建对象(catalina是tomcat的默认容器,catilina是tomcat以前的名字),整个tomcat容器的类加载器也是standardclassloader。

4 standardclassloader实际上并不处理加载逻辑,因为它是是appclassloader的代理,实际上的加载器还是appclassloader。

5 tomcat容器本身(catilina)是谁加载的不重要,我们要了解其中的servlet是谁加载的。

6 web.xml配置了每个servlet对应的类,所以应该是由类加载器显示加载的。(解析xml显示加载)

7 standardcontext(解析web.xml后映射的上下文实例)检查classloader是否存在,然后instancemanager这个类会去真正加载这些servlet实例。

8 instancemanager类使用的classloader是webappclassloader,它的处理和appclassloader有所不同。

1 首先检查webappclassloader是否已加载

2 如果没有,则检查在jvm中是否已加载,调用classloader的findloadedclass

3 如果前两个缓存没有,则用appclassloader加载

4 检查类是否在特殊的包名下,如果在的话可以用standardclassloader来加载

5 如果还没找到,则由webappclassloader来加载,回去web-inf/classes目录下查找,然后用defineclass方法生成Class对象。

如何实现自己的classloader

自定义classloader的使用场景

1 到自定义路径下查找指定class文件

2 获得网络传输的类的字节码,可能已加密

3 如果一个已加载的class文件被修改,可以重新加载这个类,实现热部署

加载自定义路径下的class文件

指定classpath即可

加载自定义格式的class文件

加密的class字节流,先解密再加载即可

实现类的热部署

jvm在加载类之前会判断请求的类是否已被加载,也就是通过findcloadedclass来判断。

1 看这个类的完整类名是否一致。

2 看加载这个类的类加载器是否是同一个实例。即使是同一个classloader类的两个实例,加载同一个类也会不一样。

3 所以实现热部署可以用classloader不同的实例对象来加载

如果重复地加载一个类,会抛出linkageerror错误。

Java应不应该加载动态类

Java有一个痛处,就是修改一个类,必须要重启一遍,才会重新加载,非常费时,如果使用动态的类加载就可以节省很多时间。

但是这是不好的,Java的优势正是基于共享对象的机制,达到信息的高度共享,对象一旦被创建就可以被重复利用。

如果动态加载一个对象到jvm,很难在jvm中平滑过渡。在理论上可以替换原对象然后修改所有指向该对象的引用,但是它违反了jvm的设计原则,。

对象的引用关系只有对象的创建者和持有和使用,jvm不可以干预对象的引用关系。

特例

对象不能被动态替换的原因是有很多引用指向它,很多变量保存着它的状态,如果对象不会被引用所指,没有人持有对象的状态,则可以动态地替换对象。

JSP就是这么做的,一旦JSP页面被修改,我们就可以热加载对应生成的servlet实例,这个实例是单例的,并且是无状态的,没有引用指向它,利用这个原理,很多热部署的方案也出现了。