Class Loaders简介
Class Loaders类加载器负责在运行时将Java类动态加载到JVM(Java虚拟机)。此外,它们也是JRE(Java运行时环境)的一部分。因此,由于类加载器的存在,JVM不需要知道底层文件或文件系统就可以运行Java程序。
此外,这些Java类不会一次全部加载到内存中,而是在应用程序需要时加载。这就是类装入器出现的地方。它们负责将类加载到内存中。
在本文中,我们将讨论不同类型的内置类装入器,它们是如何工作的,并介绍我们自己的自定义实现。
内置类加载器的类型
让我们从一个简单的例子开始,学习如何使用各种类加载器加载不同的类:
public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:"
+ PrintClassLoader.class.getClassLoader());
System.out.println("Classloader of Logging:"
+ Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
执行上述方法时,打印:
Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null
正如我们所看到的,这里有三个不同的类装入器;application应用程序、extension扩展和bootstrap引导(显示为null)。
应用程序类加载器加载包含示例方法的类。应用程序或系统类加载器在类路径中加载我们自己的文件。
接下来,扩展one加载日志类。扩展类装入器装入的类是标准核心Java类的扩展。
最后,引导程序加载ArrayList
类。引导或原始类装入器是所有其他类装入器的父类。
但是,我们可以看到最后一个out,对于ArrayList
,它在输出中显示null。这是因为引导类装入器是用本机代码编写的,而不是用Java编写的,因此它不会显示为Java类。由于这个原因,引导类装入器的行为在JVM中会有所不同。
现在让我们更详细地讨论每一个类加载器。
Bootstrap引导类加载器
Java类由Java.lang.ClassLoader
的实例加载。然而,类加载器本身就是类。因此,问题是,谁加载java.lang.ClassLoader
本身?
这就是引导程序或原始类装入器出现的地方。
它主要负责加载JDK内部类,通常是rt.jar
和位于$JAVA_HOME/jre/lib
目录中的其他核心库。此外,引导类加载器充当所有其他类加载器实例的父类。
这个引导类加载器是核心JVM的一部分,它是用本机代码编写的,正如上面的示例所指出的。不同的平台可能有这个特定类加载器的不同实现。
Extension扩展类加载器
Extension扩展类加载器是引导类加载器的子类,负责加载标准核心Java类的扩展,以便平台上运行的所有应用程序都可以使用它。
扩展类加载器从JDK扩展目录加载,通常是$JAVA_HOME/lib/ext
目录或JAVA.ext.dirs
系统属性中提到的任何其他目录。
System系统类加载器
另一方面,系统或应用程序类装入器负责将所有应用程序级类装入JVM。它加载在classpath环境变量,-classpath
或-cp
命令行选项中找到的文件。此外,它是扩展类加载器的子级。
类加载器是如何工作的?
类加载器是Java运行时环境的一部分。当JVM请求一个类时,类加载器尝试定位该类,并使用完全限定的类名将类定义加载到运行时。
java.lang.ClassLoader.loadClass()
方法负责将类定义加载到运行时。它尝试基于完全限定名加载类。
如果类尚未加载,它会将请求委托给父类加载器。这个过程是递归发生的。
最后,如果父类装入器没有找到该类,则子类将调用java.net.URLClassLoader.findClass()
方法来查找文件系统本身中的类。
如果最后一个子类加载器也无法加载该类,则会抛出java.lang.NoClassDefFoundError或java.lang.ClassNotFoundException。
让我们看一个抛出ClassNotFoundException
时的输出示例。
java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
如果我们从调用java.lang.Class.forName()
开始查看事件序列,我们可以理解它首先尝试通过父类装入器装入类,然后通过java.net.URLClassLoader.findClass()
查找类本身。
当它仍然找不到类时,它抛出ClassNotFoundException
。
类装入器/加载器有三个重要特性:
委托模型
类加载器遵循委托模型,在请求查找类或资源时,类加载器实例将把类或资源的搜索委托给父类加载器。
假设我们有一个将应用程序类加载到JVM的请求。系统类加载器首先将该类的加载委托给其父扩展类加载器,而父扩展类加载器又将其委托给引导类加载器。
只有当引导程序和扩展类加载器加载类失败时,系统类加载器才会尝试加载类本身。
唯一类
作为委托模型的结果,由于我们总是尝试向上委托,所以很容易确保类的唯一性。
如果父类装入器无法找到该类,则只有当前实例才会自己尝试这样做。
可见度
此外,子类加载器对其父类加载器加载的类可见。
例如,系统类加载器加载的类可以看到扩展类加载器和引导类加载器加载的类,但反之亦然。
为了说明这一点,如果类A由应用程序类加载器加载,而类B由扩展类加载器加载,那么就应用程序类加载器加载的其他类而言,A和B类都是可见的。
然而,就扩展类装入器装入的其他类而言,类B是唯一可见的类。
自定义类加载器
内置的类加载器在文件已经在文件系统中的大多数情况下就足够了。
但是,在需要从本地硬盘或网络加载类的场景中,我们可能需要使用自定义类加载程序。
在本节中,我们将介绍自定义类装入器的一些其他用例,并演示如何创建一个。
自定义类装入器用例
自定义类加载器不仅有助于在运行时加载类,还有一些用例可能包括:
1. 帮助修改现有字节码,例如agent代理
2. 动态创建适合用户需求的类。在JDBC中,不同驱动程序实现之间的切换是通过动态类加载完成的。
3. 实现类版本控制机制,同时为具有相同名称和包的类加载不同的字节码。这可以通过URL类加载器(通过URL加载JAR)或自定义类加载器来完成。
有更多具体的例子可以让自定义类加载器派上用场。
例如,浏览器使用自定义类加载器从网站加载可执行内容。浏览器可以使用单独的类加载器从不同的网页加载小程序。用于运行小程序的小程序查看器包含一个类加载器,它可以访问远程服务器上的网站,而不是查看本地文件系统。
然后通过HTTP加载原始字节码文件,并将它们转换为JVM内的类。即使这些小程序具有相同的名称,如果由不同的类加载器加载,它们也会被视为不同的组件。
既然我们理解了为什么自定义类加载器是相关的,那么让我们实现ClassLoader的一个子类来扩展和总结JVM如何加载类的功能。
创建自定义类加载器
为了便于说明,假设我们需要使用自定义类装入器从文件中装入类。
我们需要扩展ClassLoader类并重写findClass()
方法:
public class CustomClassLoader extends ClassLoader {
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
byteStream.write(nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
}
在上面的示例中,我们定义了一个自定义类加载器,该加载器扩展了默认类加载器,并从指定的文件加载一个字节数组。
理解java.lang.ClassLoader
让我们讨论java.lang.ClassLoader
类中的几个基本方法,以更清楚地了解其工作原理。
loadClass()方法
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
此方法负责加载给定名称参数的类。name参数引用完全限定的类名。
Java虚拟机调用loadClass()
方法来解析类引用,并将resolve
设置为true
。但是,并不总是需要解析类。如果我们只需要确定类是否存在,那么resolve参数设置为false。
此方法用作类装入器的入口点。
我们可以从java.lang.ClassLoader
的源代码中了解loadClass()
方法的内部工作:
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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
该方法的默认实现按以下顺序搜索类:
1. 调用findLoadedClass(String)
方法以查看该类是否已加载。
2. 调用父类加载器上的loadClass(String)
方法。
3. 调用findClass(String)
方法来查找该类。
defineClass()方法
protected final Class<?> defineClass(
String name, byte[] b, int off, int len) throws ClassFormatError
此方法负责将字节数组转换为类的实例。在我们使用这个类之前,我们需要解决它。
如果数据不包含有效的类,它将抛出ClassFormatError
。
此外,我们不能重写此方法,因为它被标记为final
。
findClass()方法
protected Class<?> findClass(
String name) throws ClassNotFoundException
此方法查找具有完全限定名称作为参数的类。我们需要在遵循用于加载类的委托模型的自定义类装入器实现中重写此方法。
此外,如果父类装入器找不到请求的类,loadClass()
将调用此方法。
如果类加载器的父级未找到该类,则默认实现将抛出ClassNotFoundException
。
getParent()方法
public final ClassLoader getParent()
此方法返回委托的父类装入器。
一些实现类似于前面第2节中看到的实现。使用null
表示引导类装入器。
getResource()方法
public URL getResource(String name)
此方法尝试查找具有给定名称的资源。
它将首先委托给资源的父类加载器。如果父级为null
,则搜索内置于虚拟机中的类加载器的路径。
如果失败,那么该方法将调用findResource(String)
来查找资源。指定为输入的资源名可以是相对于类路径的,也可以是绝对的。
它返回用于读取资源的URL对象,如果找不到资源或调用程序没有足够的权限返回资源,则返回null
。
需要注意的是,Java从类路径加载资源。
最后,Java中的资源加载被认为是位置独立的,因为只要环境设置为查找资源,代码在哪里运行并不重要。
Context上下文类加载器
通常,上下文类加载器为J2SE中引入的类加载委派方案提供了一种替代方法。
正如我们之前所了解的,JVM中的类加载器遵循一个分层模型,这样除了引导类加载器之外,每个类加载器都有一个父类。
然而,有时当JVM核心类需要动态加载应用程序开发人员提供的类或资源时,我们可能会遇到问题。
例如,在JNDI中,核心功能由rt.jar
中的引导类实现。但是这些JNDI类可能会加载由独立供应商(部署在应用程序类路径中)实现的JNDI提供程序。此场景要求引导类加载器(父类加载器)加载对应用程序加载器可见的类(子类加载器)。
J2SE委托在这里不起作用,为了解决这个问题,我们需要找到类加载的替代方法。它可以使用线程上下文加载器来实现。
Thread
类有一个方法getContextClassLoader()
,该方法返回特定线程的ContextClassLoader
。ContextClassLoader
由线程的创建者在加载资源和类时提供。
如果未设置该值,则默认为父线程的类装入器上下文。
结论
类加载器对于执行Java程序至关重要。
我们讨论了不同类型的类加载器,即引导、扩展和系统类加载器。Bootstrap充当所有这些类的父类,并负责加载JDK内部类。另一方面,扩展和系统分别从Java扩展目录和类路径加载类。
然后我们讨论了类加载器的工作原理,讨论了一些特性,如委托、可见性和唯一性,然后简要说明了如何创建自定义类加载器。最后,我们介绍了上下文类装入器。
像往常一样,可以在GitHub上找到代码示例:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-jvm
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2423.html
暂无评论