首先需要了解一下类的加载过程。
类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例
在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)
1. 启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分
它负责将
2 扩展(Extension)类加载器
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载
3 系统(System)类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()
方法可以获取到该类加载器。
应用程序类加载器,该加载器是由sun.misc.Launcher$AppClassLoader实现,该类加载器负责加载用户类路径上所指定的类库。开发者可通过ClassLoader.getSystemClassLoader()方法直接获取,故又称为系统类加载器。当应用程序没有自定义类加载器时,默认采用该类加载器。
ClassLoader.java
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader(); //初始化系统类加载器 【见下文】
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader ccl = getCallerClassLoader();
if (ccl != null && ccl != scl && !scl.isAncestor(ccl)) {
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
return scl;
}
系统类加载器初始化:
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
throw new Error(oops);
}
}
}
sclSet = true;
}
}
2 双亲委派模型
java双亲委派模型
4 类加载器间的关系
我们进一步了解类加载器间的关系(并非指继承关系),主要可以分为以下4点
- 启动类加载器,由C++实现,没有父类。
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
- 自定义类加载器,父类加载器肯定为AppClassLoader。
5 两个class对象是否为同一个类对象
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
也就是说,在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中,
但前提是覆写loadclass方法,从前面双亲委派模式对loadClass()方法的源码分析中可以知,在方法第一步会通过Class> c = findLoadedClass(name);从缓存查找,类名完整名称相同则不会再次被加载,因此我们必须绕过缓存查询才能重新加载class对象。当然也可直接调用findClass()方法,这样也避免从缓存查找,如下
String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
//创建两个不同的自定义类加载器实例
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);
//通过findClass创建类的Class对象
Class> object1=loader1.findClass("com.zejian.classloader.DemoObj");
Class> object2=loader2.findClass("com.zejian.classloader.DemoObj");
System.out.println("findClass->obj1:"+object1.hashCode());
System.out.println("findClass->obj2:"+object2.hashCode());
/**
* 直接调用findClass方法输出结果:
* findClass->obj1:723074861
findClass->obj2:895328852
生成不同的实例
*/
如果调用父类的loadClass方法,结果如下,除非重写loadClass()方法去掉缓存查找步骤,不过现在一般都不建议重写loadClass()方法。
//直接调用父类的loadClass()方法
Class> obj1 =loader1.loadClass("com.zejian.classloader.DemoObj");
Class> obj2 =loader2.loadClass("com.zejian.classloader.DemoObj");
//不同实例对象的自定义类加载器
System.out.println("loadClass->obj1:"+obj1.hashCode());
System.out.println("loadClass->obj2:"+obj2.hashCode());
//系统类加载器
System.out.println("Class->obj3:"+DemoObj.class.hashCode());
/**
* 直接调用loadClass方法的输出结果,注意并没有重写loadClass方法
* loadClass->obj1:1872034366
loadClass->obj2:1872034366
Class-> obj3:1872034366
都是同一个实例
*/
所以如果不从缓存查询相同完全类名的class对象,那么只有ClassLoader的实例对象不同,同一字节码文件创建的class对象自然也不会相同。
6 class文件的显示加载与隐式加载的概念
所谓class文件的显示加载与隐式加载的方式是指JVM加载class文件到内存的方式,
显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)
或this.getClass().getClassLoader().loadClass()
加载class对象。
隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
3 自定义类加载器
- 每一个ClassLoader都拥有自己独立的类名称空间,类是由ClassLoader将其加载到Java虚拟机中,故类是由加载它的ClassLoader和该类本身一起确定其在Java 运行时环境的唯一性。
- 故只有同一个ClassLoader加载的同一个类,才能算是Java 运行时环境中的相同的两个类。哪怕是来自同一个Class文件,即使被同一个虚拟机加载的两个类,只要ClassLoader不同,那么也属于不同的类。
- 对于equals()、isinstanceof()等方法来判断对象的相等或所属关系都是需要基于同一个ClassLoader。
自定义类加载器示例:
public class ClassLoadDemo{
public static void main(String[] args) throws Exception {
ClassLoader clazzLoader = new ClassLoader() {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
try {
String clazzName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(clazzName);
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(name);
}
}
};
String currentClass = "com.yuanhh.classloader.ClassLoadDemo";
Class> clazz = clazzLoader.loadClass(currentClass);
Object obj = clazz.newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.yuanhh.classloader.ClassLoadDemo);
}
}
上面代码的输出结果:
class com.yuanhh.classloader.ClassLoadDemo
false
- 输出结果的第一行,可以看出这个对象的确是com.yuanhh.classloader.ClassLoadDemo实例化的对象;
- 但第二句是false,这是由于代码中的obj是由用户自定义的类加载器clazzLoader来加载的,可通过obj.getClass().getClassLoader()获取该对象的类加载器为com.yuanhh.classloader.ClassLoadDemoAppClassLoader@XXX。
- 所以可得出结论:即使都是来自同一个Class文件,加载器不同,仍然是两个不同的类,所以返回值是false。
- 通过Class.forName()方法加载的类,采用的是系统类加载器。
4 应用场景
- Tomcat,类加载器架构,自己定义了多个类加载器,
- 保证了同一个服务器的两个Web应用程序的Java类库隔离;
- 保证了同一个服务器的两个Web应用程序的Java类库又可以相互共享;比如多个Spring组织的应用程序不能共享,会造成资源浪费;
- 保证了服务器尽可能保证自身的安全不受不受部署Web应用程序影响;
- 支持JSP应用的服务器,大多需要支持热替换(HotSwap)功能。
- OSGi(Open Service GateWay Initiative),是基于Java语言的动态模块化规范。已成为Java世界的“事实上”的模块化标准,最为熟悉的案例的Eclipse IDE。
参考
Java类加载器(ClassLoader)
深入理解Java类加载器(ClassLoader)