在JVM中,加载某个类时,通过一个类的全限定名来获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,并在内存中创建对应的Class对象作为方法区这个类的各种数据的访问入口。虚拟机设计团队通过类加载器ClassLoader来完成整个过程。通常,二进制字节流可以是jar包或者war包里的.class文件,也可以是来自远程服务器提供的字节流。流程如图:
JVM中类的加载属于延迟加载,并不是一次性就把所需要的类全部加载到内存中。通常,如下场景会触发类加载过程:
在JVM中,系统内置了多个ClassLoader实现类,用于加载不同路径的字节码文件。重要的实现类有Bootstrap ClassLoader,URLClassLoader,Extension ClassLoader和 Application ClassLoader,详细介绍分别如下:
在JVM中ClassLoader类通过loadClass方法和findClass方法通过对应类型名称加载二进制字节流,通过defineClass方法把二进制字节流转换为Class对象,ClassLoader部分源码如下:
// ClassLoader.class
/**
* 默认的加载二进制字节流
*/
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
/**
* 为了兼容双亲委派模式的加载,扩展的二进制字节流加载方法
*/
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
/**
* 定义Class对象
*/
protected final Class> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
为了防止用户复写一些与Java内置的基础类名称相同的类(列如,例如,常见的Object类,String类)被加载到内存对Java内置的对象产生影响,JVM采用双亲委派模式对类进行加载。 在ClassLoader中设置父类ClassLoader 对象parent,在对类进行加载的时候,会首先调用当前ClassLoader父类的loadClass方法进行加载,如果父类ClassLoader加载失败,则再调用当前类的findClass进行加载。在父类ClassLoader的loadClass方法调用过程属于递归调用过程,最终,是首先使用最顶级的父类ClassLoader进行加载。调用过程的源码如下:
// ClassLoader.class
/**
* 加载类
*/
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) {
// 递归地调用parent父类的loadClass获取Class对象
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
}
// 如果父类地递归调用没有加载到,则调用当前类的findClass获取Class对象
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
在JVM中,采用默认的类加载器Application ClassLoader在用户类路径下加载java.lang.Object类的过程为例。加载流程如下图:
当Application ClassLoader执行加载时,首先会调用parent父类Extension ClassLoader的loadClass方法,然后,继续调用其父类Bootstrap ClassLoader的loadClass方法加载。由于Object是java的核心类,因此在Bootstrap ClassLoader会成功加载到内存中。因此,在自定义ClassLoader时,通过实现findClass自定义加载二进制字节流的逻辑,避免破坏JVM的双亲委派模式。
在tomcat7中,使用WebappClassLoader,加载web服务中目录WEB-INF下的类,集成URLClassLoader类,采用双亲委派的模式。WebappClassLoader父类WebappClassLoaderBase中自定义findClass方法,实现WEB-INF下的二进制文件加载逻辑,部分源码如下:
// WebappClassLoaderBase.class
@Override
public Class> findClass(String name) throws ClassNotFoundException {
Class> clazz = null;
try {
if (hasExternalRepositories && searchExternalFirst) {
try {
// 父类调用findClass
clazz = super.findClass(name);
}catch (RuntimeException e) {
throw e;
}
}
if ((clazz == null)) {
try {
// 自定义findClassInternal实现二进制文件加载
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}
}
} catch (ClassNotFoundException e) {
throw e;
}
return (clazz);
}