什么是Java ClassLoader?
类加载器负责在运行时将Java类动态加载到JVM(Java虚拟机)。而且,它们是JRE(Java运行时环境)的一部分。因此,由于类加载器的缘故,JVM无需了解底层文件或文件系统即可运行Java程序。而且,这些Java类不会一次全部加载到内存中,而是在应用程序需要时加载。当我们编译Java类时,它以字节码的形式将其转换为平台和机器无关的已编译程序,并将其存储为.class文件。之后,当我们尝试使用类时,Java ClassLoader将该类加载到内存中,这些运行时数据在JDK7之前是放在永久代(PermGen),JDK8之后则放在元空间(Metaspace)。
Java中内置了三种类型的内置ClassLoader:
内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器(Bootstrap)。启动类加载器并不是Java类,而其他的加载器则都是Java类,启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。所有类加载器(除了启动类加载器)都被实现为Java类。不过,总归要有一个组件来加载第一个Java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯Java类加载器就是启动类加载器的职责。启动类加载器还会负责加载供JRE正常运行所需要的基本组件。
ClassLoader是怎么加载class文件的?
/**
* A class loader is an object that is responsible for loading classes. The
* class <tt>ClassLoader</tt> is an abstract class. Given the <a
* href="#name">binary name</a> of a class, a class loader should attempt to
* locate or generate data that constitutes a definition for the class. A
* typical strategy is to transform the name into a file name and then read a
* "class file" of that name from a file system.
*
* <p> Every {@link Class <tt>Class</tt>} object contains a {@link
* Class#getClassLoader() reference} to the <tt>ClassLoader</tt> that defined
* it.
*
* <p> <tt>Class</tt> objects for array classes are not created by class
* loaders, but are created automatically as required by the Java runtime.
* The class loader for an array class, as returned by {@link
* Class#getClassLoader()} is the same as the class loader for its element
* type; if the element type is a primitive type, then the array class has no
* class loader.
*
* <p> Applications implement subclasses of <tt>ClassLoader</tt> in order to
* extend the manner in which the Java virtual machine dynamically loads
* classes.
*
* <p> Class loaders may typically be used by security managers to indicate
* security domains.
*
* <p> The <tt>ClassLoader</tt> class uses a delegation model to search for
* classes and resources. Each instance of <tt>ClassLoader</tt> has an
* associated parent class loader. When requested to find a class or
* resource, a <tt>ClassLoader</tt> instance will delegate the search for the
* class or resource to its parent class loader before attempting to find the
* class or resource itself. The virtual machine's built-in class loader,
* called the "bootstrap class loader", does not itself have a parent but may
* serve as the parent of a <tt>ClassLoader</tt> instance.
*
* <p> Class loaders that support concurrent loading of classes are known as
* <em>parallel capable</em> class loaders and are required to register
* themselves at their class initialization time by invoking the
* {@link
* #registerAsParallelCapable <tt>ClassLoader.registerAsParallelCapable</tt>}
* method. Note that the <tt>ClassLoader</tt> class is registered as parallel
* capable by default. However, its subclasses still need to register themselves
* if they are parallel capable. <br>
* In environments in which the delegation model is not strictly
* hierarchical, class loaders need to be parallel capable, otherwise class
* loading can lead to deadlocks because the loader lock is held for the
* duration of the class loading process (see {@link #loadClass
* <tt>loadClass</tt>} methods).
*
* <p> Normally, the Java virtual machine loads classes from the local file
* system in a platform-dependent manner. For example, on UNIX systems, the
* virtual machine loads classes from the directory defined by the
* <tt>CLASSPATH</tt> environment variable.
*
* <p> However, some classes may not originate from a file; they may originate
* from other sources, such as the network, or they could be constructed by an
* application. The method {@link #defineClass(String, byte[], int, int)
* <tt>defineClass</tt>} converts an array of bytes into an instance of class
* <tt>Class</tt>. Instances of this newly defined class can be created using
* {@link Class#newInstance <tt>Class.newInstance</tt>}.
*
* <p> The methods and constructors of objects created by a class loader may
* reference other classes. To determine the class(es) referred to, the Java
* virtual machine invokes the {@link #loadClass <tt>loadClass</tt>} method of
* the class loader that originally created the class.
*
* <p> For example, an application could create a network class loader to
* download class files from a server. Sample code might look like:
*
* <blockquote><pre>
* ClassLoader loader = new NetworkClassLoader(host, port);
* Object main = loader.loadClass("Main", true).newInstance();
* . . .
* </pre></blockquote>
*
* <p> The network class loader subclass must define the methods {@link
* #findClass <tt>findClass</tt>} and <tt>loadClassData</tt> to load a class
* from the network. Once it has downloaded the bytes that make up the class,
* it should use the method {@link #defineClass <tt>defineClass</tt>} to
* create a class instance. A sample implementation is:
*
* <blockquote><pre>
* class NetworkClassLoader extends ClassLoader {
* String host;
* int port;
*
* public Class findClass(String name) {
* byte[] b = loadClassData(name);
* return defineClass(name, b, 0, b.length);
* }
*
* private byte[] loadClassData(String name) {
* // load the class data from the connection
* . . .
* }
* }
* </pre></blockquote>
*
* <h3> <a name="name">Binary names</a> </h3>
*
* <p> Any class name provided as a {@link String} parameter to methods in
* <tt>ClassLoader</tt> must be a binary name as defined by
* <cite>The Java™ Language Specification</cite>.
*
* <p> Examples of valid class names include:
* <blockquote><pre>
* "java.lang.String"
* "javax.swing.JSpinner$DefaultEditor"
* "java.security.KeyStore$Builder$FileBuilder$1"
* "java.net.URLClassLoader$3$1"
* </pre></blockquote>
*
* @see #resolveClass(Class)
* @since 1.0
*/
类加载的过程是一个相当复杂的过程(双亲委派模型,命名空间…),其本质就是根据binary name定位到这个类对应的class文件,名称转换为文件名,然后读取文件系统中该名称的“类文件”到byte数组中,然后调用native方法对其进行加载。下面我们分析一下AppClassLoader 是怎么加载类的(这里不考虑双亲委派,为了方便分析,我精简了一些代码,同时把子类没有重写的重要方法放在这里)
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
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);
}
});
}
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
if (this.ucp.knownToNotExist(var1)) {
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}
static {
ClassLoader.registerAsParallelCapable();
}
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");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
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;
}
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
}
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;
}
}
}
加载类首先调用AppClassLoader的loadClass(),在这个方法中做一下判断处理,然后调用ClassLoader的loadClass方法,首先他调用findLoadedClass判断这个类是否已经加载过了(这里涉及到命名空间的问题,暂时先不讲),然后判断是否有父类加载器(这里不是继承的关系,是包含关系)如果有则调用它的loadClass方法,如果前面都没有获取到这个类,则调用子类重写的findClass方法,findclass首先根据binary name定位到这个类对应的class文件,名称转换为文件名,然后读取文件系统中该名称的“类文件”到byte数组中,然后调用native方法defineClass1对其进行加载。
分析Java中内置了三种类型的内置ClassLoader
到这里我们已经大概知道,类是怎么加载的,下面我们来分析一些Java中内置了三种类型的内置ClassLoader他们分别的**“管辖范围”**,分析过代码我们可以知道(还是以AppClassLoader为例)
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);
}
});
}
上面的代码块是AppClassLoader的构造方法,而System.getProperty(“java.class.path”)这个就是获取系统环境变量,来设定自己需要加载的类的路径。其他两个的设定也相似。
System.out.println(System.getProperty("sun.boot.class.path")); // BootstrapClassLoader
System.out.println(System.getProperty("java.ext.dirs")); // ExtensionClassLoader
System.out.println(System.getProperty("java.class.path")); //AppClassLoader
/*C:\Java\jdk1.8.0_211\jre\lib\resources.jar;C:\Java\jdk1.8.0_211\jre\lib\rt.jar;C:\Java\jdk1.8.0_211\jre\lib\sunrsasign.jar;C:\Java\jdk1.8.0_211\jre\lib\jsse.jar;C:\Java\jdk1.8.0_211\jre\lib\jce.jar;C:\Java\jdk1.8.0_211\jre\lib\charsets.jar;C:\Java\jdk1.8.0_211\jre\lib\jfr.jar;C:\Java\jdk1.8.0_211\jre\classes
C:\Java\jdk1.8.0_211\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
C:\Java\jdk1.8.0_211\jre\lib\charsets.jar;C:\Java\jdk1.8.0_211\jre\lib\deploy.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\access-bridge-64.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\cldrdata.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\dnsns.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\jaccess.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\jfxrt.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\localedata.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\nashorn.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\sunec.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\sunjce_provider.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\sunmscapi.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\sunpkcs11.jar;C:\Java\jdk1.8.0_211\jre\lib\ext\zipfs.jar;C:\Java\jdk1.8.0_211\jre\lib\javaws.jar;C:\Java\jdk1.8.0_211\jre\lib\jce.jar;C:\Java\jdk1.8.0_211\jre\lib\jfr.jar;C:\Java\jdk1.8.0_211\jre\lib\jfxswt.jar;C:\Java\jdk1.8.0_211\jre\lib\jsse.jar;C:\Java\jdk1.8.0_211\jre\lib\management-agent.jar;C:\Java\jdk1.8.0_211\jre\lib\plugin.jar;C:\Java\jdk1.8.0_211\jre\lib\resources.jar;C:\Java\jdk1.8.0_211\jre\lib\rt.jar;C:\Users\7\Desktop\Spirit_wolf-jvm-video-zhanglong-master\jvm-video-zhanglong\code\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2018.1.8\lib\idea_rt.jar
*/
双亲委托模型
- 一个AppClassLoader查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
递归,重复第1部的操作。 - 如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找。
- Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。
- ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。
类加载双亲委托模型的好处:
- 可以确保Java核心库的类型安全:所有的Java应用都至少会引用Java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载到Java虚拟机中,如果这个加载过程是由Java应用自己的类加载器所完成的,那么很有可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。
- 可以确保Java核心类库所提供的类不会被自定义的类所替代。
- 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中得到了实际应用。
Java中有一个SPI(Service Provider Interface)标准,使用了SPI的库,比如JDBC,JNDI等,我们都知道JDBC需要第三方提供的驱动才可以,而驱动的jar包是放在我们应 用程序本身的classpath的,而jdbc 本身的api是jdk提供的一部分,它已经被bootstrp加载了,那第三方厂商提供的实现类怎么加载呢?这里面JAVA引入了线程上下文类加载的概念,线程类加载器默认会从父线程继承,如果没有指定的话,默认就是系统类加载器(AppClassLoader),这样的话当加载第三方驱动的时候,就可 以通过线程的上下文类加载器来加载。
线程上下文类加载器
Thread类中有getContextClassLoader()和setContextClassLoader(ClassLoader cl)方法用来获取和设置上下文类加载器,如果没有setContextClassLoader(ClassLoader cl)方法通过设置类加载器,那么线程将继承父线程的上下文类加载器,如果在应用程序的全局范围内都没有设置的话,那么这个上下文类加载器默认就是应用程序类加载器(Application ClassLoader),换句话说Java默认的线程上下文类加载器就是应用程序类加载器(AppClassLoader)。通过线程上下文来加载第三方库jndi实现,而不依赖于双亲委派。大部分Java Application服务器(jboss, tomcat…)也是采用contextClassLoader来处理web服务。(关于线程上下文类加载器推荐
)
命名空间
命名空间是由该类加载器以及其父类加载器所构成的,其中父类加载器加载的类对其子类可见,但是反过来子类加载的类对父类不可见,同一个命名空间中一定不会出现同一个类(全限定名一模一样的类)多个Class对象,换句话说就是在同一命名空间中只能存在一个Class对象,所以当你听别人说在内存中同一类的Class对象只有一个时其实指的是同一命名空间中。JVM在判定两个class是否相同时候,不仅要判断两个类是否相同(equals,hashCode,全限定名…),还要判断该类是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才会认为这两个class是相同。
参考资料: