“通过一个类的全限定名来获取描述此类的二进制流”这个动作放到虚拟机外部去实现,以便让应用程序决定如何去获取所需要的类。这个动作模块就是类加载
器。
而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。
一、双亲委派模型
1、Java默认类加载器
java中
只存在两种不同的类加载器:启动类加载器(Bootstrap ClassLoader),使用C++实现,是虚拟机自身的一部分。另一种是所有其他的类加载器,使用JAVA实现,独立于JVM,并且全部继承自抽象类java.lang.ClassLoader。
(1)、Bootstrap ClassLoader
启动类加载器(Bootstrap ClassLoader),是Java类加载器层次中最顶层的类加载器,负责将存放在<JAVA+HOME>\lib目录中的JDK核心类库,或者被-Xbootclasspath参数所制定的路径中的,并且是JVM识别的(仅按照文件名识别,如rt.jar,如果名字不符合,即使放在lib目录中也不会被加载),加载到虚拟机内存中,启动类加载器无法被JAVA程序直接引用。
查看 sun.boot.class.path 启动类加载路径:
System.out.println(System.getProperty("sun.boot.class.path"));
输出 :
D:\Program Files\java1\jre7\lib\resources.jar;D:\Program Files\java1\jre7\lib\rt.jar;D:\Program Files\java1\jre7\lib\sunrsasign.jar;D:\Program Files\java1\jre7\lib\jsse.jar;D:\Program Files\java1\jre7\lib\jce.jar;D:\Program Files\java1\jre7\lib\charsets.jar;D:\Program Files\java1\jre7\lib\jfr.jar;D:\Program Files\java1\jre7\classes
扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
通过:
ClassLoader extensionClassloader=ClassLoader.getSystemClassLoader().getParent();获得扩展类加载器。
查看
java.ext.dirs 扩展类加载路径:
System.out.println(System.getProperty("java.ext.dirs"));
输出:
D:\Program Files\java1\jre7\lib\ext;C:\windows\Sun\Java\lib\ext
(3)、
Application ClassLoader
应用程序类加载器(Application ClassLoader),由sun.misc.Launcher$AppClassLoader来实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器。负责加载用户类路径(ClassPath)上或者
java.class.path系统属性
所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
查看
java.class.path 系统应用类加载路径:
System.out.println(System.getProperty("java.class.path"));
输出:
E:\XL\workspace\Test\bin;D:\Program Files (x86)\eclipse\plugins\org.junit_4.10.0.v4_10_0_v20120426-0900\junit.jar;D:\Program Files (x86)\eclipse\plugins\org.hamcrest.core_1.1.0.v20090501071000.jar
(4)、ClassLoader体系结构
2、双亲委派
(1)、双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都是应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
通过双亲委派,
Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
就是保证某个范围的类一定是被某个类加载器所加载的,这就保证在程序中同 一个类不会被不同的类加载器加载。这样做的一个主要的考量,就是从安全层 面上,杜绝通过使用和JRE相同的类名冒充现有JRE的类达到替换的攻击方式。
(2)、双亲委派的实现
源码:
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); //
1.检测此Class是否载入过(即在cache中是否有此Class),如果有到6,如果没有到2
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) { // 2. 如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4
c = parent.loadClass(name, false); /
/
3.请求parent classloader载入,如果成功到6,不成功到5
} else {
c = findBootstrapClassOrNull(name); //
4. 请求jvm从bootstrap classloader中载入,如果成功到8
}
} 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.
long t1 = System.nanoTime();
c = findClass(name); // 5. 寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到6
// 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;
//
6.返回Class.
}
}
(3)、判断类相等
JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。
就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。
二、自定义类加载器
自定义类加载器,从网络上获取class文件,以及获取网络资源。步骤如下:
1、继承java.lang.ClassLoader
2、重写父类的findClass方法
注:JDK在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。
3、重写父类的findResource方法(可选)
代码:
package com.xl.jvm;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
public class NetworkClassloader extends ClassLoader {
private String rootUrl;
public NetworkClassloader(String rootUrl) {
this.rootUrl = rootUrl;
}
public NetworkClassloader(String rootUrl,ClassLoader parent){
super(parent);
this.rootUrl = rootUrl;
}
@Override //
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class clazz = null;
this.findLoadedClass(name); // 父类已加载
if (clazz == null) { //检查该类是否已被加载过
byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组
if (classData == null) {
throw new ClassNotFoundException();
}
clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例
}
return clazz;
}
@Override
protected URL findResource(String path) {
// TODO Auto-generated method stub
URL url = null;
try {
url = new URL(rootUrl +"/" + path);
} catch (MalformedURLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return url;
}
private byte[] getClassData(String name) {
InputStream is = null;
try {
String path = classNameToPath(name);
URL url = new URL(path);
byte[] buff = new byte[1024*4];
int len = -1;
is = url.openStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while((len = is.read(buff)) != -1) {
baos.write(buff,0,len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return null;
}
private String classNameToPath(String name) {
return rootUrl + "/" + name.replace(".", "/") + ".class";
}
public String getRootUrl() {
return rootUrl;
}
public void setRootUrl(String rootUrl) {
this.rootUrl = rootUrl;
}
}
使用:
String rootUrl = "http://localhost:8080/myweb/classes";
NetworkClassloader networkClassLoader = new NetworkClassLoader(rootUrl);
String classname = "org.com.xl.NetClassLoaderTest";
Class clazz = networkClassLoader.loadClass(classname);
System.out.println(clazz.getClassLoader());
Url resource = networkClassLoader.getResource("classes.config");
参考资料:《深入理解Java虚拟机》