类的唯一性
判断是否为同一个类的条件:
- 类的完整类名必须一致,包括包名。
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
//创建两个不同的自定义类加载器实例
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);
//通过findClass创建类的Class对象
Class<?> object1=loader1.findClass("com.john.classloader.DemoObj");
Class<?> object2=loader2.findClass("com.john.classloader.DemoObj");
System.out.println("obj1:"+object1.hashCode());//obj1:723074861
System.out.println("obj2:"+object2.hashCode());//obj2:895328852
双亲委派模型
BootStrap ClassLoader称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,由C++实现,是虚拟机自身的一部分,没有父类。
EtxClassLoader称为扩展类加载器,负责加载Java的扩展类库,Java 虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载 Java 类。默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。它由Java语言实现,父类加载器为null。
AppClassLoader称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。一般来说,Java 应用的类都是由它来完成加载的。它由Java语言实现,父类加载器为ExtClassLoader。
UserClassLoader称为自定义类加载器,父类加载器为AppClassLoader。
工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
优点
1、避免重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
2、考虑到安全因素,java核心api中定义类型只会被启动类加载器加载,不会被随意替换。
自定义类加载器
ClassLoader类主要方法介绍:
1、loadClass(String)
该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先从缓存查找该class对象,找到就不用重新加载
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
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
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;
}
}
当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载。
2、findClass(String)
在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
3、defineClass(byte[] b, int off, int len)
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象。
自定义类加载器主要步骤:
1、继承ClassLoader类
2、重写findClass方法,之前已经提过在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,但是在JDK1.2之后引入了双亲委派模型,如果再重写loadClass方法每次都要重写里面双亲委派的逻辑,所以建议重写findClass方法。
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
tomcat类加载机制
如上图所示,tomcat在初始化时创建以下类加载器:
Bootstrap 类加载器
加载JVM启动所需的类以及标准扩展类($JAVA_HOME/jre/lib/ext下)
System类加载器
加载tomcat启动所需的类,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下。
包含一下类:$CATALINA_HOME/bin/bootstrap.jar:tomcat server启动所需的main方法,
$CATALINA_BASE/bin/tomcat-juli.jar:日志接口实现类,包含java.util.loggingAPI 。
Common 类加载器
这个类加载器包含对Tomcat内部类和所有Web应用程序都可见的附加类。 位于 $CATALINA_BASE/lib下
webappclassloader
每个web应用都有一个对应的类加载器实例,该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般加载器的顺序是相反的。这是java servlet规范中的推荐做法,其目的是是的web应用自己的类优先级高与web容器提供的类。这种代理模式的一个例外是:Java核心库是不再查找范围之内的。这也是为了保证Java核心库的类型安全。
因此,从web应用程序的角度来看,类或资源加载的顺序如下:
- 使用Bootstrap 加载器加载
- web application下的/WEB_INF/classes
- web application下的/WEB_INF/lib/*.jar
- System类加载器
- Common类加载器
线程上下文类加载器Thread.getContextClassLoader()