它的作用
它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。
JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。
jvm里ClassLoader的层次结构

BootstrapClassLoader(启动类加载器)
负责加载 JVM 运行时核心类,加载System.getProperty("sun.boot.class.path")所指定的路径或jar
ExtensionClassLoader
负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVAHOME/lib/rt.jar文件中.
加载System.getProperty("java.ext.dirs")所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld。
AppClassLoader
才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld
此外
URLClassLoader
那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。
双亲委派机制
就是优先由父亲来加载,这样可以保证只有一份,不然每个子类都加载,我岂不是不知道哪个是正确的?
源码如下
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 {
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
}
//父亲都加载不到了,那就只能往下找,看下子类是否重写了findClass,这也是双亲委派机制的破坏
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;
}
}
关于双亲委派机制的破坏
也就是不是一直向上的,顶级父类都加载不到,那就只能向下了,即先想上,再向下
ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。
loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。下面我使用伪代码表示一下基本过程.
class ClassLoader {
// 加载入口,定义了双亲委派规则
Class loadClass(String name) {
// 是否已经加载了
Class t = this.findFromLoaded(name);
if(t == null) {
// 交给双亲
t = this.parent.loadClass(name)
}
if(t == null) {
// 双亲都不行,只能靠自己了
t = this.findClass(name);
}
return t;
}
// 交给子类自己去实现
Class findClass(String name) {
throw ClassNotFoundException();
}
// 组装Class对象
Class defineClass(byte[] code, String name) {
return buildClassFromCode(code, name);
}
}
class CustomClassLoader extends ClassLoader {
Class findClass(String name) {
// 寻找字节码
byte[] code = findCodeFromSomewhere(name);
// 组装Class对象
return this.defineClass(code, name);
}
}
注意
不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。
Class.forName()与ClassLoader
/**
* Returns the {@code Class} object associated with the class or
* interface with the given string name, using the given class loader.
* Given the fully qualified name for a class or interface (in the same
* format returned by {@code getName}) this method attempts to
* locate, load, and link the class or interface. The specified class
* loader is used to load the class or interface. If the parameter
* {@code loader} is null, the class is loaded through the bootstrap
* class loader. The class is initialized only if the
* {@code initialize} parameter is {@code true} and if it has
* not been initialized earlier.
大概意思就是你传入一个全路径名,我就给你返回对应的Class对象,但你没有指定类加载器时,我就用bootstrapClassLoader来加载,此外当为true时,如果Class之前还没有初始化,那么Class会立马初始化.
也从侧面说明ClassLoader本身是延时加载的
* <p> If {@code name} denotes a primitive type or void, an attempt
* will be made to locate a user-defined class in the unnamed package whose
* name is {@code name}. Therefore, this method cannot be used to
* obtain any of the {@code Class} objects representing primitive
* types or void.
*
* <p> If {@code name} denotes an array class, the component type of
* the array class is loaded but not initialized.
*
* <p> For example, in an instance method the expression:
*
* <blockquote>
* {@code Class.forName("Foo")}
* </blockquote>
*
* is equivalent to:
*
* <blockquote>
* {@code Class.forName("Foo", true, this.getClass().getClassLoader())}
* </blockquote>
*
* Note that this method throws errors related to loading, linking or
* initializing as specified in Sections 12.2, 12.3 and 12.4 of <em>The
* Java Language Specification</em>.
* Note that this method does not check whether the requested class
* is accessible to its caller.
*
* <p> If the {@code loader} is {@code null}, and a security
* manager is present, and the caller's class loader is not null, then this
* method calls the security manager's {@code checkPermission} method
* with a {@code RuntimePermission("getClassLoader")} permission to
* ensure it's ok to access the bootstrap class loader.
*
* @param name fully qualified name of the desired class
* @param initialize if {@code true} the class will be initialized.
* See Section 12.4 of <em>The Java Language Specification</em>.
* @param loader class loader from which the class must be loaded
* @return class object representing the desired class
*
* @exception LinkageError if the linkage fails
* @exception ExceptionInInitializerError if the initialization provoked
* by this method fails
* @exception ClassNotFoundException if the class cannot be located by
* the specified class loader
*
* @see java.lang.Class#forName(String)
* @see java.lang.ClassLoader
* @since 1.2
*/
@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
ClassLoader loader)
throws ClassNotFoundException
{
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
关于jvm类的加载
在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:
装载:查找和导入类或接口的二进制数据; //byte[]
链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;//包含了虚方法表的初始化
校验:检查导入类或接口的二进制数据的正确性; //魔数 babycafe?
准备:给类的静态变量分配并初始化存储空间; //分配内存空间
解析:将符号引用转成直接引用;
初始化:激活类的静态变量的初始化Java代码和静态Java代码块。
//对应的为<clinit>()方法,该方法在多线环境中如果有多个线程同时去初始化一个类,那么久只有一个线程去执行。
//这也是我们写单例时,为什么可以使用静态内部类了,保证了内部重排序对外部线程时不可见的,具体可以见对DCL的分析
具体的可参见周志明老师的深入理解JAVA虚拟机
小结一下
Class.forName(className)方法,其实调用的方法是Class.forName(className,true,classloader);注意看第2个boolean参数,它表示的意思,在loadClass后必须初始化。在执行过此方法后,目标对象的 static块代码已经被执行,static参数也已经被初始化。
ClassLoader.loadClass(className)方法,其实他调用的方法是ClassLoader.loadClass(className,false);还是注意看第2个 boolean参数,该参数表示目标对象被装载后不进行链接,这就意味这不会去执行该类静态块中间的内容
所以,如果你在静态代码块里面初始化了东西,那么你就应该用Class.forName而不是直接用类加载器去加载
Thread.contextClassLoader
class Thread {
...
private ClassLoader contextClassLoader;
public ClassLoader getContextClassLoader() {
return contextClassLoader;
}
public void setContextClassLoader(ClassLoader cl) {
this.contextClassLoader = cl;
}
...
}
contextClassLoader「线程上下文类加载器」,这究竟是什么东西?
首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它
Thread.currentThread().getContextClassLoader().loadClass(name);
这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。
其次线程的 contextClassLoader 默认是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。
那这个 contextClassLoader 究竟是做什么用的?
它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。
如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。
如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。tomcat和spring就用到了这个
如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。
网友评论