ClassLoader,即java类加载器,主要作用是将class加载到JVM内,同时它还要考虑class由谁来加载。在说java的类加载机制之前,还是像前面的博客一样,先说说为什么要知道java的类加载机制。个人认为主要有以下几个原因:
一般说到java的类加载机制,都要说到“双亲委派模型”(其实个人很不理解为什么叫“双亲”,其实英文叫“parent”)。使用这种机制,可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。JVM根据 类名+包名+ClassLoader实例ID
来判定两个类是否相同,是否已经加载过(所以这里可以略微扩展下,可以通过创建不同的classloader实例来实现类的热部署)。
有个图很形象(来源见参考资料)。
注意:如果想形象地看到java的类加载顺序,可以在运行java的时候加个启动参数,即java –verbose
下面结合上图来详细介绍下java的类加载过程。
很多资料和文章里说,ExtClassLoader
的父类加载器是BootStrapClassLoader
,其实这里省掉了一句话,容易造成很多新手(比如我)的迷惑。严格来说,ExtClassLoader
的父类加载器是null
,只不过在默认的ClassLoader 的 loadClass
方法中,当parent为null
时,是交给BootStrapClassLoader
来处理的,而且ExtClassLoader
没有重写默认的loadClass
方法,所以,ExtClassLoader
也会调用BootStrapLoader
类加载器来加载,这就导致“BootStrapClassLoader具备了ExtClassLoader父类加载器的功能”。看一下下面的代码就很容易理解上面这一大段话了。
/**
* 查看父类加载器
*/
private static void test1() {
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
System.out.println("系统类装载器:" + appClassLoader);
ClassLoader extensionClassLoader = appClassLoader.getParent();
System.out.println("系统类装载器的父类加载器——扩展类加载器:" + extensionClassLoader);
ClassLoader bootstrapClassLoader = extensionClassLoader.getParent();
System.out.println("扩展类加载器的父类加载器——引导类加载器:" + bootstrapClassLoader);
}
可以看出ExtensionClassLoader
的parent
为null
。
查看classloader的源码可以发现三个重要的方法:
好,可能有人看了上面会疑惑,为什么上面说自定义classloader是需要重写findClass而不是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.
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;
}
}
可以看到,JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的判断方法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。
看完这一大串可能有人还是不理解,ok,那我们现在就动手写一个自己的ClassLoader,尽量包含上面三个方法。
先定义一个Person接口。
public interface Person {
public void say();
}
再定一个高富帅类实现这个接口
public class HighRichHandsome implements Person {
@Override
public void say() {
System.out.println("I don't care whether you are rich or not");
}
}
好的,开胃菜结束,主角来了,MyClassLoader。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
/*
* 覆盖了父类的findClass,实现自定义的classloader
*/
@Override
public Class> findClass(String name) {
byte[] bt = loadClassData(name);
return defineClass(name, bt, 0, bt.length);
}
private byte[] loadClassData(String className) {
InputStream is = getClass().getClassLoader().getResourceAsStream(
className.replace(".", "/") + ".class");
ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = is.read()) != -1) {
byteSt.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
return byteSt.toByteArray();
}
}
代码很简单,不解释了,最后在测试类LoaderTest里写个测试方法。
/**
* 父类classloader
* @throws Exception
*/
private static void test2() throws Exception{
MyClassLoader loader = new MyClassLoader();
Class> c = loader.loadClass("com.alibaba.classload.HighRichHandsome");
System.out.println("Loaded by :" + c.getClassLoader());
Person p = (Person) c.newInstance();
p.say();
HighRichHandsome man = (HighRichHandsome) c.newInstance();
man.say();
}
main方法中调用这个方法即可。LoaderTest默认构造函数会设置AppClassLoader
为parent
, 测试时执行test2()
方法会发现HighRichHandsome
类是委托AppClassLoader
加载的,所以AppClassLoader可以访问到,不会出错。
但是我们再想一下,如果我们直接加载,不委托给父类加载,会出现什么情况?
/**
* 自己的classloader加载
* @throws Exception
*/
private static void test3() throws Exception{
MyClassLoader loader = new MyClassLoader();
Class> c = loader.findClass("com.alibaba.classload.HighRichHandsome");
System.out.println("Loaded by :" + c.getClassLoader());
Person p = (Person) c.newInstance();
p.say();
//注释下面两行则不报错
HighRichHandsome man = (HighRichHandsome) c.newInstance();
man.say();
}
可以看到,悲剧的报错了。根据class loader命名空间规则,每个class loader都有自己唯一的命名空间,每个class loader 只能访问自己命名空间中的类,如果一个类是委托parent加载的,那么加载后,这个类就类似共享的,parent和child都可以访问到这个类,因为parent是不会委托child加载类的,所以child加载的类parent访问不到。简单来说,即子加载器的命名空间包含了parent加载的所有类,反过来则不成立。 本例中LoaderTest
类是AppClassLoader
加载的,所以其看不见由MyClassLoader
加载的HighRichHandsome
类,但Person接口是可以访问的,所以赋给Person类型不会出错。
1.Class.forName()
相信大家都写过连接数据库的例子,基本上就是加载驱动,建立连接,创建请求,写prepareStatement,关闭连接之类的。在这里,有一段代码:
public DbTest() {
try {
Class.forName("com.mysql.jdbc.Driver");// 加载驱动
conn = DriverManager.getConnection(url, "root", "");// 建立连接
stm = conn.createStatement(); // 创建请求
} catch (Exception e) {
e.printStackTrace();
}
}
我相信大家一开始的时候肯定都有些疑惑,就是Class.forName(“com.mysql.jdbc.Driver”),为什么加载驱动是Class.forName,而不是ClassLoader的loadClass?为什么这么写就可以加载驱动了呢?
其实Class.forName()
是显示加载类,作用是要求JVM查找并加载指定的类,也就是说JVM会执行该类的静态代码段。查看com.mysql.jdbc.Driver
源码可以发现里面有个静态代码块,在加载后,类里面的静态代码块就执行(主要目的是注册驱动,把自己注册进去),所以主要目的就是为了触发这个静态方法。
2.Web容器加载机制
其实web容器的加载机制这一点我说不好,因为没看过诸如tomcat之类的源码,但是根据自己使用感觉以及查了相关资料,可以简单介绍一下。一般服务器端都要求类加载器能够反转委派原则,也就是先加载本地的类,如果加载不到,再到parent中加载。这个得细看,我这里只知道这个概念,所以就不说了。JavaEE规范推荐每个模块的类加载器先加载本类加载的内容,如果加载不到才回到parent类加载器中尝试加载。
3.重复加载与回收
一个class可以被不同的class loader重复加载,但同一个class只能被同一个class loader加载一次。见下列代码:
/**
* 对象只加载一次,返回true
*/
private static void test4() {
ClassLoader c1 = LoaderTest.class.getClassLoader();
LoaderTest loadtest = new LoaderTest();
ClassLoader c2 = loadtest.getClass().getClassLoader();
System.out.println("c1.equals(c2):"+c1.equals(c2));
}
类的回收。说到最开始的why,其实java的回收机制我前面有篇博客说的比较详细,这里就插一句废话吧,当某个classloader加载的所有类实例化的所有对象都被回收了,则该classloader会被回收。
参考:http://blog.csdn.net/xyang81/article/details/7292380
ps:话说csdn的markdown写出来的效果实在不敢恭维,小小的吐槽一下。