1.ClassLoader 做什么的?
ClassLoader是用来加载Class文件。他负责将Class的字节码形式转换成内存形式的Class对象。Class的字节码文件可以来自磁盘文件*.class,也可以是jar包里面的*.class,或者使网络上的字节流。其加载的本质是byte[ ]形式的字节数组。
类加载器主要有两种,一种是jvm自带的类加载器,另一种用户继承了抽象类java.lang.ClassLoader
来自定义加载器。通常自定义加载器作用除了加载某些特性的文件夹,或者对class文件进行解密后在加载进内存中。
每个Class对象内部都有一个classLoader字段来标识自己是由哪个加载器加载的。
public final class Class<T>{
// ....
// Initialized in JVM not by private constructor
// This field is filtered from reflection access, i.e. getDeclaredField
// will throw NoSuchFieldException
private final ClassLoader classLoader;
}
2.延迟加载
jvm运行不是一次性的把类都加载到内存中,他是按需加载,也就是延迟加载。程序在运行的过程中,会逐步遇到很多不认识的新类,这时候就会调用ClassLoader来加载这些类。加载完成后,就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。
比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。
3.类加载器
jvm提供了3个类加载器:
- 启动(BootStrap)类加载器:负责加载
<Java_Home>/lib
下面的核心类库,或者-Xbootclasspath
选项指定的jar包,主要是 JVM 运行时核心类,这些类位于JAVA_HOME/lib/rt.jar
文件中,我们常用内置库 java.xxx.* 都在里面,比如java.util.*、java.io.*、java.nio.*、java.lang.*
等等。这个类加载器是内嵌与jvm中的机器码,由c++完成,启动类加载器不存在java对象,用null表示。 - 扩展(Extension)类加载器:负责加载
<Java_Home>/lib/ext
或者由系统变量-Djava.ext.dir
指定的位置。加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中 - 系统(System)类加载器:负责加载系统类路径-classpath或
-Djava.class.path
变量所指的目录下的类库。程序可以访问并使用系统类加载器。
那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。
4.双亲委派类加载机制
双亲委派类加载机制是当某个类需要被加载时,当前类加载器,委派给父类加载器,如果父类加载器还有父类加载器,则继续向上委托,从最上的父类去查找该类,尝试加载,如果加载不到,则子类加载,直到最初的类加载器,如果任然查找不到,则会抛出classnotFoundException
。注意,他们之间不是继承继承关系,是一种组合关系,子加载器中含有父类加载器的引用。
双亲委托机制实现原理(递归加载类)
一个将类class文件形式,加载到内存class表现的流程
public static void main(String[] args) throws ClassNotFoundException {
//获取系统默认的类加载器(SystemClassLoader)
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
//输入class的限定名
Class<?> clazz = classLoader.loadClass("bean.Person");
}
loadClass()
是抽象类ClassLoader
中的类加载的核心方法.
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) {
//递归调用,parent中还含有父加载器,实现了双亲委托机制
c = parent.loadClass(name, false);
} else {//父加载器为null,表示是根加载器,直接调用根加载器本地方法加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//吃掉父加载器加载不到的异常,如果父加载器加载不到
}
if (c == null) {
long t1 = System.nanoTime();
//如果c任然为空,表示父加载加载不到,在自己去加载
// findClass找到class文件后将调用defineClass方法把字节码导入方法区,同时缓存结果
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();
}
}
// 是否解析,默认false
if (resolve) {
resolveClass(c);
}
return c;
}
}
可以看到双亲委托机制其实就是下面代码,通过递归调用父加载,去尝试加载类,加载不到,吃掉异常,继续让子加载器去加载。
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
1. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
2. 类加载后将进入连接(link)阶段,它包含验证、准备、解析,resolve参数决定是否执行解析阶段,
jvm规范并没有严格指定该阶段的执行时刻
3. 由于先使用findLoadedClass()查找缓存,相同的类只会被加载一次
5.用户自定义类加载器
用户通过继承ClassLoader类,重写里面的findClass()方法,自定义加载类的class文件,即可完成
自定义类加载器的操作。通常,构造方法可以指定父加载,默认的加载器是系统类加载器。
// 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;
//默认使用系统类加载器
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
当实现自定义类加载器时不应重写loadClass()
,除非你不需要双亲委派机制。要重写的是findClass()
的逻辑,也就是寻找并加载类的方式。
使用自定义类加载器获取到的Class对象需通过newInstance()
获取实例,要比较具有相同类全限定名的两个Class对象是否是同一个,取决于是否是同一类加载器加载了它们,也就是调用defineClass()
的那个类加载器,而非之前委派的类加载器。
5.1 常用方法分析
1. java.lang.Class
-
Class<?> forName(……)
这是手动加载类的常见方式,在Class类中有两个重载
//使用调用者的加载器去加载文件
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
//类名,是否初始化,指定类加载器加载
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
2.java.lang.ClassLoader
ClassLoader getParent();//获取parent加载器
Class loadClass(String);//加载类,传入类的限定名
URL getResource(String);
获取具有给定名称的资源定位符。资源可以是任何数据,名称须以“/”分离路径名。实际调用findResource()
方法,该方法无实现,需子类继承实现。InputStream getResourceAsStream(String);
获取可以读取资源的InputStream输入流,实际上就是用上面的方法获取到URL后调用url.openStream()
得到 InputStream。
5.2 在自定义类加载器之前先去分析下ExtClassLoa和SystemClassLoader的实现
这两个类是sun.misc.Launcher
的内部类,两者都继承了Classloader
,关系图如下:
Launcher.AppClassLoader
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
//在Launcher类初始化的时候,会调用这个方法,获取AppClassLoader
//extcl是扩展类加载器,先加载完扩展类加载器,作为AppClassLoader的父加载器
public static ClassLoader getAppClassLoader(final ClassLoader extcl)
throws IOException
{
final String s = System.getProperty("java.class.path");
//通过系统环境变量,获取Classpath路径
final File[] path = (s == null) ? new File[0] : getClassPath(s);
// Note: on bugid 4256530
// Prior implementations of this doPrivileged() block supplied
// a rather restrictive ACC via a call to the private method
// AppClassLoader.getContext(). This proved overly restrictive
// when loading classes. Specifically it prevent
// accessClassInPackage.sun.* grants from being honored.
//
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
//将获取的路径,封装成URL,创建对象。
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
/*
* Creates a new AppClassLoader
* 创建一个新的类加载器,通过调用父类构造方法
*/
AppClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent, factory);
}
/**
* 系统类加载器,重写了loadClass方法,也是调用父类(URLClassLoader )的方法
*/
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
//调用父类的构造方法
return (super.loadClass(name, resolve));
}
}
通过AppClassLoader
发现,主要实现在URLClassLoader
中,包括创建系统类加载器实例和loadClass(),findClass()
的实现。ClassLoader
只是一个抽象类,很多方法是空的需要自己去实现,比如 findClass()、findResource()
等。而java提供了java.net.URLClassLoader
这个实现类,适用于多种应用场景。
该类加载器用于从一组URL路径(指向JAR包或目录)中加载类和资源。约定使用以 ‘/’结束的URL来表示目录。
如果不是以该字符结束,则认为该URL指向一个JAR文件。
之前提到的AppClassLoader、ExtClassLoader都是URLClassLoader的子类,自定义类加载器推荐直接继承它。
URLClassLoader
使用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);
}
}
/* The search path for classes and resources */
private final URLClassPath ucp;
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();
}
acc = AccessController.getContext();
//创建一个ucp,负责搜索加载路径下的资源,不同加载器会创建不同的
ucp = new URLClassPath(urls, factory, acc);
}
//实现了父类尚未实现的方法,根据name,去加载路径下查找资源
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");
//用ucp查找加载路径下的资源,由于这个URLClassLoader是两个类加载器的基类,
//所以ucp对于不同的参数,会有不同的实现。
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//找到了,就加载类
return defineClass(name, res);
} catch (IOException e) {
//name不在加载路径下
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;
}
上述是讲解关于系统类加载器是如何被创建出来了,已经实现的机制。通过上面源码,分析了双亲委托机制是如何实现了(递归调用父加载的loadClass(String name)
方法)。除了根类加载器,其余的类加载器全部都是继承java.;lang.Classloader
的子类,为便于实现,提供了URLClassLoader
类的支持,这个类中实现了一些常用的方法,比如URL findResource(final String name);
负责查找资源。因为URLClassLoader
要负责加载每个加载器的加载目录,所以,该类为每个加载器提供了一个URLClassPath
对象,这个对象负责类加载的加载路径的维护和资源加载,而URLClassLoader
则是重写findClass()
方法,通过资源加载器的返回结果,判断是否资源存在。