Home >Java >javaTutorial >An in-depth look at Java class loaders
Class loader (class loader) is a very important concept in Java™. The class loader is responsible for loading the byte code of Java classes into the Java virtual machine. This article first introduces the basic concepts of Java class loaders in detail, including proxy mode, the specific process of loading classes and thread context class loaders, etc., and then introduces how to develop your own class loader. loader, and finally introduces the application of class loader in Web containers and OSGi™.
Develop and deploy your next app on the IBM Bluemix cloud platform.
Start your trial
The class loader is an innovation of the Java language and one of the important reasons for the popularity of the Java language. It allows Java classes to be dynamically loaded into the Java virtual machine and executed. Class loaders have been around since JDK 1.0 and were originally developed to meet the needs of Java Applets. Java Applet needs to download Java class files from remote to the browser and execute them. Class loaders are now widely used in web containers and OSGi. Generally speaking, developers of Java applications do not need to interact directly with class loaders. The default behavior of the Java virtual machine is sufficient to meet the needs of most situations. However, if you encounter a situation where you need to interact with the class loader, and you don't know much about the mechanism of the class loader, it is easy to spend a lot of time Debugging ClassNotFoundException and NoClassDefFoundError and other exceptions. This article will introduce Java's class loader in detail to help readers deeply understand this important concept in the Java language. Below we first introduce some related basic concepts.
Basic concept of class loader
As the name suggests, the class loader (class loader) is used to load Java classes into the Java virtual machine. Generally speaking, the Java virtual machine uses Java classes as follows: Java source programs (.java files) are converted into Java byte codes (.class files) after being compiled by a Java compiler. The class loader is responsible for reading Java byte code and converting it into an instance of the java.lang.Class class. Each such instance represents a Java class. An object of this class can be created through the newInstance() method of this instance. The actual situation may be more complicated. For example, Java byte code may be dynamically generated through tools or downloaded through the network.
Basically all class loaders are an instance of the java.lang.ClassLoader class. This Java class is described in detail below.
java.lang.ClassLoader**Class introduction**
The basic responsibility of the java.lang.ClassLoader class is to find or generate the corresponding byte code based on the name of a specified class. , and then define a Java class from these byte codes, that is, an instance of the java.lang.Class class. In addition, ClassLoader is also responsible for loading the resources required by Java applications, such as image files and configuration files, etc. However, this article only discusses its function of loading classes. In order to complete the responsibility of loading classes, ClassLoader provides a series of methods, the more important methods are shown in Table 1. Details on these methods are described below.
Table 1. Methods related to loading classes in ClassLoader
Method description
getParent()
Return the parent class loader of this class loader.
loadClass(String name)
Load the class named name, and the returned result is an instance of the java.lang.Class class.
findClass(String name)
Find the class named name, and the returned result is an instance of the java.lang.Class class.
findLoadedClass(String name)
Find the loaded class named name, and the returned result is an instance of the java.lang.Class class.
defineClass(String name, byte[] b, int off, int len)
Convert the contents of the byte array b into a Java class and return the result Is an instance of the java.lang.Class class. This method is declared final.
resolveClass(Class6b3d0130bba23ae47fe2b8e8cddf0195 c)
Link the specified Java class.
For the methods given in Table 1, the value of the name parameter representing the class name is the binary name of the class. What needs to be noted is the representation of internal classes, such as com.example.Sample$1 and com.example.Sample$Inner. These methods will be further explained below when the working mechanism of the class loader is introduced. The following describes the tree-like organizational structure of the class loader.
Tree organizational structure of class loader
Class loaders in Java can be roughly divided into two categories, one is provided by the system, and the other is written by Java application developers. There are three main class loaders provided by the system:
bootstrap class loader: It is used to load the core library of Java and is implemented in native code. Inherited from java.lang.ClassLoader. Extensions class loader: It is used to load Java extension libraries. The implementation of the Java virtual machine provides a directory of extension libraries. The class loader looks for and loads Java classes in this directory. System class loader (system class loader): It loads Java classes according to the class path (CLASSPATH) of the Java application. Generally speaking, Java application classes are loaded by it. It can be obtained through ClassLoader.getSystemClassLoader().
In addition to the class loaders provided by the system, developers can implement their own class loaders by inheriting the java.lang.ClassLoader class to meet some special needs.
other
Java classes, is also loaded by the class loader. Generally speaking, the parent class loader of a class loader written by a developer is the system class loader. Class loaders are organized in this way to form a tree structure. The root node of the tree is the boot class loader. Figure 1 shows a typical class loader tree organizational structure diagram, in which the arrow points to the parent class loader. Figure 1. Classloader tree organizational structure diagram
##classload_tree.png
Listing 1. Demo class loader tree organizationpublic class ClassLoaderTree {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
Each Java class maintains a pointer to the class loader that defines it
Reference
Listing 2. Running results demonstrating the tree organization structure of the class loader sun.misc.Launcher$AppClassLoader@9304b1 sun.misc.Launcher$ExtClassLoader@190d11
As shown in code listing 2, the first output is the class loader of the ClassLoaderTree class, which is the system class loader. It is an instance of the sun.misc.Launcher$AppClassLoader class; the second output is the extension class loader, which is an instance of the sun.misc.Launcher$ExtClassLoader class. It should be noted that the boot class loader is not output here. This is because some JDK implementations return null when the parent class loader is the boot class loader. After understanding the tree-like organizational structure of the class loader, the following introduces the proxy mode of the class loader.
Proxy mode of class loader
When a class loader tries to find the byte code of a class by itself and defines it, it will first proxy to its parent class loader, and the parent class loader will The class loader first tries to load this class, and so on. Before introducing the motivation behind the proxy pattern, we first need to explain how the Java virtual machine determines that two Java classes are the same. The Java virtual machine not only looks at whether the full names of the classes are the same, but also whether the class loaders that load this class are the same. Two classes are considered to be the same only if both are the same. Even if the same byte code is loaded by different class loaders, the classes obtained are different. For example, a Java class com.example.Sample generates a byte code file Sample.class after compilation. Two different class loaders, ClassLoaderA and ClassLoaderB, respectively read this Sample.class file and define two instances of the java.lang.Class class to represent this class. These two instances are not identical. To the Java virtual machine, they are different classes. Attempting to assign objects of these two classes to each other will throw a runtime exception ClassCastException. The following is explained in detail through examples. The Java class com.example.Sample is given in Code Listing 3.
List 3. com.example.Sample class
package com.example; public class Sample { private Sample instance; public void setSample(Object instance) { this.instance = (Sample) instance; } }
如 代码清单 3所示,com.example.Sample类的方法 setSample接受一个 java.lang.Object类型的参数,并且会把该参数强制转换成com.example.Sample类型。测试 Java 类是否相同的代码如 代码清单 4所示。
清单 4. 测试 Java 类是否相同
public void testClassIdentity() { String classDataRootPath = "C:\workspace\Classloader\classData"; File SystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath); FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath); String className = "com.example.Sample"; try { Class<?> class1 = fscl1.loadClass(className); Object obj1 = class1.newInstance(); Class<?> class2 = fscl2.loadClass(className); Object obj2 = class2.newInstance(); Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class); setSampleMethod.invoke(obj1, obj2); } catch (Exception e) { e.printStackTrace(); } }
代码清单 4中使用了类 FileSystemClassLoader的两个不同实例来分别加载类 com.example.Sample,得到了两个不同的java.lang.Class的实例,接着通过 newInstance()方法分别生成了两个类的对象 obj1和 obj2,最后通过 Java 的反射 API 在对象 obj1上调用方法 setSample,试图把对象 obj2赋值给 obj1内部的 instance对象。代码清单 4的运行结果如 代码清单 5所示。
清单 5. 测试 Java 类是否相同的运行结果
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more
从 代码清单 5给出的运行结果可以看到,运行时抛出了 java.lang.ClassCastException异常。虽然两个对象 obj1和 obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。
了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到,后面会详细介绍。
下面具体介绍类加载器加载类的详细过程。
加载类的过程
在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是java.lang.NoClassDefFoundError异常。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
下面讨论另外一种类加载器:线程上下文类加载器。
线程上下文类加载器
The thread context class loader (context class loader) was introduced starting with JDK 1.2. The methods getContextClassLoader() and setContextClassLoader(ClassLoader cl) in class java.lang.Thread are used to obtain and set the context class loader of the thread. If it is not set through the setContextClassLoader(ClassLoader cl) method, the thread will inherit the context class loader of its parent thread. The context class loader for the initial thread that a Java application runs on is the system class loader. Code running in a thread can load classes and resources through this type of loader.
The proxy mode of the class loader mentioned above cannot solve all the problems of class loaders that will be encountered in Java application development. Java provides many service provider interfaces (Service Provider Interface, SPI), allowing third parties to provide implementations for these interfaces. Common SPIs include JDBC, JCE, JNDI, JAXP and JBI. These SPI interfaces are provided by the Java core library. For example, the SPI interface definition of JAXP is included in the javax.xml.parsers package. These SPI implementation codes are likely to be included as jar packages that Java applications depend on, and can be found through the class path (CLASSPATH), such as the jar package included in Apache Xerces that implements JAXP SPI. The code in the SPI interface often needs to load specific implementation classes. For example, the newInstance() method in the javax.xml.parsers.DocumentBuilderFactory class in JAXP is used to generate a new instance of DocumentBuilderFactory. The real class of the instance here is inherited from javax.xml.parsers.DocumentBuilderFactory, provided by the SPI implementation. For example, in Apache Xerces, the implemented class is org.apache.xerces.jaxp.DocumentBuilderFactoryImpl. The problem is that the SPI interface is part of the Java core library and is loaded by the boot class loader; the Java classes implemented by SPI are generally loaded by the system class loader. The boot class loader cannot find the SPI implementation class because it only loads Java's core library. It also cannot proxy to the system class loader because it is the ancestor class loader of the system class loader. In other words, the proxy mode of the class loader cannot solve this problem. The thread context class loader just solves this problem. If no settings are made, the context class loader of the thread of the Java application defaults to the system context class loader. By using the thread context class loader in the SPI interface code, you can successfully load the SPI implementation class. The thread context class loader is used in many SPI implementations. The following introduces another method of loading classes: Class.forName.
Class.forName
Class.forName is a static
method that can also be used to load classes. This method has two forms: Class.forName(String name, boolean initialize, ClassLoader loader) and Class.forName(String className). The first form of parameter name represents the full name of the class; initialize represents whether to initialize the class; loader represents the class loader used when loading. The second form is equivalent to setting the value of the parameter initialize to true, and the value of loader to the class loader of the current class. A very common use of Class.forName is when loading the databasedriver. For example, Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() is used to load the driver of the Apache Derby database. After introducing the basic concepts related to class loaders, here is how to develop your own class loader.
Although in most cases, the class loader implementation provided by the system by default can meet the needs. But in some cases, you still need to develop your own class loader for your application. For example, your application transmits Java class byte codes through the network. To ensure security, these byte codes are encrypted. At this time, you need your own class loader to read the encrypted byte code from a certain network address, then decrypt and verify it, and finally define the class to be run in the Java virtual machine. The following will illustrate the development of class loaders through two specific examples.
File SystemClass LoaderThe first class loader is used to load Java byte code stored on the file system. The complete implementation is shown in Code Listing 6.
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
如 代码清单 6所示,类 FileSystemClassLoader继承自类 java.lang.ClassLoader。在 表 1中列出的 java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。
类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。
网络类加载器
下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。
类 NetworkClassLoader负责通过网络下载 Java 类字节代码并定义出 Java 类。它的实现与 FileSystemClassLoader类似。在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用 Java 反射 API。另外一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用 Java 反射 API 可以直接调用 Java 类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。网络类加载器的具体代码见 下载。
在介绍完如何开发自己的类加载器之后,下面说明类加载器和 Web 容器的关系。
类加载器与 Web 容器
对于运行在 Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
在介绍完类加载器与 Web 容器的关系之后,下面介绍它与 OSGi 的关系。
Class Loaders and OSGi
OSGi™ is a dynamic module system on Java. It provides developers with a service-oriented and component-based runtime environment and provides a standard way to manage the software life cycle. OSGi has been implemented and deployed on many products and has received widespread support in the open source community. Eclipse is built based on OSGi technology. Every module (bundle) in OSGi contains Java packages and classes. A module can declare the Java packages and classes of other modules that it depends on (via Import-Package), and it can also declare and export (export) its own packages and classes for use by other modules (via Export-Package). . This means that you need to be able to hide and share certain Java packages and classes within a module. This is achieved through OSGi's unique class loader mechanism. Each module in OSGi has a corresponding class loader. It is responsible for loading the Java packages and classes contained in the module itself. When it needs to load a class from the Java core library (packages and classes starting with java), it will proxy to the parent class loader (usually the startup class loader) to complete it. When it needs to load an imported Java class, it delegates to the module that exports the Java class to complete the loading. Modules can also explicitly declare certain Java packages and classes that must be loaded by the parent class loader. Just set the value of the system property
org.osgi.framework.bootdelegation. Suppose there are two modules bundleA and bundleB, both of which have their own corresponding class loaders classLoaderA and classLoaderB. BundleA contains class com.bundleA.Sample, and the class is declared exported, which means it can be used by other modules. bundleB declares that it imports the class com.bundleA.Sample provided by bundleA and contains a class com.bundleB.NewSample that inherits from com.bundleA.Sample. When bundleB starts, its class loader classLoaderB needs to load class com.bundleB.NewSample, which in turn needs to load class com.bundleA.Sample. Since bundleB declares class com.bundleA.Sample to be imported, classLoaderB delegates the work of loading class com.bundleA.Sample to the class loader classLoaderA of bundleA that exports this class. classLoaderA looks for the class com.bundleA.Sample inside its module and defines it. The resulting class com.bundleA.Sample instance can be used by all modules that declare this class. For classes starting with java, they are loaded by the parent class loader. If the system property org.osgi.framework.bootdelegation=com.example.core.* is declared, then the classes in the package com.example.core are completed by the parent class loader. The class loader structure of the OSGi module allows different versions of a class to coexist in the Java virtual machine, bringing great flexibility. However, this difference will also bring some troubles to developers, especially when the module needs to use libraries provided by third parties. Here are some good suggestions:
If a
class library
is used by only one module, put the jar package of the class library in the module and specify it in the Bundle-ClassPath. If a class library is shared by multiple modules, you can create a separate module for this class library and declare the Java packages that other modules need to use as exported. Other module declarations import these classes. If the class library provides an SPI interface and uses the thread context class loader to load the Java class implemented by SPI, the Java class may not be found. If a NoClassDefFoundError exception occurs, first check whether the context class loader of the current thread is correct. The class loader can be obtained through Thread.
current
Thread().getContextClassLoader(). This class loader should be the corresponding class loader for this module. If not, you can first get the class loader corresponding to the module through class.getClassLoader(), and then set the context class loader of the current thread through Thread.currentThread().setContextClassLoader().
The class loader is an innovation in the Java language. It makes it possible to dynamically install
and update software components. This article introduces class loader related topics in detail, including basic concepts, proxy mode, thread context class loader, relationship with web containers and OSGi, etc. When developers encounter exceptions such as ClassNotFoundException and NoClassDefFoundError, they should check the class loader of the class that threw the exception and the context class loader of the current thread to discover the problem. When developing your own class loader, you need to pay attention to coordination with the existing class loader organizational structure.
The above is the detailed content of An in-depth look at Java class loaders. For more information, please follow other related articles on the PHP Chinese website!