类加载器
虚拟机设计团队把类的加载阶段中的”通过类的全限定名来获取描述此类的二进制文件“这个操作放到虚拟机外部去实现,以便让程序自己去决定去实现如何加载一个类,实现这一功能的就是“类加载器”。
类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定它在JVM中的唯一性。在比较两个类是否相等的时候,只有这两个这两个类是由同一个类加载器加载的时候才有意义,否则即使来源属于同一个Class文件,被同一个虚拟机加载,仍然不会相等,这里的相等包括对象的equals方法和isInstance方法的返回值,还包括instanceof关键字。
双亲委派模型
多数Java程序都会用到三种类加载器:
启动类加载器(Bootstrap ClassLoader): 启动类加载器是用本地代码实现的类加载器,它负责将 /lib下面的核心类库 或 -Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中,这是开发者无法通过引用去访问到的。
扩展类加载器(Extenssion ClassLoader):负责将 /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中,开发者可以直接使用扩展类加载器。
应用类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器(由于这个类加载器是getSystemClassLoader()的返回值,所以也叫做系统类加载器), 如果没有自定义过类加载器,一般情况下这就是默认的类加载器。
在应用中,这三种类加载器会配合使用, 遵循双亲委派模型:
双亲委派模型的规定的工作流程是:
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用)。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。
可以通过一段简单的代码来加深对这一过程的理解:
public class Test{
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
}
}
输出的结果是:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@7440e464
null
通过代码可以看出,ExtClassLoadrer确实是AppClassLoader的父加载器,同时通过第三个输出null
也可进一步验证了“开发者无法通过引用操作启动类加载器”这一说法。
双亲委派模型实际上定义了一种类加载的时候选择类加载器的优先级关系,这样的好处是保证了默认情况下同一个Class文件产生类是由同一个类加载器加载,也就是保证了同一个类的“唯一性”,例如 java.lang.Object类应是所有类的父类了,自然只能有一个,可如果允许使用不同的类加载器去加载它,那么肯定会存在多个互不相等的java.lang.Object类,会造成无法想象的后果。
双亲委派模型的委派过程也很好理解,其loadClass的过程是这样的:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查该类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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.
//如果父类的加载器无法完成加载操作时,就调用自身的findClass方法来进行类加载。
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
自定义类加载器
当然有时候也会遇到自定义类加载器的情况,比如需要给自己的类加密,加密过后的类肯定没办法用系统的类加载器进行加载,这时候就需要使用自定义的类加载器进行解密然后加载类了。还比如说一些字节码需要从网络上加载,就需要自定义类加载器来加载指定来源的类。
下面就是一个简单的从本地文件加载类的例子:
public class LoadExternalClass extends ClassLoader {
String rootDir;
public LoadExternalClass(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);//获取类的字节数组
if (classData == null){
throw new ClassNotFoundException();
} else {
return defineClass(name,classData,0,classData.length);
}
}
private byte[] getClassData(String className){
String path = classNameToPath(className);
try {
InputStream clazzStream = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 0;
byte[] buffer = new byte[4096];
//将字节码写入输出流
while ((bufferSize = clazzStream.read(buffer))!=-1){
baos.write(buffer,0,bufferSize);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className){
return rootDir + File.separatorChar + className.substring(className.lastIndexOf(".")+1)+".class";
}
}
自定义类加载器的时候并不需要重写loadClass()方法,应双亲委派逻辑已经在loadClass方法中写明了,它首先调用findLoadedClass()方法来检查该类是否已经被加载过,如果没有加载过的话,就会调用父类加载器的loadClass()方法来尝试加载该类,如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类.所以,只需要重写findClass()方法即可。
最后便可依在main方法中调用:
LoadExternalClass lec = new LoadExternalClass("/Users/xushuzhan/Desktop/blog");
Class<?> clazz = lec.loadClass("parental_appointment.TestBean");
TestBean b = (TestBean) clazz.newInstance();
b.sayHello("word");
便会输出:
hello word
TestBean中只有一个方法:
public String sayHello(String content){
System.out.println("hello "+ content);
return "hello "+content;
}