深入理解JVM类加载机制
注:本文基于jdk1.8
1.类加载器
1.1类加载器的分类
JVM中默认的类加载器有三种,分别是:
(1)引导类加载器bootstrapLoaser(C++负责实例化,C++实现)
(2)扩展类加载器ExtClassLoader(Launcher类负责实例化,Java实现)
(3)应用类加载器AppClassLoader(Launcher类负责实例化,Java实现)
bootstrapLoader主要加载jre/lib包下的jar包,这些jar包都是JVM中比较核心的jar包,比如其中的rt.jar,我们常用的String,HashMap等就在这个jar包下,也包括下面着重要说的Launcher类。ExtClassLoader主要加载jre/lib/ext包下的jar包。剩下的类比如我们导入的jar包中的类或者是自己写的类默认都是由AppClassLoader加载。这一点可以通过下面一段代码验证:
public class ClassLoaderTest {
class Apple {
int weight;
}
public static void main(String[] args) {
System.out.println("String类的类加载器:" + String.class.getClassLoader());
System.out.println("DNSNameService类的类加载器:" + DNSNameService.class.getClassLoader());
System.out.println("Apple类的类加载器:" + Apple.class.getClassLoader());
System.out.println("----------------------分割线----------------------");
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
System.out.println("bootstrapLoader加载路径如下:");
for (URL url : urls) {
System.out.println(url);
}
System.out.println("----------------------分割线----------------------");
System.out.println("extClassLoader加载路径如下:" + System.getProperty("java.ext.dirs"));
// appClassLoader加载路径较长,建议复制到txt文本中查看
System.out.println("appClassLoader加载路径如下:" + System.getProperty("java.class.path"));
System.out.println("系统默认的类加载器:" + ClassLoader.getSystemClassLoader());
}
}
输出结果如下:
String类的类加载器为null是因为bootstrapLoader是由C++实现的,Java中无法看到;DNSNameService是jre/lib/ext/dnsns.jar包中的类。除此以外,我们还看到系统默认的类加载器就是appClassLoader,也说明除了jre中的包,其他jar包或者说其中的java类默认都是由appClassLoader加载的。
1.2类的加载过程,以com.xx.A为例
加载流程图如下:
其中左侧黄色背景的部分是由JVM底层C++实现的,这里不做赘述,而右边蓝色背景的则是Java实现,需要重点关注一下。bootstrapLoader(也就是引导类加载器)会加载Launcher类,之后由Launcher类负责初始化ExtClassLoader和AppClassLoader,我们来看下Launcher类的源码(截取了重要的部分):
public class Launcher {
// 这里保证了Launcher是单例的
private static Launcher launcher = new Launcher();
// 这个loader就是appClassLoader
private ClassLoader loader;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 初始化extClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 初始化appClassLoader,这里将extClassLoader作为参数传入了,后面会说明传入的原因
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
}
public ClassLoader getClassLoader() {
return this.loader;
}
}
通过源码,我们知道Launcher类是一个单例的类,并且会初始化extClassLoader和appClassLoader,最后向外界提供了获取AppClassLoader的接口,extClassLoader因为不会给外界用了所以不用提供接口。至于初始化appClassLoader时将extClassLoader作为参数传入是为了设置appClassLoader的父加载器为extClassLoader,后面的双亲委派机制会详细说用,而extClassLoader实际上是将父加载器设置为了null,所以extClassLoader是没有父加载器的,更确切的说父加载器是bootstrapLoader,因为bootstrapLoader在java中拿不到所以设置为了null。
2.双亲委派机制
所谓双亲委派机制就是子加载器在加载一个类时,会先请求父加载器加载,如果父加载器没有加载成功,再由自己加载,具体如下图所示:
需要注意的是箭头指示表示extClassLoader是appClassLoader的父加载器,而不是父类。事实上extClassLoader和appClassLoader都继承自URLClassLoader,其继承关系图如下:
并且默认情况下所有自定义的类加载器的父加载器都是appClassLoader。这是因为所有的类加载器都是ClassLoader类的子类,初始化时会默认调用ClassLoader类的实例化方法,具体代码如下图所示:
public abstract class ClassLoader {
private final ClassLoader parent;
protected ClassLoader() {
// 上面的代码验证过getSystemClassLoader返回的是appClassLoader的实例
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
// 这里设置了parent属性
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
}
同理,还记得创建appClassLoader实例时传入了extClassLoader,但是创建extClassLoader实例时什么都没有传入吗,如果你跟一下源代码,就会发现最终还是会调到这个方法,只是extClassLoader传入的parent是一个null值。
3.loadClass()方法详解
appClassLoader的loadClass方法最终会调用到ClassLoader的loadClass方法,那来看看ClassLoader类的loadClass方法以及另外两个重要方法findClass和defineClass源码:
public abstract class ClassLoader {
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 从已经加载过的类中按name查找
Class<?> c = findLoadedClass(name);
// 如果没有加载过则请求父加载器加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 只有extClassLoader的父加载器是null,所以让bootstrapLoader加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 自己加载名为name的类
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
@Deprecated
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
int len = b.remaining();
// Use byte[] if not a direct ByteBufer:
if (!b.isDirect()) {
if (b.hasArray()) {
return defineClass(name, b.array(),
b.position() + b.arrayOffset(), len,
protectionDomain);
} else {
// no array, or read-only array
byte[] tb = new byte[len];
b.get(tb); // get bytes out of byte buffer.
return defineClass(name, tb, 0, len, protectionDomain);
}
}
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
private native Class<?> defineClass0(String name, byte[] b, int off, int len,
ProtectionDomain pd);
private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);
}
从代码可以看出双亲委派机制主要是在loadClass方法中实现的,所以想要打破双亲委派机制的话只要重写loadClass方法,发现类没有加载过时,直接调用findClass加载即可。不过这里的findClass方法只是抛出了一个异常,原因在于ClassLoader是一个抽象类,findClass方法会由子类重写。上面不是说过appClassLoader是继承自URLClassLoader吗,那我们看看URLClassLoader中的findClass方法:
public class URLClassLoader extends SecureClassLoader implements Closeable {
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// 调用defineClass方法真正去加载类
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
}
findClass方法最终调用了defineClass方法去加载类,这下也明白defineClass方法的作用了,那继续跟到defineClass方法就可以知道具体的类加载过程了,不过很遗憾,defineClass是通过调用native方法加载类的,已经脱离Java的范畴了,那就通过文字了解一下具体的加载过程吧:
1.加载:通过磁盘IO将.class文件加载到内存中
2.验证:加载到.class文件之后总得验证一下是不是真的.class文件吧,或者是不是有错误
3.准备:给静态变量分配内存并赋初始值(Java默认的0,false等)
4.解析:静态方法和静态变量的内存地址通常不会改变了,所以对它们的符号引用可以替换为直接引用,这个过程也叫静态链接
5.初始化:给静态变量赋予程序指定的初始值并且执行静态代码块(所以静态代码块是在类加载时就执行了)
总算是讲完整个Java类加载过程了,如有错误,请在评论中指出,感谢指正!