JVM类加载器与双亲委派模型
Posted LackMemory
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM类加载器与双亲委派模型相关的知识,希望对你有一定的参考价值。
(7)URLClassLoader类
前面说到,ClassLoader这个顶级父类只是定义好了双亲委派模型的工作机制;但是ClassLoader是个抽象类,无法直接创建对象,所以需要由继承它的子类完成创建对象的任务。子类需要自己实现findClass方法,并且在实例化时指定parent属性的值。如果parent设为null,则意味着它的父“类加载”是启动类加载器。
public URLClassLoader(URL[] urls, ClassLoader parent)
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkCreateClassLoader();
ucp = new URLClassPath(urls);
this.acc = AccessController.getContext();
第一个需要传入的参数有包括一个URL数组和父“类加载器”ClassLoader:parent变量的意义无需多言;而第一个参数
urls用来构造私有常量ucp:
private final URLClassPath ucp;
URLClassLoader还有另外一个私有常量acc:
private final AccessControlContext acc;
它跟权限控制有关,也会频繁出现在URLClassLoader的构造方法中,暂时不用理会。
于是,这个构造方法中,除了指定父“类加载器”外,最重要的代码就是这行了:
ucp = new URLClassPath(urls);
根据传入的URL数组构造一个URLClassPath对象,这个对象用来根据class文件的路径生成一个Resource对象,其中包含了文件的二进制数据:
Resource res = ucp.getResource(path, false);
这个URLClassPath对象才是整个URLClassLoader对象进行类加载的核心,下文我们将会分析到。那么传进来的这些urls都长啥样?先看下构造方法的注释:
/**
* Constructs a new URLClassLoader for the given URLs. The URLs will be
* searched in the order specified for classes and resources after first
* searching in the specified parent class loader. Any URL that ends with
* a '/' is assumed to refer to a directory. Otherwise, the URL is assumed
* to refer to a JAR file which will be downloaded and opened as needed.
*/
字面理解,url可以是本地文件系统的路径,如果以“/”结尾代表其是一个目录,否则默认为jar包文件。其实,这个url也可以是一个网络地址,只要下载下来的数据是个jar包就行。
public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkCreateClassLoader();
ucp = new URLClassPath(urls, factory);
acc = AccessController.getContext();
对比前一个,这个构造方法只是生成URLClassPath对象的方式不一样,使用了传入的URLStreamHandlerFactory对象。
public URLClassLoader(URL[] urls)
super();
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null)
security.checkCreateClassLoader();
ucp = new URLClassPath(urls);
this.acc = AccessController.getContext();
这个构造方法最省事,只需要传入一个URL数组,但是其实现是最复杂的,复杂在父“类加载器”的构造上。这个空参的super()方法,最终调用了ClassLoader的如下构造方法:
protected ClassLoader()
this(checkCreateClassLoader(), getSystemClassLoader());
看下它的注释:
/**
* Creates a new class loader using the <tt>ClassLoader</tt> returned by
* the method @link #getSystemClassLoader()
* <tt>getSystemClassLoader()</tt> as the parent class loader.
*/
也就是说,当创建URLClassLoader对象时如果不指定parent值,那么parent的值最终由ClassLoadder.getSystemClassLoader方法决定,其代码如下:
public static ClassLoader getSystemClassLoader()
initSystemClassLoader();
if (scl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null)
checkClassLoaderPermission(scl, Reflection.getCallerClass());
return scl;
显然,返回值scl会在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();
if (oops != null)
if (oops instanceof Error)
throw (Error) oops;
else
// wrap the exception
throw new Error(oops);
sclSet = true;
其核心逻辑是:1、获取一个Laucher对象;2、调用Laucher对象的getClassLoader方法,用其返回值初始化scl变量;3、使用SystemClassLoaderAction再次为scl赋值。先看一下
Laucher.getClassLoader()方法:
public ClassLoader getClassLoader()
return this.loader;
而loader变量的初始化在Laucher的构造方法中:
public Launcher()
Launcher.ExtClassLoader var1;
try
var1 = Launcher.ExtClassLoader.getExtClassLoader();
catch (IOException var10)
throw new InternalError("Could not create extension class loader");
try
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
catch (IOException var9)
throw new InternalError("Could not create application class loader");
可以看到,在Launcher的构造方法中,先后创建了两个内部类的对象Launcher.ExtClassLoader和Launcher.AppClassLoader,并且后者的parent变量就设置为前者;然后,将Launcher.AppClassLoader对象赋值给this.loader。也就是说,第二步scl的值就是一个Launcher.AppClassLoader对象。我们看下这个内部类:
static class AppClassLoader extends URLClassLoader
是URLClassLoader的子类,其getClassLoader方法如下:
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);
);
首先,获取系统属性java.class.path的值,这个值其实就是经常设置的环境变量ClassPath的值;然后,根据根据这个系统属性的值生成一个File数组,数组包含了ClassPath指定的各个类的class文件;最后,根据File数组生成URL数组,然后把这个数组和传入的parent ClassLoader一起传入给构造方法生成一个Launcher.AppClassLoader对象。
接着看下Launcher.AppClassLoader的构造方法:
AppClassLoader(URL[] var1, ClassLoader var2)
super(var1, var2, Launcher.factory);
直接调用了其父类URLClassLoader的构造方法,这个构造方法在前文已经分析过。
第三步,调用了SystemClassLoaderAction类的run方法为scl重新赋值,来看下这个类:
class SystemClassLoaderAction
implements PrivilegedExceptionAction<ClassLoader>
private ClassLoader parent;
SystemClassLoaderAction(ClassLoader parent)
this.parent = parent;
public ClassLoader run() throws Exception
String cls = System.getProperty("java.system.class.loader");
if (cls == null)
return parent;
Constructor ctor = Class.forName(cls, true, parent)
.getDeclaredConstructor(new Class[] ClassLoader.class );
ClassLoader sys = (ClassLoader) ctor.newInstance(
new Object[] parent );
Thread.currentThread().setContextClassLoader(sys);
return sys;
这个类和ClassLoader在同一个类文件中,但是没有生命为public,我没想明白为何这么设计(内部类不行吗?)。
这个类的作用通过下面这行代码就已经自解释了:
String cls = System.getProperty("java.system.class.loader”);
又是一个环境变量,或者说jvm参数,当指定了
java.system.class.loader系统变量时,那么会把指定的这个类作为System Class Loader,这段代码就是为了生成一个该类的实例(利用了反射的方式),并且它的的父“类加载器”就是上一步生成的
Launcher.AppClassLoader对象。如果没有指定这个系统变量的值,那么就返回
Launcher.AppClassLoader。
绕了这么大一个圈子,原来在调用 public URLClassLoader(URL[] urls)生成对象时,这个对象的父“类加载器”要么是环境变量 java.system.class.loader指定的类,要么是 URLClassLoader的子类 Launcher.AppClassLoader。
说完构造方法,也就是如何创建对象,接着该分析这个URLClassLoader类是如何去加载其他类的,也就是findClass方法是如何实现的。URLClassLoader中定义的findClass方法如下:
protected Class<?> findClass(final String name) throws ClassNotFoundException
try
return 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
return defineClass(name, res);
catch (IOException e)
throw new ClassNotFoundException(name, e);
else
throw new ClassNotFoundException(name);
, acc);
catch (java.security.PrivilegedActionException pae)
throw (ClassNotFoundException) pae.getException();
抛开安全检查等代码之后,核心代码如下:
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null)
try
return defineClass(name, res);
catch (IOException e)
throw new ClassNotFoundException(name, e);
else
throw new ClassNotFoundException(name);
第一行:
String path = name.replace('.', '/').concat(".class”);
name是传入的参数,这个参数显然是loadClass方法传进来的,而loadClass的这个参数是调用时传入的,一般形式如下:
ClassLoader.loadClass(“xx.xx.Xxx”);
当loadClass没有找到时,就会把这个类传递给findClass让它去找。而这行代码显然是在把一个类名转换为其对应的class文件的相对路径名。
第二行:
Resource res = ucp.getResource(path, false);
根据方法名,可以推测是在根据path去磁盘加载这个文件,在内存中生存一个Resource对象。构造方法中初始化的ucp变量终于派上用场了:private final URLClassPath ucp;
其类型是URLClassPath,这个变量的初始化是在构造方法中,这个后面再分析。getResource的实现也暂时略过。
如果这个res不为空,那么进入下一行关键代码:
return defineClass(name, res);
调用了defineClass方法,这是个私有方法,如下:
private Class defineClass(String name, Resource res) throws IOException
long t0 = System.nanoTime();
int i = name.lastIndexOf('.');
URL url = res.getCodeSourceURL();
if (i != -1)
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
if (getAndVerifyPackage(pkgname, man, url) == null)
try
if (man != null)
definePackage(pkgname, man, url);
else
definePackage(pkgname, null, null, null, null, null, null, null);
catch (IllegalArgumentException iae)
// parallel-capable class loaders: re-verify in case of a
// race condition
if (getAndVerifyPackage(pkgname, man, url) == null)
// Should never happen
throw new AssertionError("Cannot find package " +
pkgname);
// Now read the class bytes and define the class
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null)
// Use (direct) ByteBuffer:
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
else
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
方法很长,其参数是类名和对应的文件资源(Resource对象),返回值类型是Class,这意味着,这个方法将会根据clas文件流生成一个Class对象。这个方法在逻辑上分为两个部分,第一部分用来解析、验证包名,具体细节就不再分析,我们假设包名通过了验证。然后进入到第二部分:
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null)
// Use (direct) ByteBuffer:
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, bb, cs);
else
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
return defineClass(name, b, 0, b.length, cs);
If-else分支中的逻辑是类似的:根据url和CodeSigner构造一个CodeSource对象,最终还都是调用了父类中的defineClass方法,传入的参数包括类名name,刚刚生成的CodeSource对象cs,以及字节流。来看下父类中的defineClass方法:
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b, CodeSource cs)
return defineClass(name, b, getProtectionDomain(cs));
被声明为final说明不可覆盖。逻辑很简单,就是继续调用父类的defineClass方法,只是把刚刚的CodeSource对象传递给getProtectionDomain方法得到一个ProtectionDomain对象。继续跟踪父类也就是ClassLoader类的defineClass方法:
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b, ProtectionDomain protectionDomain)
throws ClassFormatError ...
对于这个方法,只需要看注释就足够了,具体逻辑有点复杂就不分析了:
Converts a @link java.nio.ByteBuffer <tt>ByteBuffer</tt> into an instance of class <tt>Class</tt>
说白了,就是把一段字节流转化为一个Class对象。那么URLClassLoader加载类的机制已经分析完毕,最核心的一行代码其实就是:
Resource res = ucp.getResource(path, false);
利用了URLClassPath类的getResource方法根据class文件的相对路径去文件系统中加载字节流。 这个方法就不在本文的讨论范围内了。
以上是关于JVM类加载器与双亲委派模型的主要内容,如果未能解决你的问题,请参考以下文章