一、类加载器
概念:
通过一个类的全限定类名来获取描述该类的二进制字节流,将这个动作放到JAVA虚拟机外部去实现,以便应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为**“类加载器”**。
1.1 类与类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在JAVA虚拟机的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话的意思是,比较两个类是否相等,只有在这两个类都是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同个class文件,被同一个JAVA虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。这里的相等指的是Class对象的equals()方法、instanceof 关键字做对象所属关系判定等各种情况。
public class DemoClassLoader {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader() {
//这个方法如果被覆盖掉了,说明双亲委派机制失效,双亲委派机制,是通过loadClass来连接父加载器的
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
Object o = classLoader.loadClass("com.example.demo.classLoader.DemoClassLoader").newInstance();
System.out.println(o.getClass());
//自定义的类加载器实例化的对象
System.out.println(o instanceof DemoClassLoader);
DemoClassLoader a = new DemoClassLoader();
DemoClassLoader b = new DemoClassLoader();
System.out.println(o.getClass()==a.getClass());//false
System.out.println(a.getClass()==b.getClass());//true
}
}
以上的输出结果为:
class com.example.demo.classLoader.DemoClassLoader
false
false
true
从结果不难得知,我们自定义了一个类加载去加载这个类,并实例化出了对象,但是从上面个的输出结果可以看出,这个类确实是com.example.demo.classLoader.DemoClassLoader,但是在给对象做所属类型检验是返回了false,说明JAVA虚拟机中同时存在两个DemoClassLoader类,一个是由虚拟机的应用程序加载,一个是由我们自定义的加载器加载,虽然他们都是来自于同个class文件,但是在虚拟机中仍然是连个相互独立的类。
1.3 三层类加载器
对于JAVA虚拟机来说,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。
站在开发者角度,则分为三层类加载器:启动类加载器、扩展类加载器、应用程序类加载器。
-
启动类加载器:
这个类加载器负责加载<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是JAVA虚拟机能够识别的类库加载到虚拟机的内存中。启动类加载器无法被JAVA程序直接引用,所以当获取启动类加载器的时候,会返回一个null值。 -
扩展类加载器:
这个类加载器由ExtClassLoader实现,负责加载<JAVA_HOME>/lib/ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。 -
应用程序类加载器:
这个类加载器由AppClassLoader实现,负责加载用户类路径(classPath)上所有的类库,一般情况下,如果程序中没有自定义类加载器,AppClassLoader为应用程序的默认的类加载器。
1.4 双亲委派机制
双亲委派模型的工作过程是:如果一个类加载器收到了类的加载请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有类的加载请求,最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求的时候(在搜索范围内搜索不到对应的类),子加载器才会尝试自己去完成加载。
双亲委派机制的一个好处就是,它设置了类加载器的优先级,例如类java.lang.Object,它存在rt.jar包中,无论哪一个类加载器要加载这个类,最终都委派给处于模型最顶端的启动类加载器,这样就能确保程序里面的Object类在各种类加载器中都是同一个。以下代码为双亲委派机制的核心代码在loadClass方法里面。
双亲委派机制的好处:
-
可以避免重复加载,当父类加载器已经加载到该类时,就没必要子加载器再加载一次。
-
考虑到安全因素,JAVA核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Object的类,通过双亲委派机制模型传递启动类加载器,而启动类加载器在核心java api中发现这个名称的类已经被加载过了,并不会重新加载网络传递过来的java.lang.Object,而直接返回已经加载过的Object,从而保证相同的类名的字节码文件,在虚拟机内存只保存一份。这样就可以避免核心api没恶意篡改,不过这个机制的前提是,这个传递过来的Object类加载请求必须是得到启动类加载器才可以,因为同一个类在同一个加载器里,只会被加载一次,但是如果是不同的类加载器的话,还是可以被加载的。
二、自定义类加载器
在讲解热部署之前,需要先连接ClassLoader这个类里面几个比较核心的方法:
- findClass: 在自定义类加载时,一般是覆盖这个方法,且ClassLoader中给出了一个抛错的实现。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
- loadClass: 这个方法是双亲委派机制的代码实现,只有父类加载器加载不到类的时候,才会调用自身findClass方法进行类的查找,所以在定义自己的类加载器时,不要覆盖掉该方法,而是应该覆盖掉findClass方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查类是否已经被加载过
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异常说明父加载器,加载不到
}
if (c == null) {
// 父加载器无法加载时,调用本身的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;
}
}
- defineClass: 用来将class字节解析成虚拟机能够识别的对象,defineClass方法一般和findClass一起结合使用,在自定义类加载器的时候,一般会覆盖ClassLoader的findClass方法,然后再调用defineClass方法生成Class对象。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
- resolveClass: 连接指定的类。
三、通过类加载器来实现热部署
绕开双亲委派机制核心方法loadClass,直接调用自身重写的findClass方法
四、JAVA模块化系统(JDK9)
//TODO