一、类加载的时机
1. 类加载的 生命周期 :加载(Loading)-->验证(Verification)-->准备(Preparation)-->解析(Resolution)-->初始化(Initialization)-->使用(Using)-->卸载(Unloading)
2. 加载:这由虚拟机自行决定。
3. 初始化阶段:
a) 遇到new、getstatic、putstatic、invokestatic这4个字节码指令时,如果类没有进行过初始化,出发初始化操作。
b) 使用java.lang.reflect包的方法对类进行反射调用时。
c) 当初始化一个类的时候,如果发现其父类还没有执行初始化则进行初始化。
d) 虚拟机启动时用户需要指定一个需要执行的主类,虚拟机首先初始化这个主类。
注意:接口与类的初始化规则在第三点不同,接口不要其所有的父接口都进行初始化。
二、类加载的过程
1. 加载
a) 加载阶段的工作
i. 通过一个类的全限定名来获取定义此类的二进制字节流。
ii. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
iii. 在java堆中生成一个代表这个类的java.lang.Class对象,做为方法区这些数据的访问入口。
b) 加载阶段完成之后二进制字节流就按照虚拟机所需的格式存储在方区去中。
2.验证
这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。
a) 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
b) 元数据验证:对字节码描述的信息进行语义分析,以确保其描述的信息符合java语言规范的要求。
c) 字节码验证:这个阶段的主要工作是进行数据流和控制流的分析。任务是确保被验证类的方法在运行时不会做出危害虚拟机安全的行为。
d) 符号引用验证:这一阶段发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),主要是对类自身以外的信息进行匹配性的校验。目的是确保解析动作能够正常执行。
3. 准备
准备阶段是正式为变量分配内存并设置初始值,这些内存都将在方法区中进行分配,这里的变量仅包括类标量不包括实例变量。
4. 解析
解析是虚拟机将常量池的符号引用替换为直接引用的过程。
a) 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
b) 直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。直接饮用是与内存布局相关的。
c) 类或接口的解析
d) 字段的解析
e) 类方法解析
f) 接口方法解析
5. 初始化
是根据程序员制定的主观计划区初始化变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
三、双亲委派模型
从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:
1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\ext目录中的类库,但是貌似在JDK的安装目录下,没看到该指定的目录。该加载器可以被开发者直接使用。
3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这三类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。这些类加载器之间的关系如下图所示:
如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。
在rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程的代码,具体源码如下:
protected synchronized Class<?> loadClass(String name, boolean resolve)通过上面代码可以看出,双亲委派模型是通过loadClass()方法来实现的,根据代码以及代码中的注释可以很清楚地了解整个过程其实非常简单:先检查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载。
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
四、自定义类加载器的一般步骤
Java的类加载器自从JDK1.2开始便引入了一条机制叫做父类委托机制。一个类需要被加载的时候,JVM先会调用他的父类加载器进行加载,父类调用父类的父类,一直到*类加载器。如果父类加载器加载不了,依次再使用其子类进行加载。当然这类所说的父类加载器,不一定他们之间是继承的关系,有可能仅仅是包装的关系。
Java之所以出现这条机制,因为是处于安全性考虑。害怕用户自己定义class文件然后自己写一个类加载器来加载原本应该是JVM自己加载的类。这样会是JVM虚拟机混乱或者说会影响到用户的安全。下面我们来自己实现一个类加载器,其中主要就是继承ClassLoader类。我们有必要明白:
虽然在绝大多数情况下系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输 Java 类的字节代码,为了保证安全性,这些字节码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在 Java 虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。
①ClassLoader加载类的顺序
1调用findLoadedClass(String) 来检查是否已经加载类
2在父类加载器上调用loadClass方法。如果父亲不能加载,一次一级一级传给子类
3调用子类findClass(String) 方法查找类。若还加载不了就返回ClassNotFoundException,不交给发起请求的加载器的子加载器
②实现自己的类加载器
1 获取类的class文件的字节数组,如loadClassData方法
2 将字节数组转换为Class类的实例,重写findClass中调用的defineClass方法
一般来说自己开发的类加载器只需要覆写findClass(String name)
方法即可。java.lang.ClassLoader
类的方法loadClass()
封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()
方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()
方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()
方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写loadClass()
方法,而是覆写findClass()
方法。
由于只重写了findClass方法并没有重写loadClass方法,故没有改变父类委托机制。也就数说如果某个.class可以被父类加载,我们自定义的类加载器就不会被执行了。比如Animal.java被自动编译为Animal.class放在bin目录下,AppClassLoader完全可以加载,所以就不调用自定义的加载器了。
尝试办法1:把Animal.class放在别的目录中比如D盘的根目录下
报 Class A can not access a member of class B with modifiers ""错。Java语言中的包访问成员实际上指的是运行时包访问可见,而不是编译时。因此当你试图访问不在同一个runtime package的成员时,即便在编译时它们在同一个包内,但是却由不同的class loader加载,也同样会得到java.lang.IllegalAccessException: Class A can not access a member of class B with modifiers "" 这样的异常。
尝试办法2:把该Animal.class的后缀名为.myClass,让AppClassLoader找不到
网上有人说可以解决,但是我实验的结果是会和办法1报一样的异常。
尝试办法3:解决方案是通过扩展自定义的ClassLoader,重写loadClass方法先从当前类加载器加载再从父类加载器加载。