标签:
虽然之前也看过jvm相关的书籍,但是都是概念层次上的理解。今天特地花一天时间研究了下类加载器,感觉上是没有那么生疏了,但也只是冰山一角,索性就不完整地分析一番吧。内容有些长,可使用目录快速查阅。
简单说下JVM预定义的三种类型的类加载器,这个也算是老生常谈了。当JVM启动一个项目的时候,它将缺省使用以下三种类型的类加载器:
1. 启动(Bootstrap)类加载器:负责装载<Java_Home>/lib
下面的核心类库或-Xbootclasspath
选项指定的jar包。由native方法实现加载过程,程序无法直接获取到该类加载器,无法对其进行任何操作。
2. 扩展(Extension)类加载器:扩展类加载器由sun.misc.Launcher.ExtClassLoader
实现的。负责加载<Java_Home>/lib/ext
或者由系统变量-Djava.ext.dir
指定位置中的类库。程序可以访问并使用扩展类加载器。
3. 系统(System)类加载器:系统类加载器是由sun.misc.Launcher.AppClassLoader
实现的,有的地方也叫SystemClassLoader
。负责加载系统类路径-classpath
或-Djava.class.path
变量所指的目录下的类库。程序可以访问并使用系统类加载器。
三种类加载器的父子关系如图所示
注意这儿的父子并不是继承的意思,它们都是ClassLoader
抽象类的实现,因此都含有一个ClassLoader parent
成员变量,该变量指向其父加载器。这儿的委派关系也可以被成为代理。
然后我们来看看代码吧,loadClass
是抽象类ClassLoader
中的类加载的核心方法。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 若本加载器之前是否已加载过,直接取缓存,native方法实现
Class c = findLoadedClass(name);
if (c == null) {
try {
// 只要有父加载器就先委派父加载器来加载
if (parent != null) {
// 注意此处递归调用
c = parent.loadClass(name, false);
} else {
// Bootstrap是无法被访问的,所以ext的parent必然null
// 此时直接用native方法调用启动类加载加载,若找不到则抛异常
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 对ClassNotFoundException不做处理,仅用作退出递归
}
if (c == null) {
// 如果父加载器无法加载那么就在本类加载器的范围内进行查找
// findClass找到class文件后将调用defineClass方法把字节码导入方法区,同时缓存结果
c = findClass(name);
}
}
// 是否解析,默认false
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看出所谓的双亲委派的本质就是这两句递归代码:
if (parent != null) {
c = parent.loadClass(name, false);
}
加载成功就得到Class对象c,失败就抛异常然后前一级方法用catch抓住并忽略,再进行当前类加载器的findClass()
操作,如此反复。
注意
1. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
2. 类加载后将进入连接(link)阶段,它包含验证、准备、解析,resolve参数决定是否执行解析阶段,jvm规范并没有严格指定该阶段的执行时刻
3. 由于先使用findLoadedClass()
查找缓存,相同的类只会被加载一次
当你自己写一个类实现了ClassLoader
后,那么它就是用户自定义类加载器了。实例化自定义类加载器时,若不指定父类加载器(不把父ClassLoader传入构造函数)的情况下,默认采用系统类加载器()。对应的无参默认构造函数实现如下:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
它将调用有参构造函数,将getSystemClassLoader()
取到的系统类加载器作为parent传入。因此用户自定义类加载器也可以通过双亲委派的方式获取到那3个类加载器加载的类对象了。
当实现自定义类加载器时不应重写loadClass()
,除非你不需要双亲委派机制。要重写的是findClass()
的逻辑,也就是加载类的方式。
使用自定义类加载器获取到的Class对象通过newInstance()获取实例,要比较具有相同类全限定名的两个Class对象是否是同一个,取决于是否是同一类加载器加载了它们,也就是调用defineClass()
的那个类加载器,而非之前委派的类加载器。
java.lang.Class
对象的方法Class<?> forName(……)
这是手动加载类的常见方式,在Class
类中有两个重载:
public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
这儿可能要有疑问了,第一个方法默认使用哪个类加载器来加载的呢?我们来看下实现:
public static Class<?> forName(String className)
throws ClassNotFoundException {
// 使用native方法获取调用类的Class对象
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
其中getClassLoader(caller)
设置了所使用的类加载器,继续看其实现:
static ClassLoader getClassLoader(Class<?> caller) {
if (caller == null) {
return null;
}
return caller.getClassLoader0();
}
}
这段代码的官方注解是“返回caller的类加载器”,即native方法getClassLoader0()
返回调用者的类加载器。也就是说假设在A类里执行forName(String className)
,那么所使用的ClassLoader就是加载A的ClassLoader。
提示
forName0()
本质还是调用ClassLoader
的loadClass()
来加载类。
ClassLoader getClassLoader()
该方法用于获取加载某Class对象的类加载器,可是通过实例或类对象来获取:
(new A()).getClass().getClassLoader()
A.class.getClassLoader()
反射得到Class对象后通过以下方法获取类信息:
Field[] getDeclaredFields()
Class[] getDeclaredClasses()
Method[] getDeclaredMethods()
等等
详情可查阅javadoc或查看源码
java.lang.ClassLoader
对象的方法ClassLoader getParent()
获取父ClassLoader
Class loadClass(String)
显式调用该方法来进行类加载,传入类全限定名
URL getResource(String)
获取具有给定名称的资源定位符。资源可以是任何数据,名称须以“/”分离路径名。实际调用findResource()
方法,该方法无实现,需子类继承实现。
InputStream getResourceAsStream(String)
获取可以读取资源的InputStream输入流,实际上就是用上面的方法获取到URL后调用url.openStream()
得到 InputStream。
ClassLoader getSystemClassLoader()
这是一个静态方法,通过ClassLoader.getSystemClassLoader()
便可获取到系统类加载器AppClassLoader, 和调用类无关。具体实现见最后一小节。
ClassLoader
只是一个抽象类,很多方法是空的需要自己去实现,比如 findClass()
、findResource()
等。而java提供了java.net.URLClassLoader
这个实现类,适用与多种应用场景。
首先之前提到的AppClassLoader
、ExtClassLoader
都是URLClassLoader
的子类,自定义类加载器推荐直接继承它。
来看下javadoc中的描述:
该类加载器用于从一组URL路径(指向JAR包或目录)中加载类和资源。约定使用以 ‘/’结束的URL来表示目录。如果不是以该字符结束,则认为该URL指向一个JAR文件。
URLClassLoader
接受一个URL数组为参数,它将在这些提供的路径下加载所需要的类,对应的主要构造函数有
public URLClassLoader(URL[] urls)
URLClassLoader(URL[] urls, ClassLoader parent)
getURLs()
方法使用URL[] getURLs()
方法可以获取URL路径,参考代码:
public static void main(String[] args) {
URL[] urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
for (URL url : urls) {
System.out.println(url);
}
}
// file:/D:/Workbench/Test/bin/
在findClass()
中其使用了URLClassPath
类中的Loader
类来加载类文件和资源。URLClassPath
类中定义了两个Loader
类的实现,分别是FileLoader
和JarLoader
类,顾名思义前者用于加载目录中的类和资源,后者是加载jar包中的类和资源。Loader
类默认已经实现getResource()
方法,即从网络URL地址加载jar包然后使用JarLoader
完成后续加载,而两个实现类不过是重写了该方法。
你们可能要问如何URLClassPath
是如何选择使用正确的Loader
的呢?答案是——根据URL格式而定。看下下面删减过的核心代码,简单易懂。
private Loader getLoader(final URL url)
{
String s = url.getFile();
// 以"/"结尾时,若url协议为"file"则使用FileLoader加载本地文件
// 否则使用默认的Loader加载网络url
if(s != null && s.endsWith("/"))
{
if("file".equals(url.getProtocol()))
return new FileLoader(url);
else
return new Loader(url);
} else {
// 非"/"结尾则使用JarLoader
return new JarLoader(url, jarHandler, lmap);
}
}
getSystemClassLoader()
方法的实现追溯getSystemClassLoader()
的源码可以发现其实质上是通过sun.misc.Launcher
实例获取返回其成员变量loader
的。那这个loader
是何时赋值的呢?我们来看下它的构造函数(删减了不相关的内容):
public Launcher()
{
ExtClassLoader extclassloader;
try
{
// 创建并初始化扩展类加载器ExtClassLoader
extclassloader = ExtClassLoader.getExtClassLoader();
}
catch(IOException ioexception)
{
throw new InternalError("Could not create extension class loader");
}
try
{
// 创建并初始化系统类加载器AppClassLoader,并赋给loader
loader = AppClassLoader.getAppClassLoader(extclassloader);
}
catch(IOException ioexception1)
{
throw new InternalError("Could not create application class loader");
}
// 默认将线程上下文类加载器设置为AppClassLoader
// 相关信息见另一篇博文
Thread.currentThread().setContextClassLoader(loader);
}
可以看到Launcher
初始化时创建生成了ExtClassLoader
和AppClassLoader
,并将线程上下文类加载器默认设置为了AppClassLoader
。虽然没去看jvm的源码,但我推测jvm可能就是通过创建Launcher
实例来完成扩展和系统类加载器的创建的,而启动(Bootstrap)类加载器的创建则是另外调用本地方法完成的。
很明显,getSystemClassLoader()
返回的loader就是AppClassLoader
无误,这儿我们也发现了线程上下文类加载器赋值处,具体有关线程上下文类加载器的学习请参考另一篇博文。
通常需要你自己写类加载器的场景不多,但通过上述对类加载器的分析研究至少可以让你了解jvm的底层实现机制以及熟悉反射的实现方式。我个人的风格就是知其然知其所以然,在我理解范围内的知识我都有兴趣去研究。之前总是花一整段时间去啃下难点后就置之不理了,工作后才养成这种常记笔记的习惯,自己总结梳理后的确比看别人的文章要来得更深刻更透彻,望继续保持!
标签:
原文地址:http://blog.csdn.net/yangcheng33/article/details/52464898