Java class loader
类加载器的基本概念
类加载器(class loader)用来加载加载Java类到Java虚拟机中,一般来说,Java虚拟机使用Java类的方式如下:Java源程序(.java文件)在经过Java编译器编译后被转换成Java字节代码(.class文件)。类加载器负责读取Java字节代码,并转换成java.lang.Class类的一个实例。每一个这样的事例都可以表示一个Java类。通过此实例的newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如Java字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
classloader的加载过程
类从被加载到虚拟机内存到被卸载,整个完成的生命周期包括:类加载、验证、准备、解析、初始化、使用、卸载七个阶段。
其中验证、准备、解析三个部分统称为连接。
加载
指把class字节码文件从各个来源通过类加载器装载入内存中
字节码的来源:一般的加载来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络以及动态代理实时编译
在加载阶段,JVM需要完成三件事情
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法去的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
找到需要加载的类并把类信息放到JVM的方法区中,然后在堆区中实例化java.lang.Class对象,作为方法区这个类的信息的入口。
验证
保证加载进来的字节流符合虚拟机规范,不会造成安全错误
- 文件格式验证,文件中是否有不规范或者附加的其他信息,例如常量中是否有不被支持的常量
- 元数据的验证,保证其描述的信息符合Java语言规范的要去。例如类是否有父类,是否继承了不被允许的final类等
- 字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性
- 符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类,校验符号引用中的访问性(private,public等)是否可以被当前类访问等
准备
主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值。
注意: 初值并不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值,比如int类型的初值为0,reference为null等
解析
将常量池内的符号引用替换为直接引用的过程。
比如:现在调用hello()方法,这个方法的地址是1234567,那么hello就是符号引用,123456就是直接引用。
在解析阶段,虚拟机会把所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用
初始化
对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)
-
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始 化,则需要先触发其初始化阶段。能够生存着四条指令的典型Java代码场景有
- 使用new关键字实例化对象的时候
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类型的静态方法的时候
-
使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。
-
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
-
如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStaticREF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
-
当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
虽然classloader的加载过程有复杂的5步,但事实上除了加载之外的四部,其它都是由JVM虚拟机控制的,我们除了适应它的规范进行开发外,能干预的空间并不多。加载则是我们控制classloader实现特殊目的最重要的手段
classloader双亲委托机制
双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码的。
站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader)马哥分类加载器是由C++语言实现的,是虚拟机的一部分;另一部分是其他所有的类加载器,这些类加载器是由Java语言实现,独立存在于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader(针对HotSpot,其他有些虚拟机,整个虚拟机本身都是由Java编写的)
classloader的双亲委托机制是指多个类加载器之间存在父子关系的时候,某个class类具体由哪个加载器进行加载的问题。其具体的过程表现为:当一个类加载的过程中,它首先不会去加载,而是委托给自己的父类去加载,父类又委托给自己的父类,因此所有的类加载都会委托给顶层的负累,即Bootstrap Classloader进行加载,然后弗雷自己无法完成这个加载请求,子加载器才会尝试自己去进行加载。使用双亲委派模型,Java类随着它的加载器一起具备了一种带有优先级的层次关系,通过这种层次模型,可以避免类的重复加载,也可以避免和心累被不同的类加载器加载到不同的内存中造成冲突和混乱,从而保证了Java核心库的安全。
启动类加载器
Bootstrap Classloader负责将<JAVA_HOME>/lib目录下被虚拟机识别的类库加载到虚拟机内存中。我们常用的基础类,例如java.util.,java.io.,java.lang.**等等都是由根加载器加载
扩展类加载器
Extention Classloader负责加载JVM扩展类,比如swing系列,内置的js引擎、xml解析器等
应用程序加载器
Application Classloader也叫系统类加载器,他负责加载用户路径上的指定的类库。我们自己编写的代码以及使用的第三方jar包都是由它来加载的
自定义加载器
Custom Classloader通过我们为了某些特殊的实现的自定义加载器
classloader的应用场景
类加载器可以解决:类冲突问题、实现热加载以及热部署,甚至可以保护jar包的加密保护
java.lang.ClassLoader类介绍
该类的基本职责就是根据一个指定的类的名称,找到或生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即java.lang.Class类的一个实例。此外,ClassLoader还负责加载Java应用所需要的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。
ClassLoader 中与加载类相关的方法
| 方法 | 说明 |
|---|---|
getParent() | 返回该类加载器的父类加载器。 |
loadClass(String name) | 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findClass(String name) | 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findLoadedClass(String name) | 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
破坏双亲委托机制
双亲委派模型主要出现过3次较大规模“被破坏”的情况
-
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2面世以前的“远 古”时代。由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类 java.lang.ClassLoader则在Java的第一个版本中就已经在,面对已经存在的用户自定义类加载器的代 码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术 手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的 protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass()中编写代码。
-
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的。当基础类型需要其他服务的接口时,就会破坏该机制。例如JNDI、 JDBC、JCE、JAXB和JBI等
解决方法是:线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方 法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
-
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)
OSGi实现模块化部署的关键是它自定义的类加载机制的实现,每个程序模块(OSGi中称为Bundile)都有一给自己的类加载器,当需要更换一个Bundile时,就把Bundle连同类加载器一起换掉实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模式推荐的树状结构,而是进一步发展为复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜素
- 将以Java.*开头的类,委派给父类加载器加载
- 否则,将委派列表名单内的类,委派给父类加载器加载。
- 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
- 否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的。
参考/资料来源:《深入理解Java虚拟机》