类的使用
-
任何一个类型在使用之前都必须经历过完整的加载、链接、初始化3个类加载步骤
-
一旦一个类型成功经历过这3个步骤之后,等着开发者使用了
-
开发人员可以在程序中访问和调用它的静态类成员信息(静态字段、静态方法),或者使用
new
关键字为其创建对象实例
卸载(Unloading)
类、类的加载器、类的实例之间的引用关系
-
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用
-
一个
Class对象
总是会引用它的类加载器,调用Class对象的getClassLoader
方法,就能获得它的类加载器 -
代表某个类的
Class实例
与其类的加载器
之间为双向关联
关系 -
一个
类的实例
总是引用
代表这个类的Class对象
。在Object类中定义了getClass
方法,这个方法返回代表对象所属类的Class对象的引用,此外,所有的java类都有一个静态属性class,它引用代表这个类的Class对象
类的生命周期
- Sample类被加载、链接和初始化后,它的生命周期就开始了
Sample
类的Class对象
不再被引用,即不可触及时,Class对象
就会结束生命周期Sample
类在方法区内的数据
也会被卸载,从而结束Sample
类的生命周期- 一个类何时结束生命周期,取决于代表它的
Class对象
何时结束生命周期
类的卸载
-
启动类加载器加载的类型
在整个运行期间
是不可能被卸载
的(JVM和JLS规范) -
系统类加载器
和扩展类加载器加载的类型
在运行期间不太可能被卸载
,因为系统类加载器实例或者扩展类的实例
基本上在整个运行期间总能直接或者间接的访问到
,其达到不可触及的可能性极小 -
被开发者
自定义类加载器
实例加载的类型只有在很简单的上下文环境中才能被卸载
,而且一般还要借助于强制调用虚拟机的垃圾收集功能
才可以做到。可以预想,稍微复杂点的应用场景中(很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的) -
一个
已经加载的类型被卸载的几率很小
,至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能
类加载器
概述
-
类加载器是JVM执行类加载机制的前提
-
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例
-
然后交给Java虚拟机进行链接、初始化等操作
-
ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变
类的链接和初始化行为
,至于它是否可以运行
,则由Execution Engine决定
题目
-
深入分析ClassLoader,双亲委派机制
-
类加载器的双亲委派模型是什么?双亲委派机制及使用原因
-
都有哪些类加载器,这些类加载器都加载哪些文件?
-
手写一个类加载器Demo
-
Class的forName(“java.lang.String”)和Class的getClassLoader()的Loadclass(“java.lang.String”)有什么区别?
-
什么是双亲委派模型?
-
类加载器有哪些?
-
双亲委派模型介绍一下
-
简单说说你了解的类加载器,讲一下双亲委派模型,以及其优点
-
什么是类加载器,类加载器有哪些?
-
类加载器的双亲委派模型是什么?
-
双亲委派机制可以打破吗?为什么
类加载器分类(JDK8)
类加载分类
class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式
显式加载
- 显式加载指的是在代码中通过
调用ClassLoader
加载class对象,如直接使用Class.forName(name)
或this.getClass().getClassLoader().loadClass()
加载class对象
隐式加载
- 隐式加载则是
不直接在代码中调用ClassLoader的方法加载class对象
,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中
在日常开发以上两种方式一般会混合使用
类加载器必要性
-
一般情况下,Java开发人员并不需要在程序中显式地使用类加载器
-
避免在开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时手足无措
-
只有了解类加载器的 加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题
-
需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了
-
开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑
命名空间
-
对于住意一个类,都需要由
加载它的类加载器
和这个类本身
一同确认其在Java虚拟机中的唯一性
-
每一个类加载器,都拥有一个独立的类名称空间:
-
比较两个类
是否相等,只有在这两个类是由同个类加载器
加载的前提下才有意义 -
即便这两个类源自
同一个Class文件
,被同一个虚拟机加载,只要加载他们的类加载器不同
,那这两个类就必不相等
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
- 在同一命名空间中,不会出现类的完整名字(包括包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括包名)相同的两个类
- 大型应用中,我们往往借助这一特性,来运行同一个类的不同版本
类加载机制的基本特征
-
双亲委派模型:但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如JDK内部的
ServiceProvider/ServiceLoader机制
,用户可以在标准API框架上,提供自己的实现,JDK也需要提供些默认的参考实现,例如,Java中JNDI、JDBC、文件系统、Cipher等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器 -
可见性:子类加载器可以访问父加载器加载的类型,但是反过来是不允许的,不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。
-
单一性:由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载,但是注意,类加载器“邻居”(如2个自定义加载器加载类)间,同一类型仍然可以被加载多次,因为互相并不可见
类加载器间的关系
- ExtClassLoader的Parent是null
- AppClassLoader的Parent是ExtClassLoader
- 当前线程的ClassLoader是AppClassLoader
parent不是Java语言的继承关系的父类
类加载器分类
-
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
-
自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,
-
Java虚拟机规范却没有这么定义,而是将
所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
-
除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器
-
不同类加载器是包含关系,在下层加载器中,包含着上层加载器的引用
引导类加载器(Bootstrap ClassLoader)
- 加载使用C/C++语言实现的,嵌套在JVM内部
- 用来加载Java的核心库(JAVAHOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
- 不继承自java.lang.ClassLoader,没有父加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器
扩展类加载器(ExtClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 继承于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库,如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
应用程序类加载器(系统类加载器,AppClassLoader)
-
java语言编写,由sun.misc.Launcher$AppClassLoader实现
-
继承于ClassLoader类
-
父类加载器为扩展类加载器
-
它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库
-
用户自定义类加载器的默认父加载器
-
通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
用户自定义类加载器
- 自定义类加载器通常需要继承于ClassLoader
- Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式
- 体现Java语言强大生命力和巨大魅力的关键因素之一,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源
- 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例举不胜举
- 著名的OSGI组件框架,再如Eclipse的插件机制,类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现
- 自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块,这种机制比C/C程序要好太多,想不修改C/C程序就能为其新增功能几乎是不可能的
测试不同类加载器
- 每个
Class对象
都会包含一个定义它的ClassLoader的一个引用
获取ClassLoader的途径
获得当前类的ClassLoader
clazz.getClassLoader()
获得当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
获得系统的ClassLoader
ClassLoader.getSystemClassLoader()
- 站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器
- 引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。
- 由于引导类加载器压根儿就不是一个Java类,因此在Java程序中只能打印出空值
- 数组类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的
- 对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的
- 如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的
ClassLoader类
主要方法
Class<?> loadClass(String name)
:加载名称为name的类,返回结果为java.lang.Class类的实例,如果找不到类,则返回 ClassNotFoundException异常,该方法中的逻辑就是双亲委派模式的实现
Class<?> findClass(String name)
:查找二进制名称为name的类,返回结果为java.lang.Class类的实例,这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用
- 在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写
loadClass方法
,从而实现自定义的类加载类 - 但是在JDK1.2之后已
不再建议用户去覆盖loadClass方法
,而是建议把自定义的类加载逻辑写在findClass方法
中,从前面的分析可知 findClass
方法是在loadClass
方法中被调用的,当loadClass
方法中父加载器加载失败后,则会调用自己的findClass
方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。- 需要注意的是ClassLoader类中并没有实现
findClass
方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法
通常是和defineClass
方法一起使用的 - 一般情况下,在
自定义类加载器
时,会直接覆盖ClassLoade
r的findClass方法
并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass
方法生成类的Class对象 Class<?> defineClass(String name, byte[] b,int off,int len)
- 根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的,这是受保护的方法,只有在自定义ClassLoader子类中可以使用
defineClass
方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码
,然后转换为byte字节流创建对应的Class对象defineClass
方法通常与findClass
方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass
方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass
方法生成对象
SecureClassLoader与URLClassLoader
-
SecureClassLoader扩展了ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联
-
前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如
findClass()
、findResource()
等,而URLClassLoader
这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能 -
在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承
URLClassLoader类
,这样就可以避免自己去编写findClass方法
及其获取字节码流的方式
,使自定义类加载器编写更加简洁
ExtClassLoader与AppClassLoader
-
ExtClassLoader、AppClassLoader
都继承自URLClassLoader
,是sun.misc.Launcher的静态内部类
-
sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的
-
ExtClassLoader
并没有重写loadClass
方法,这足矣说明其遵循双亲委派模式
,而AppClassLoader重载了loadClass()方法,但最终调用的还是父类loadClass方法
,因此依然遵守双亲委派模式
Class.forName()与ClassLoader.loadClass()
Class.forName()
:是一个静态方法,最常用的是Class.forName(String className);
- 根据传入的类的全限定名返回一个Class对象,该方法在将Class文件加载到内存的同时,会执行
类的初始化
- ClassLoader.loadClass()
ClassLoader.loadClass()
:这是一个实例方法,需要一个ClassLoader对象
来调用该方法,该方法将Class文件加载到内存时,并不会执行类的初始化
,直到这个类第一次使用时才进行初始化- 该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器