1.ClassLoader作用
顾名思义,就是用来加载Class文件到JVM,以供程序使用的。我们知道,java程序可以动态加载类定义,而这个动态加载机制就是通过ClassLoader来实现的。
那么就有一个问题:既然ClassLoader是用来加载类到JVM的,那么ClassLoader又是如何加载的呢?难道它不是java类?确实有一个ClassLoader不是用java编写的,而是c++,是JVM实现的一部分,这个ClassLoader就是bootstrap ClassLoader(启动类加载器)。这个ClassLoader在JVM运行的时候加载java核心的类来满足java程序最基本的需求,其中就包括其他各个java实现的ClassLoader,如ExtClassLoader,这个ClassLoader用来加载java的扩展API,也就是/lib/ext中的类,一个是AppClassLoader,这个ClassLoader是用来加载用户机器上ClassPath目录中的Class,通常在没有指定ClassLoader的情况下,程序员自定义的类就由该ClassLoader进行加载。
2.加载ClassLoader过程
当运行一个程序的时候,JVM启动,运行bootstrap ClassLoader,该ClassLoader加载java核心API(ExtClassLoader和AppClassLoader也是此时被加载的),然后调用ExtClassLoader加载扩展API,最后AppClassLoader加载ClassPath下定义的Class。
3.ClassLoader的双亲委托模式
每一个自定义ClassLoader都必须继承ClassLoader这个抽象类,而每个ClassLoader都会有一个parent ClassLoader,我们可以看到ClassLoader这个抽象类中有一个getParent()方法,这个方法用来返回当前ClassLoader的parent。注意:这个parent不是被继承的类,而是在实例化该ClassLoader时指定的一个ClassLoader,如果这个parent为null,那么就默认该ClassLoader的parent是bootstrap ClassLoader。那么这个parent有什么用呢?
假设我们自定义了一个ClientClassLoader,我们使用这个自定义的ClassLoader加载java.lang.String,那么这里String是否会被这个ClassLoader加载呢?事实上,java.lang.String这个类不会被ClientClassLoader加载,而是被bootstrap ClassLoader加载。为何会这样?原因就是双亲委托模式,因为在任何一个自定义ClassLoader加载一个类之前,都会先委托它的父亲ClassLoader进行加载,只有当父亲ClassLoader无法加载成功时,才会自己加载。
在上面情况中,因为java.lang.String是属于java核心API的一个类,所以当使用ClientClassLoader加载它时,该ClassLoader会先委托它的父亲ClassLoader进行加载,上面讲过,当parent为null的时候,ClassLoader的parent就是bootstrap ClassLoader,所以在ClassLoader的最顶层就是bootstrap ClassLoader,因此最终委托到bootstrap的时候,bootstrap ClassLoader就会返回String的Class。
下面来看一下ClassLoader中加载类的代码
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null)
{
try
{
if (parent != null)
{
c = parent.loadClass(name, false);
}
else
{
c = findBootstrapClass0(name);
}
}
catch (ClassNotFoundException e)
{
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve)
{
resolveClass(c);
}
return c;
}
从上面的代码可以看出,我们加载一个类的大致过程与之前的描述是一致的,而我们要实现自定义ClassLoader时,只用实现findClass这个方法即可(实现查找Class文件用来加载)。
4.使用双亲委托模式的原因
1)可以避免重复加载。
当父亲加载了该类的时候,就没有必要再使用子ClassLoader再加载一次。
2)考虑到安全i因素。
如果不使用这一种委托模式,那么我们就可以随时使用自定义的String来动态替代java核心API中定义的类型,这样会存在很大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义ClassLoader时无法加载一个自定义的String。
5.类加载过程
6.ClassLoader与命名空间
(1)定义类加载器
简单地说,就是实际加载类的ClassLoader
(2)初始类加载器
不是由他加载的,但是是他的祖先加载的,就叫初始类加载器
举个例子:如果代码中用到File这个类,在没有自定义ClassLoader的情况下,File类先交给AppClassLoader,由于双亲委托模式,又会交给ExtClassLoader,再交给BootstrpClassLoader。这样的话,File类实际上是由BootstrapClassLoader来加载的,所以BootstrapClassLoader就是定义类加载器,AppClassLoader和ExtClassLoader都叫做初始类加载器。
(3)命名空间
在代码中,包名+类名就构成了唯一一个类,但是在jvm中,这还不够,必须再加上ClassLoader。也就是说:代码空间:ClassLoader + 包名 + 类名 才能构成唯一一个class
命名空间还有两个规则:
<1>如果class被某个ClassLoader的parent或者祖先加载了,那么在这个ClassLoader命名空间中也会添加这个类,即也能访问
<2>如果class被平级或者其他分支的下级ClassLoader加载了,肯定就不能访问了。
eg:
如果MyClassA.class文件被UserDefinedClassLoader2(CL2)加载,那么CL3是访问不到这个类的;但是如果被ApplicationClassLoader加载,那么CL1是可以访问的
7.类的生命周期
当我们编写一个java源文件后,经过编译会生成一个 后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能在java虚拟机中运行。java类的生命周期就是指一个class文件从加载到卸载的全过程。
一个java类的完整的生命周期会经历加载、连接、初始化、使用、卸载五个过程。当然也有在加载货连接之后没有初始化就直接使用的情况。现在主要研究类加载器所执行的部分,也就是加载、连接和初始化
1)加载:查找并加载类的class文件
实际上这个过程对应ClassLoader中的defineClass方法:
/**
* Converts an array of bytes into an instance of class <tt>Class</tt>.
* Before the <tt>Class</tt> can be used it must be resolved.
*
* @param name
* The expected <a href="#name">binary name</a> of the class, or
* <tt>null</tt> if not known
*
* @param b
* The bytes that make up the class data. The bytes in positions
* <tt>off</tt> through <tt>off+len-1</tt> should have the format
* of a valid class file as defined by the <a
* href="http://java.sun.com/docs/books/vmspec/">Java Virtual
* Machine Specification</a>.
*
* @param off
* The start offset in <tt>b</tt> of the class data
*
* @param len
* The length of the class data
*
* @return The <tt>Class</tt> object that was created from the specified
* class data.
*
*/
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
可以看出byte[] b中实际上就是class文件的字节码,会返回具体的Class
2)连接:
-验证:确保被加载的类的正确性
-准备:为类的静态变量分配内存,并将其初始化为默认值
-解析:把类中的符号引用转换为直接引用
/**
* Links the specified class. This (misleadingly named) method may be
* used by a class loader to link a class. If the class <tt>c</tt> has
* already been linked, then this method simply returns. Otherwise, the
* class is linked as described in the "Execution" chapter of the <a
* href="http://java.sun.com/docs/books/jls/">Java Language
* Specification</a>.
* </p>
*
* @param c
* The class to link
*
* @throws NullPointerException
* If <tt>c</tt> is <tt>null</tt>.
*
* @see #defineClass(String, byte[], int, int)
*/
protected final void resolveClass(Class<?> c) {
check();
resolveClass0(c);
}
3)初始化:为类的静态变量赋予正确的初始值
从上面可以看出,类的静态变量赋了2次值,什么原因呢?原因是,在连接过程中为静态变量赋值为默认值,也就是说只要定义了静态变量,不管是否设置了初始值,都会先按照静态变量的类型给他一个默认值(如int为0)。到了初始化过程,系统就检查是否用户是否为静态变量设置了初始值,如果设置了就将静态变量设置为指定值。
7.Class类
每个被ClassLoader加载的class文件,最终都会以Class类的实例被程序员引用,我们可以把Class类当做是普通类的一个模板,JVM根据这个模板生成对应的实例,并最终被程序员使用。
Class.forName()是将类加载并且进行初始化,在JDBC中通常使用如下写法:
Class.forName("com.mysql.jdbc.Driver");
同样也可以使用如下写法:
com.mysql.jdbc.Driverdriver = new com.mysql.jdbc.Driver();
效果是完全一样的。
这么做是为了在后面调用DriverManager时,自己选的数据库连接Driver已经加载了,可以看一下加载代码:
publicclass Driver extends NonRegisteringDriver implements java.sql.Driver
{
static
{
try
{
java.sql.DriverManager.registerDriver(newDriver());
}
catch (SQLException E)
{
throw new RuntimeException("Can't register driver!");
}
}
可以看出这是一个静态代码块,所以在类加载并初始化完成之后,就完成了注册。
但是如果使用ClassLoader来加载,即如下写法:
ClassLoader cl = new ClassLoader();
cl.loadClass("com.mysql.jdbc.Driver")
这样
只是在JVM中加载了该类,并没有进行初始化,所以必然会报错