Java类加载深度剖析
- 1.类加载的入口
- 2.AppClassLoader、ExtClassLoader、BootstrapClassLoader的血脉渊源
- 3.ExtClassLoader究竟是不是孙大圣
- 4.为什么自定义类加载器的父类加载器是AppClassLoader呢?
- 5.我们应该如何打破双亲委派机制呢?
- 6.如何保证同class对象是唯一的
- 7.确定是否是同一个类对象取决于类加载器的类型还是说在类型相同的基础上还得是同一个具体对象(内存地址要相同)
- 8.心得推论
1.类加载的入口
sum.misc.Launcher:
它是一个java虚拟机的入口应用。
这里我们可以发现BootstrapClassLoader的加载路径:System.getProperty("sun.boot.class.path")
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return launcher;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//拓展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//应用类加载器,将拓展类加载器作为应用类加载器的parent
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//设置当前线程上下文加载器是应用类加载器
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
2.AppClassLoader、ExtClassLoader、BootstrapClassLoader的血脉渊源
上面代码片段java虚拟机的入口应用实例化了ExtClassLoader和AppClassLoader,那么我们看看实例化这两个ClassLoader的时候有没有对它们有额外的设置,比如双亲委派中的parent:
AppClassLoader:
这里也可以看出AppClassLoader的加载路径System.getProperty("java.class.path");
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
new Launcher.AppClassLoader(var1x, var0);
中var0是ExtClassLoader(1中提及过),一路追踪上去,追踪到了顶层父类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;
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}
之前一直说ExtClassLoader 的父类加载器是null,那么我们也遵循同样的思路一探究竟:
static class ExtClassLoader extends URLClassLoader {
private static volatile Launcher.ExtClassLoader instance;
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
注意,跟AppClassLoader一样的逻辑,但这里的ClassLoader是null,这也就解释了日常输出的ExtClassLoader 的父类加载器是null。
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
3.ExtClassLoader究竟是不是孙大圣
既然ExtClassLoader父类加载器为null,那么双亲委派机制中的ExtClassLoader委托BootstrapClassLoader加载类又是怎么一回事呢?jdk从加载类的逻辑去实现这个功能:
在ClassLoader类中:
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 {
//有父类加载器就使用父类加载器加载,例如AppClassLoader和我们不正确地编写自定义类加载器(误将AppClassLoader作为父类加载器)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//针对ExtClassLoader
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;
}
}
4.为什么自定义类加载器的父类加载器是AppClassLoader呢?
那就从构造方法的执行顺序中说起了,我们自定义的ClassLoader会继承ClassLoader,执行自定义的ClassLoader之前会执行ClassLoader的构造方法,一般是无参构造方法。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}
对比可得我们重点关注getSystemClassLoader()方法
追踪:
initSystemClassLoader()->
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;
//重点
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
我们之前提及过this.loader被设置为AppClassLoader,因此自定义类加载器的父类加载器是AppClassLoader。
5.我们应该如何打破双亲委派机制呢?
与其说打破双亲委派机制,不如说如何将我们要加载的class与我们自定义的类加载器建立联系,而不是委派AppClassLoader加载。
我们平常自定义类加载器的时候可能在不经意的时候在重写loadClass方法的时候调用了getParent().loadClass(name);
这就导致了使用AppClassLoader去加载类。
public abstract class ClassLoader {
@CallerSensitive
public final ClassLoader getParent() {
if (parent == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Check access to the parent class loader
// If the caller's class loader is same as this class loader,
// permission check is performed.
checkClassLoaderPermission(parent, Reflection.getCallerClass());
}
return parent;
}
}
那么自定义类加载器和类是如何建立联系的呢?这里先放个彩蛋:
注意this(每new一个classLoader都会调这个方法将自身设置进去)。
// The "default" domain. Set as the default ProtectionDomain on newly
// created classes.
private final ProtectionDomain defaultDomain =
new ProtectionDomain(new CodeSource(null, (Certificate[]) null),
null, this, null);
重点关注protectionDomain = preDefineClass(name, protectionDomain);
和Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
前者封装classLoader,后者将classLoader和类对象建立关系。
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;
}
每个classLoader加载类的时候,最终都会调用这个方法Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
,
每个classLoader都对应一个defaultDomain。
最后调用本地方法生成class对象并且与classLoader建立联系。
private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
综上所述:要打破双亲委派机制,关键点是想办法让真正的ProtectionDomain 与自定义的类加载器关联,要做到这一点,就不能在重写loadClass方法中调用getParent().loadClass(name);
方法。而且,往往会存在一种幻觉,new 自定义类加载器的时候ProtectionDomain 是封装了自定义类加载器,但是deBug到protectionDomain = preDefineClass(name, protectionzDomain);
的时候发现是AppClassLoader或者其他上层加载器,那就是因为在重写loadClass方法中调用getParent().loadClass(name);
方法。
6.如何保证同class对象是唯一的
参考地址
jvm使用包路径的类名和类加载器唯一确定了一个类
怎么说呢,在每次调用loadClass
方法将要类的时候,内部会首先调用Class<?> c = findLoadedClass(name);
,JDK官方源码注释:First, check if the class has already been loaded,中译:首先,检查这个类是否已经加载过了,如果返回的引用不为空,就将这个class对象返回去,同时,这个方法最终会调用本地方法,这个本地方法的作用就如上所述:jvm使用包路径的类名和类加载器唯一确定了一个类。
7.确定是否是同一个类对象取决于类加载器的类型还是说在类型相同的基础上还得是同一个具体对象(内存地址要相同)
答案:确定是否是同一个类对象取决在类加载器的类型相同的基础上还得是同一个具体对象(内存地址要相同),验证思路:自定义类加载器,加载过程不得调用父加载器,new两个类加载器和使用同一个类加载器去加载这个类,如果class对象相同说明判定一个类对象是否是同一个门槛并不高,仅仅取决于是不是同样的类加载器类型,反之说明门槛稍高:得是同样的类加载器类型并且是同一个类加载器对象才可以。
8.心得推论
重新认识一个类对象和改类对象类型的实例对象,java万物皆对象,平常所说的类模板衍生出实例对象,其实这个类模板就是类对象,而且在jvm中只有一个,朦胧之中就可以将jvm将class文件加载到内存落地生成类对象(方法区-公共-模板-唯一)作为入口联系起来。