java类加载器
Java类加载器(英语:Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。
JVM中的默认类加载器
JVM中有3个默认的类加载器:
- 引导(Bootstrap)类加载器。由原生代码(C++语言)编写,不继承自
java.lang.ClassLoader
。负责加载JVM自身需要的类,负责将<JAVA_HOME>/jre/lib
路径下的核心类库或-Xbootclasspath
参数指定的路径下的jar包加载到内存中。 - 扩展(Extensions)类加载器。用来在
<JAVA_HOME>/jre/lib/ext
,或java.ext.dirs
中指明的目录中加载 Java的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。该类由sun.misc.Launcher$ExtClassLoader
实现。
//ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
//加载<JAVA_HOME>/lib/ext目录中的类库
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for (int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
- Apps类加载器(也称系统类加载器)。根据 Java应用程序的类路径(
java.class.path
或CLASSPATH环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。该类由sun.misc.Launcher$AppClassLoader
实现,是程序中默认的类加载器。
在Java的日常应用程序开发中,类的加载几乎都是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
双亲委派模式
双亲委派模式的工作原理
双亲委派模式要求除了顶层的引导类加载器外,其余的类加载器都应当有自己的父类加载器,类加载器之间的关系如下:
双亲委派模式的工作原理是,如果一个类加载器收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才尝试去自己加载,这就是双亲委派模式。
双亲委派模式的优势
采用双亲委派模式的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层次关系可以避免类的重复加载,当父ClassLoader已经加载了该类时,就没有必要子ClassLoader再加载一次。
其次是考虑到安全因素,Java核心api中定义的类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer
的类,通过双亲委派模式传递到引导类加载器,而引导类加载器在核心Java API中发现这个名字的类,发现该类已经被加载,并不会重新加载网络传递过来的java.lang.Integer
,而是直接返回已加载过的Integer.class
,这样可以防止核心API库被随意篡改。
类与类加载器
在JVM中表示两个class对象是否为同一个类对象的两个必要条件
- 类的包名和类名必须一致
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
也就是说,在JVM中,即使这两个类对象来源于同一个class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间
,所以加载的class对象存在不同的类名称空间中。
class文件的显示加载和隐式加载
所谓class文件的显示加载与隐式加载是指JVM加载class文件到内存的方式。
显示加载指在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)
或 this.getClass().getClassLoader().loadClass()
加载class对象
隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
编写自己的类加载器
自定义类加载器的用途
- 运行时装载或卸载类。这常用于:
- 改变Java字节码的装入,例如,可用于Java类字节码的加密装入。当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中。
- 修改已装入的字节码
- 热部署
- Tomcat容器,每个WebApp有自己的ClassLoader,加载每个WebApp的ClassPath路径上的类,一旦遇到Tomcat自带的Jar包就委托给CommonClassLoader加载。
- 隔离,比如早些年比较火的Java模块化框架OSGI,把每个Jar包以Bundle的形式运行,每个Bundle有自己的类加载器(不同Bundle可以有相同的类名),Bundle与Bundle之间起到隔离的效果,同时如果一个Bundle依赖了另一个Bundle的某个类,那这个类的加载就委托给导出该类的BundleClassLoader进行加载。
- Android热修复,组件化
实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader则需要自己重写findClass()方法,并编写加载逻辑,继承URLClassLoader则可以省去编写findClass()方法及class文件加载转换成字节码流的代码。
自定义File类加载器
继承ClassLoader
public class FileClassLoader extends ClassLoader {
private String rootDir;
public FileClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的class文件字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
// 直接生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
/**
* 获取class文件,并转换成字节码流
*
* @param name
* @return
*/
private byte[] getClassData(String name) {
String path = getClassPath(name);
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int read = 0;
while ((read = is.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String getClassPath(String name) {
return rootDir + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
}
}
DemoObj.java
import com.oyty.classloader;
public class DemoObj {
@Override
public String toString() {
return "I am demo obj";
}
}
运行代码,输出I am demo obj
,说明DemoObj类被成功加载。需要注意的是如果DemoObj有包路径的话,如本例中com.oyty.classloader
,则编译后的class文件也需要放在包路径的文件夹下。本例中最后class文件的完整路径是/Users/oyty/Documents/newworkspace/idea/classloader/com/oyty/classloader/DemoObj.class
一般情况下,自己开发的类加载只需要覆写findClass(string name)方法即可。java.lang.ClassLoader类的方法loadClass()封装前面提到的委派模式。该方法首先会调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。因此为了保证类加载器都正确实现委派模式,在开发自己的类加载器时,最好不要覆写loadClass()方法,而是覆写findClass()方法。
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.
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;
}
}
继承URLClassLoader
public class FileUrlClassLoader extends URLClassLoader {
public FileUrlClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public FileUrlClassLoader(URL[] urls) {
super(urls);
}
public FileUrlClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
public static void main(String[] args) throws MalformedURLException {
String rootDir = "/Users/oyty/Documents/newworkspace/idea/classloader";
File file = new File(rootDir);
URI uri = file.toURI();
URL[] urls = {uri.toURL()};
FileUrlClassLoader loader = new FileUrlClassLoader(urls);
try {
Class<?> obj = loader.loadClass("com.oyty.classloader.DemoObj");
System.out.println(obj.newInstance().toString());
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
可以知道,当自定义类加载器继承自URLClassLoader,将会非常简洁,无需额外编写findClass()方法和class文件的字节流转换逻辑。
自定义网络类加载器
讲一个网络类加载器的实际用途:通过网络类加载器实现组件的动态更新。基本场景是:Java的字节码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文本即可。
public class NetworkClassLoader extends ClassLoader {
private String rootUrl;
public NetworkClassLoader(String rootUrl) {
this.rootUrl = rootUrl;
}
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 {
URL url = new URL(path);
InputStream ins = url.openStream();
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 (Exception e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootUrl + "/"
+ className.replace('.', '/') + ".class";
}
}
类NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API;另一种做法是使用接口。需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法,而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类,在客户端通过相同的接口来使用这些实现类。
双亲委派模型的破坏者--线程上下文类加载器
待完善......
类加载器与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 应用共享的目录下面。
- 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
参考:
https://zh.wikipedia.org/wiki/Java%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8
https://blog.csdn.net/javazejian/article/details/73413292
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html