java中的Class装载系统ClassLoader是怎样使用,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
ClassLoader
在Java中有着非常重要的作用,它主要工作是在Class装载
的加载阶段,主要作用是从系统外部获得Class二进制数据流
。
ClassLoader
是java的核心组件,所有的Class都是由ClassLoader
进行加载的,ClassLoader
负责通过各种方式将ClassLoader
在整个装载阶段,只能影响类的加载,而无法通过ClassLoader
改变类的连接和初始化行为。
从代码层面上看,ClassLoader是一个抽象类,它提供了一些重要的接口,用于自定义Class的加载流程和加载方式。ClassLoader的主要方法如下:
public Class<?> loadClass(String name) throws ClassNotFoundException
:给定一个类名,加载一个类,返回代码这个类的Class实例,如果找不到类,则返回ClassNotFoundException
异常。
protected final Class<?> defineClass(byte[] b, int off, int len)
:根据给定的字节码流b定义一个类,off
和len
参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader
从外部获取的。这是一个受保护的方法,只有在自定义ClassLoader
子类中可以使用。
protected Class<?> findClass(String name) throws ClassNotFoundException
:查找一个类,这是一个受保护的方法,也是重载ClassLoader
时重要的系统扩展点。这个方法在loadClass()
中被调用,用于自定义查找类的逻辑。如果不需要修改类加载默认机制,只是想改变类加载的形式,就可以重载该方法。
protected final Class<?> findLoadedClass(String name)
:这也是一个受保护的方法,它会寻找已经加载的类。这个方法是final方法,无法被修改。
在ClassLoader
的结构中,还有一个重要的字段:parnet
。它也是一个ClassLoader
的实例,这个字段所表示的ClassLoader
称为这个ClassLoader
的双亲。在类加载的过程中,ClassLoader
可能会将某些请求交给自己的双亲处理。
ClassLoader
的分类在标准的java程序中,java虚拟机会创建3类ClassLoader
为整个应用程序服务。它们分别是:Bootstrap ClassLoader
(启动类加载器)、Extension ClassLoader
(扩展类加载器)和 App ClassLoader
(应用类加载器,也称系统类加载器)。此外每一个应用程序还可以拥有自定义的 ClassLoader
,以扩展java虚拟机获取Class数据的能力。
ClassLoader层次结构如下图所示。当系统需要使用一个类时,在判断类是否已经被加载时,会从底层类加载器开始进行判断。当系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功。
启动类加载器:完全由c语言实现,并且java中没有对象与之对应,负责加载系统的核心类,比如rt.jar
中的java类。
扩展类加载器:用于加载%JAVA_HOME%/lib/ext/*.jar
中的java类。
应用类加载器:用于加载用户类,也就是用户程序的类。
自定义类加载器:用于加载一些特殊途径的类,一般也是用户程序的类。
下列代码输出了加载的类加载器:
public class Demo04 { public static void main(String[] args) { ClassLoader cl = Demo04.class.getClassLoader(); while (cl != null) { System.out.println(cl.getClass().getName()); cl = cl.getParent(); } } }
代码中先取得装载当前类Demo04
的ClassLoader
,然后打印当前ClassLoader
并获得其双亲,直到类加载器树被遍历完成。运行结果如下:
sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader
由此得知,Demo03
是由AppClasLoader
(应用类加载器)加载的,而AppClassLoader
的双亲为ExtClassLoader
(扩展类加载器)。从ExtClassLoader
无法再取得启动类加载器,因为这是一个系统级的纯C语言实现。因此,任何启动类加载器中加载的类是无法获得其ClassLoader
实例的,比如:
String.class.getClassLoader()
由于String
属于java核心类,会被启动类加载器加载,故以上代码返回的是null
.
系统中的ClassLoader
在协同工作时,默认会使用双亲委托模式。在类加载的时候,系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载。在尝试加载时,会请求双亲处理,如果请求失败,则会自己加载。
以下代码显示了ClassLoader
加载类的详细过程,它在ClassLoader.loadClass()
中实现:
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) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果双亲不为null // 在双亲加载不成功时,抛出ClassNotFoundException } if (c == null) { // 如果双亲加载不成功 // 使用findClass查找类 long t1 = System.nanoTime(); c = findClass(name); // 定义类加载器,记录数据 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
的职责非常明确,但是会带来一个问题:即上层的ClassLoader
无法访问下层的ClassLoader
所加载的类,如下图:
通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中为应用类。按照这种模式,应用类访问系统类自然没问题,但是系统类访问应用类就会出现问题。比如,在系统类中提供了一个接口,该接口需要在应用中得以实现,还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这些就会出现该工厂方法无法创建由应用类加载器的应用实例的问题。
在java平台中,通常把核心类(rt.jar
)中提供外部服务、可由应用层自行实现的接口称为Service Provider Interface
,即SPI
.
下面以javax.xml.parsers
中实现XML文件解析功能模块为例,说明如何在启动类加载中访问由应用类加载器实现的SPI
接口实例。
public static DocumentBuilderFactory newInstance() { return FactoryFinder.find( /* The default property name according to the JAXP spec */ DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory" /* The fallback implementation class name */ "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"); }
FactoryFinder.find()
函数试图加载并返回一个DocumentBuilderFactory
实例。当这个实例在应用层jar包时,它会使用如下方法进行查找:
Object provider = findJarServiceProvider(factoryId);
其中factoryId
就是字条串javax.xml.parsers.DocumentBuilderFactory
,findJarServiceProvider
的主要内容如下代码所示(这段代码并非jdK中的源码,为了展示主要功能,做了删减):
private static Object findJarServiceProvider(String factoryId) throw ConfigurationError { String serviceId = "META-INF/services" + factoryId; InputStream is = null; ClassLoader cl = ss.getContextClassLoader(); InputStream is = ss.getResourceAsStream(cl, serviceId); BufferedReader rd = new BufferedReader(new InputStreamReader(is, "UTF-8")); String factoryClassName = rd.readLine(); return newInterface(factoryClassName, cl, false, useBSClsLoader); }
从以上代码可知,系统通过读取jar包中META-INF/services
目录下的类名文件读取工厂类类名,然后根据类名生成对应的实例,并将此ClassLoader
传入newInstance()
方法,由这个ClassLoader
完成实例的加载和创建,而不是由这段代码所在的启动类加载品加载。从而解决了启动类加载器无法访问factoryClassName
指定类的问题。
以上代码中,加载工厂类方法略有曲折,我们平时写代码时,知道了一个类的包名.类名
,要生成该类的对象,通常是这么进行的:
Class.forname("包名.类名"),拿到Class对象。
拿到Class对象后,调用Class.newInstance()方法,生成该对象的实例。
但是,在DocumentBuilderFactory
中,这样做就行不通了,主要原因在于Class.forName()
无法拿到类加载器。我们来看看Class.forName()
的源码:
public static Class<?> forName(String className) throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }
从上面可以看到,在哪个类里调用了Class.forName()
,就使用加载那个类的类加载器进行类加载,即DocumentBuilderFactory调用了Class.forName()
,就使用加载DocumentBuilderFactory的类加载器进行加载包名.类名
,但问题是DocumentBuilderFactory是由BootClassLoader加载的,获取到的类加载器是null,这是无法加载包名.类名
。
双亲模式的类加载方式是虚拟机默认的行为,但并非必须这么做,通过重载ClassLoader
可以修改该行为。下面将演示如何打破默认的双亲模式:
package jvm.chapter10; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; class MyClassLoader extends ClassLoader { private String fileName; public MyClassLoader(String fileName) { this.fileName = fileName; } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class re = findClass(name); if (re != null) { return re; } System.out.println("load class " + name + " failed, parent load start"); return super.loadClass(name, resolve); } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { Class clazz = this.findLoadedClass(className); if (null == clazz) { try { String classFile = getClassFile(className); FileInputStream fis = new FileInputStream(classFile); FileChannel fileChannel = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel outChannel = Channels.newChannel(baos); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); while (true) { int i = fileChannel.read(buffer); if (i == 0 || i == -1) { break; } buffer.flip(); outChannel.write(buffer); buffer.clear(); } fis.close(); byte[] bytes = baos.toByteArray(); clazz = defineClass(className, bytes, 0, bytes.length); } catch (Exception e) { e.printStackTrace(); } } return clazz; } private String getClassFile(String packageName) { return fileName + packageName.replaceAll("\\.", File.separator) + ".class"; } } /** * {这里添加描述} * * @author chengyan * @date 2019-11-29 4:12 下午 */ public class Demo05 { public static void main(String[] args) throws Exception { MyClassLoader myClassLoader = new MyClassLoader("/Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/"); Class clz = myClassLoader.loadClass("jvm.chapter10.Demo01"); System.out.println(clz.getClassLoader().getClass().getName()); System.out.println("=======class load tree==========="); ClassLoader cl = clz.getClassLoader(); while(cl != null) { System.out.println(cl.getClass().getName()); cl = cl.getParent(); } } }
以上代码通过自定义ClassLoader重载loadClass()方法,改变了默认的委托双亲加载的方式,运行结果如下:
java.io.FileNotFoundException: /Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/java/lang/Object.class (No such file or directory) at java.io.FileInputStream.open0(Native Method) at java.io.FileInputStream.open(FileInputStream.java:195) at java.io.FileInputStream.<init>(FileInputStream.java:138) at java.io.FileInputStream.<init>(FileInputStream.java:93) at jvm.chapter10.MyClassLoader.findClass(Demo05.java:36) at jvm.chapter10.MyClassLoader.loadClass(Demo05.java:22) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.lang.ClassLoader.defineClass(ClassLoader.java:642) at jvm.chapter10.MyClassLoader.findClass(Demo05.java:52) at jvm.chapter10.MyClassLoader.loadClass(Demo05.java:22) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at jvm.chapter10.Demo05.main(Demo05.java:76) load class java.lang.Object failed, parent load start jvm.chapter10.MyClassLoader =======class load tree=========== jvm.chapter10.MyClassLoader sun.misc.Launcher$AppClassLoader sun.misc.Launcher$ExtClassLoader
可以看到,程序首先试图由MyClassLoader
加载Object
类,但由于指定的路径中没有该类信息,故加载失败,抛出异常,但随后就由应用类加载器加载成功。接着尝试加载Demo01
,Demo01
在指定的路径中,加载成功。打印加载Demo01
的ClassLoader
,显示为MyClassLoader
,打印ClassLoader
层次,依次为MyClassLoader
,AppClassLoader
,ExtClassLoader
.
关于java中的Class装载系统ClassLoader是怎样使用问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。