ClassLoader就是类加载器。ClassLoader的作用就是将class文件加载到jvm虚拟机中去。jvm启动时,并不会一次性加载所有的class文件,而是按需动态加载。
Class文件
平时我们在IDE上编写的都是.java文件,.java文件并不能直接在JVM上运行,例子:
public class Main {
//运行入口main函数
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
java文件
编译文件
生成.class文件
运行.class文件
.class文件是字节码格式文件,java虚拟机不能识别.java源文件,只能识别运行.class文件,因此我们需要用javac
将.java转换为.class文件。
JAVA类加载流程
三大类加载器:
- Bootstrap ClassLoader 最顶层的加载类,主要加载 核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
- Extention ClassLoader 扩展的类加载器,加载 目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
- Appclass Loader也称为SystemAppClass 加载 当前应用的classpath的所有类。
PS:Bootstrap ClassLoader可以通过java -Xbootclasspath/a:path
来修改加载的目录,而Extention ClassLoader可以通过-D java.ext.dirs
来修改加载的目录
入口源码
查看精简源码,以下是java虚拟机的入口应用
public class Launcher {
private static Launcher launcher = new Launcher();
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
public static Launcher getLauncher() {
return launcher;
}
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
try {
extcl = ExtClassLoader.getExtClassLoader();
} catch (IOException e) {
throw new InternalError(
"Could not create extension class loader", e);
}
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
//设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
Thread.currentThread().setContextClassLoader(loader);
}
/*
* Returns the class loader used to launch the main application.
*/
public ClassLoader getClassLoader() {
return loader;
}
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {}
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {}
}
- Launcher初始化了ExtClassLoader和AppClassLoader
- Launcher中并没有看见BootstrapClassLoader,但通过
System.getProperty("sun.boot.class.path")
得到了字符串bootClassPath
,这个应该就是BootstrapClassLoader加载的jar包路径。
为了验证刚才说的第2点,我们运行下程序:
果然都是jre下的jar包或class文件
ExtClassLoader源码
/*
* The class loader used for loading installed extensions.
*/
static class ExtClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
/**
* create an ExtClassLoader. The ExtClassLoader is created
* within a context that limits which files it can read
*/
public static ExtClassLoader getExtClassLoader() throws IOException
{
final File[] dirs = getExtDirs();
try {
// Prior implementations of this doPrivileged() block supplied
// aa synthesized ACC via a call to the private method
// ExtClassLoader.getContext().
return AccessController.doPrivileged(
new PrivilegedExceptionAction<ExtClassLoader>() {
public ExtClassLoader run() throws IOException {
int len = dirs.length;
for (int i = 0; i < len; i++) {
MetaIndex.registerDirectory(dirs[i]);
}
return new ExtClassLoader(dirs);
}
});
} catch (java.security.PrivilegedActionException e) {
throw (IOException) e.getException();
}
}
private static File[] getExtDirs() {
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
......
}
前面说过,ExtClassLoader可以通过java -Xbootclasspath/a:path
来修改加载的目录,我们打印下String s = System.getProperty("java.ext.dirs");
果然都是Extensions下的jar包或class文件
AppClassLoader源码
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
......
}
按照前面的规矩,打印final String s = System.getProperty("java.class.path");
图上指的路径,就是项目存放编译生成的class的路径
此时:我们已经知道BootstrapClassLoader、ExtClassLoader、AppClassLoader实际就是查询相应环境属性sun.boot.class.path
、java.ext.dirs
和java.class.path
来加载资源文件的。
我们在跑个例子:
Main.class
这个类的类加载为AppClassLoader,那!为啥String.class
却报错了,然道是Stirng.class
这个类没有类加载器加载。答案是否定的,String.class
不仅有类加载器加载,且是Bootstrap ClassLoader加载的。
每个类加载器都有一个父加载器
比如Main.class
的父加载器是AppClassLoader,那AppClassLoader的父加载器呢?可以使用getParent
很明显看出AppClassLoader的父加载器就是ExtClassLoader,那ExtClassLoader的父加载器呢?
又是空指针,然道ExtClassLoader没有父加载器?
父加载器不是父类
static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}
ExtClassLoader和AppClassLoader其实都是URLClassLoader的子类,为啥AppClassLoader的getParent()
得到的却是ExtClassLoader实例呢?我们先来看看URLClassLoader。
URLClassLoader的源码中并没有找到getParent()
方法。这个方法在ClassLoader.java中。
ClassLoader源码
public abstract class ClassLoader {
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
// @GuardedBy("ClassLoader.class")
private static ClassLoader scl;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
...
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
public final ClassLoader getParent() {
if (parent == null)
return null;
return parent;
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
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;
//通过Launcher获取ClassLoader
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 {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
}
其实getParent()
实际上就是ClassLoader的属性parent,parent的赋值是在ClassLoader对象的构造方法中,有两种情况:
- 外部类指定了ClassLoader时,则ClassLoader的parent就是指定的ClassLoader
- 外部类未指定时,由
getSystemClassLoader()
方法生成,结合前面sun.misc.Laucher源码的getClassLoader()
,返回的是AppCLassLoader。
总结: 一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。
我们在回到前面Lanucher里的关键代码:
ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
loader = AppClassLoader.getAppClassLoader(extcl);
前面我已经证明了AppClassLoader的parent是ExtClassLoader实例,而ExtClassLoader并没有对parent赋值。它调用了父类URLClassLoader的构造方法并传递3个参数。
//单参数构造函数
public ExtClassLoader(File[] dirs) throws IOException {
super(getExtURLs(dirs), null, factory);
}
//三参数构造函数
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory) {
super(parent);
}
这里也就可以证实,ExtClassLoader的parent为null。也能解释前面为啥ExtClassLoader调用getParent()
时会抛错了。
ExtClassLoader既然parent为空,为什么我们还是说Boostrap ClassLoader是它的父加载器呢?
Bootstrap ClassLoader是由C++编写的。
Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。
JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作为一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象。
类的加载 双亲委托
一个类加载器找class和recource时,是通过“委托模式”进行。它首先判断这个class是不是已经加载成功,如果有直接返回,如果没有就通过父加载器查找,不断递归,直到Bootstrap ClassLoader,如果Bootstrap找到了,直接返回,如果没有一级级返回,最后到底自身去查找这些对象。这种机制就叫双亲委托。
流程:
蓝色的代表类加载器向上委托的方向,如果当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。直到Bootstrap ClassLoader。
如果Bootstrap ClassLoader也没有加载过此class实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器,也就是ExtClassLoader,这样类似操作直到终点,也就是我上图中的红色箭头示例。
- 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
- 递归,重复第1部的操作。
- 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是
sun.mic.boot.class
下面的路径。找到就返回,没有找到,让子加载器自己去找。 - Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在
java.ext.dirs
路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。 - ExtClassLoader查找不成功,AppClassLoader就自己查找,在
java.class.path
路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
上面的序列,详细说明了双亲委托的加载流程。委托是从下向上,查找过程却是自上至下。
从上面两张图可以直观的看出类加载的大致过程。若要了解更细点我们还得知道几个重要方法loadClass()
、findLoadedClass()
、findClass()
、defineClass()
重要方法 loadClass( )
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap Classloader
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;
}
}
- 调用
findLoadedClass(String)
去检测这个class是不是已经加载过了 - 调用父加载器
loadClass()
。若父加载器为null,则jvm内置的加载器去替代,也就是Bootstrap ClassLoader。这也解释ExtClassLoader的parent为null,但却说Bootstrap ClassLoader就是它的父加载器 - 如果向上委托父加载器没有加载成功,则通过
findClass(String)
查找。
4.如果class在上面的步骤中找到了,参数resolve又是true的话,那么loadClass()
又会调用resolveClass(Class)
这个方法来生成最终的Class对象
PS:如果要编写一个classLoader的子类,也就是自定义一个classloader,建议覆盖findClass()
方法,而不要直接改写loadClass()
方法。
PS:本文
整理
自以下博客
一看你就懂,超详细java中的ClassLoader详解
若有发现问题请致邮 caoyanglee92@gmail.com
网友评论