类加载器用来把类加载到Java虚拟机中。从JDK 1.2版本开始,类的加载过程采用父亲委托机制,这种委托机制能更好地保证Java平台的安全性。
一、 ClassLoader的概念
ClassLoader是一个抽象类,我们用它的实例对象来装载类,它负责将Java字节码装载到JVM 中,并使其成为JVM 一部分。JVM 的类动态装载技术能够在运行时刻动态地加载或者替换系统的某些功能模块,而不影响系统其他功能模块的正常运行。
二、 Java虚拟机自带的几种加载器简介
1.Bootstrap ClassLoader/启动类加载器 :主要负责jdk_home/lib目录下的核心api 或-Xbootclasspath 选项指定的jar包装入工作。
2.Extension ClassLoader/扩展类加载器 :主要负责jdk_home/lib/ext目录下的jar包或-Djava.ext.dirs 指定目录下的jar包装入工作。
3.System ClassLoader/系统类加载器 :主要负责java-classpath/-Djava.class.path所指的目录下的类与jar包装入工作。
4.User Custom ClassLoader/用户自定义类加载器(java.lang.ClassLoader的子类) :在程序运行期间,通过java.lang.ClassLoader的子类动态加载class文件,体现java动态实时类装入特性。
其中BootstrapClassLoader是JVM级别的,由C++撰写;ExtensionClassLoader、AppClassLoader都是java类,都继承自URLClassLoader超类。BootstrapClassLoader由JVM启动,然后初始化sun.misc.Launcher,sun.misc.Launcher初始化ExtensionClassLoader、AppClassLoader。
Bootstrap ClassLoader、ExtensionClassLoader、AppClassLoader三者的关系如下:
Bootstrap ClassLoader是ExtensionClassLoader的parent,ExtensionClassLoader是AppClassLoader的parent。但是这并不是继承关系,只是语义上的定义,基本上,每一个ClassLoader实现,都有一个ParentClassLoader。可以通过ClassLoader的getParent方法得到当前ClassLoader的parent。BootstrapClassLoader比较特殊,因为它不是javaclass所以ExtensionClassLoader的getParent方法返回的是NULL。
类加载器的特性有两点:
1.每个ClassLoader都维护了一份自己的名称空间,同一个名称空间里不能出现两个同名的类。
2.为了实现java安全沙箱模型顶层的类加载器安全机制,java默认采用了" 双亲委派的加载链" 结构。
三、 Java类加载器classLoader的工作机制
类加载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件。在Java中,类转载器把一个类装入JVM中,需要经过以下步骤:
1.装载:查找和导入Class文件;
2.链接:执行校验、准备和解析步骤,其中解析步骤是可以选择的:
a)校验:检查载入Class文件数据的正确性;
b)准备:给类的静态变量分配存储空间;
c)解析:将符号引用变成直接引用;
3.初始化:对类的静态变量、静态代码块进行初始化工作。
ClassLoader被组织成树形,一般的工作原理是:
1线程需要用到某个类,于是contextClassLoader被请求来载入该类
2contextClassLoader请求它的父ClassLoader来完成该载入请求
3 如果父ClassLoader无法载入类,则contextClassLoader试图自己来载入。
四、 ClassLoader的工作原理分析
1. 预先加载与依需求加载
Java 运行环境为了优化系统,提高程序的执行速度,在 JRE 运行的开始会将 Java 运行所需要的基本类采用预先加载( pre-loading )的方法全部加载要内存当中,因为这些单元在 Java 程序运行的过程当中经常要使用的,主要包括 JRE 的 rt.jar文件里面所有的 .class 文件。
当java.exe 虚拟机开始运行以后,它会找到安装在机器上的 JRE 环境,然后把控制权交给JRE , JRE 的类加载器会将 lib 目录下的 rt.jar基础类别文件库加载进内存,这些文件是 Java 程序执行所必须的,所以系统在开始就将这些文件加载,避免以后的多次 IO 操作,从而提高程序执行效率。
相对于预先加载,我们在程序中需要使用自己定义的类的时候就要使用依需求加载方法( load-on-demand ),就是在 Java 程序需要用到的时候再加载,以减少内存的消耗,因为 Java 语言的设计初衷就是面向嵌入式领域的。
2. 隐式加载和显示加载
Java 的加载方式分为隐式加载( implicit )和显示加载(explicit ),上面的例子中就是用的隐式加载的方式。所谓隐式加载就是我们在程序中用 new 关键字来定义一个实例变量,JRE 在执行到 new 关键字的时候就会把对应的实例类加载进入内存。隐式加载的方法很常见,用的也很多, JRE 系统在后台自动的帮助用户加载,减少了用户的工作量,也增加了系统的安全性和程序的可读性。
Java代码
1. Class c = Class.forName("TestClass");
2.
3. TestClass object = (TestClass)c.newInstance();
4.
5. object.method();
通过 Class 类的 forName (String s) 方法把自定义类 TestClass 加载进来,并通过 newInstance ()方法把实例初始化。事实上 Class 类还很多的功能,这里就不细讲了,有兴趣的可以参考 JDK 文档。
Class 的 forName() 方法还有另外一种形式: Class forName(String s, boolean flag, ClassLoaderclassloader) , s 表示需要加载类的名称, flag 表示在调用该函数加载类的时候是否初始化静态区, classloader 表示加载该类所需的加载器。
forName (String s) 是默认通过 ClassLoader.getCallerClassLoader() 调用类加载器的,但是该方法是私有方法,我们无法调用,如果我们想使用 Class forName(String s,boolean flag, ClassLoader classloader) 来加载类的话,就必须要指定类加载器,可以通过如下的方式来实现:
Test test = new Test();//Test 类为自定义的一个测试类;
ClassLoader cl = test. getClass().getClassLoader();
// 获取 test 的类装载器;
Class c = Class.forName("TestClass", true, cl);
因为一个类要加载就必需要有加载器,这里我们是通过获取加载 Test 类的加载器 cl 当作加载 TestClass 的类加载器来实现加载的。
2. 自定义类加载机制
之前我们都是调用系统的类加载器来实现加载的,其实我们是可以自己定义类加载器的。利用 Java 提供的 java.net.URLClassLoader 类就可以实现。下面我们看一段范例:
try{
URL url = new URL("file:/d:/test/lib/");
URLClassLoader urlCL = new URLClassLoader(new URL[]{url});
Class c = urlCL.loadClass("TestClassA");
TestClassA object = (TestClassA)c.newInstance();
object.method();
}catch(Exception e){
e.printStackTrace();
}
我们通过自定义的类加载器实现了 TestClassA 类的加载并调用 method ()方法。分析一下这个程序:首先定义 URL 指定类加载器从何处加载类, URL 可以指向网际网络上的任何位置,也可以指向我们计算机里的文件系统 ( 包含 JAR 文件 ) 。上述范例当中我们从 file:/d:/test/lib/ 处寻找类;然后定义 URLClassLoader 来加载所需的类,最后即可使用该实例了。
3. 类加载器的阶层体系
当执行 java ***.class 的时候, java.exe 会帮助我们找到 JRE ,接着找到位于 JRE 内部的 jvm.dll ,这才是真正的 Java 虚拟机器 , 最后加载动态库,激活 Java 虚拟机器。虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰写而成,这个 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载 Launcher.java 之中的 ExtClassLoader ,并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoader 。然后 Bootstrap Loader 再要求加载 Launcher.java 之中的 AppClassLoader ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的。这里要请大家注意的是,Launcher$ExtClassLoader.class 与 Launcher$AppClassLoader.class 都是由 Bootstrap Loader 所加载,所以 Parent 和由哪个类加载器加载没有关系。
这三个加载器就构成我们的 Java 类加载体系。他们分别从以下的路径寻找程序所需要的类:
BootstrapLoader : sun.boot.class.path
ExtClassLoader: java.ext.dirs
AppClassLoader: java.class.path
这三个系统参量可以通过 System.getProperty() 函数得到具体对应的路径。
五、 重要问题总结
1.为什么要使用这种双亲委托模式呢?
因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。
2.java动态载入class的两种方式是?
implicit隐式,即利用实例化才载入的特性来动态载入class
explicit显式方式,又分两种方式:①java.lang.Class的forName()方法②java.lang.ClassLoader的loadClass()方法
3.解释用Class.forName加载类
Class.forName使用的是被调用者的类加载器来加载类的。
这种特性, 证明了java类加载器中的名称空间是唯一的, 不会相互干扰。
即在一般情况下, 保证同一个类中所关联的其他类都是由当前类的类加载器所加载的。
1 |
public static Class forName(String className) |
|
2 |
throws ClassNotFoundException { |
3 |
return forName0(className, true , ClassLoader.getCallerClassLoader()); |
|
4 |
} |
5 |
|
|
6 |
/** Called after security checks have been made. */ |
7 |
private static native Class forName0(String name, boolean initialize, |
|
8 |
ClassLoader loader) |
9 |
throws ClassNotFoundException; |
上面中 ClassLoader.getCallerClassLoader 就是得到调用当前forName方法的类的类加载器
4.static块在什么时候执行?
当调用forName(String)载入class时执行,如果调用ClassLoader.loadClass并不会执行.forName(String,false,ClassLoader)时也不会执行.
如果载入Class时没有执行static块则在第一次实例化时执行.比如new,Class.newInstance()操作static块仅执行一次
5.各个java类由哪些classLoader加载?
ava类可以通过实例.getClass.getClassLoader()得知接口由AppClassLoader(SystemClassLoader,可以由ClassLoader.getSystemClassLoader()获得实例)载入;ClassLoader类由bootstrap loader载入
6.NoClassDefFoundError和ClassNotFoundException
NoClassDefFoundError:当java源文件已编译成.class文件,但是ClassLoader在运行期间在其搜寻路径load某个类时,没有找到.class文件则报这个错
ClassNotFoundException:试图通过一个String变量来创建一个Class类时不成功则抛出这个异常