概述
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成
java.lang.Class
类的一个实例。每个这样的实例用来表示一个 Java 类。
类从加载到虚拟机到卸载,它的整个生命周期包括:加载(Loading),验证(Validation),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(Unloading)。其中,验证、准备和解析部分被称为连接(Linking)。
加载:
在加载阶段,虚拟机主要完成三件事:
1.通过一个类的全限定名来获取定义此类的二进制字节流。
2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口。
既然如此那么它从哪里获取此类的二进制字节流,可以从JAR、EAR、WAR等格式中读取;从网络中获取;运行时计算生成,动态代理;从其他文件中读取等等。
类加载器:
类的加载由类加载器完成,Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:引导(启动)类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。 除了以上列举的三种类加载器,还有一种比较特殊的类型 — 线程上下文类加载器。
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。
- 双亲委派模型:
- 这么多类加载器,实际加载一个类的时候到底是个什么顺序呢?如下图所示
- 如图所示,这种层次关系,称为类加载器的双亲委派模型。简单来说:就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
- 下面是demo测试类:
- 根据输出日志,可以看出:第一个输出的是 ClassLoaderTree类的类加载器,即系统类加载器。它是 sun.misc.Launcher$AppClassLoader类的实例;第二个输出的是扩展类加载器,是 sun.misc.Launcher$ExtClassLoader类的实例。第三个输出是获取标准扩展类加载器的父类加载器时确得到了null。看图知道应该是引导(启动)类加载器(bootstrap class loader),这个加载器很特殊,它不是Java类,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类,因此获得它的引用肯定返回null。
- 总结:
- 1、运行一个程序时,总是由AppClass Loader(系统类加载器)开始加载指定的类。
- 2、在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。
- 3、Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null.
- 意义: 保证 Java 核心库的类型安全,防止内存中出现多份同样的字节码
比如两个类A和类B都要加载System类:
- 如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
- 如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了。
初始化:
在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
至于使用和卸载阶段阶段,这里不再过多说明,使用过程就是根据程序定义的行为执行,卸载由GC完成。
于类加载过程中初始化阶段,虚拟机严格规定了有且只有四种情况必须立即对类进行“初始化”:
- 遇到new、getstatic、putstatic和invokestatic这4条指令码时,如果类没有初始化,则需要先进行初始化。这4条指令对应的java场景是:
- 使用new关键字实例化对象
- 读取或设置一个静态字段,不包括被final修饰的、已在编译器把结果放入常量池的静态字段
- 调用一个静态方法
- 使用java.lang.reflect包方法对类进行发射调用
- 当初始化一个类的时候,发现其父类还没有进行过初始化,则需要先初始化其父类。但是对于一个接口来说,只有在用到其父接口的时候才会去初始化。
- 当虚拟机启动时,用户需要指定一个要执行的类,虚拟机会先初始化这个类
以上这四种情况被称为是对一个类的主动引用。除此之外所有引用类的方式,都不会触发初始化,被称为被动应用。
下面是几个demo:
demo1:Class.forName()与ClassLoader.loadClass()动态加载的区别
通过Class.forName()方法动态加载,默认会执行初始化块。
通过ClassLoader.loadClass()方法动态加载,不会初始化块
public class Person implements Serializable { static{ System.out.println("static ok");
}
private static final long serialVersionUID = -8347820354686844949L;
private int age;
private String name;
public static int x = 6;
public Person(int age,String name){ this.age =age; this.name = name;
.....
}
demo2:父类静态变量
public class Male extends Person { static { System.out.println("SubClass male ok..."); } public Male(int age, String name) { super(age, name); // TODO Auto-generated constructor stub } private static final long serialVersionUID = 1L; }
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类来引用父类的静态字段会导致父类进行初始化,而子类不进行初始化。 另外通过数组来定义引用类,不会触发此类的实例化
相关问题:
1能不能自己写个类叫java.lang.System
?
答案:通常不可以,但可以采取另类方法达到这个需求。
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。但是,我们可以自己定义一个类加载器来达到这个目的。这块我没写demo测试下。
由不同的类加载器加载的指定类型还是相同的类型吗?
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间.我们可以用两个自定义类加载器去加载某自定义类型。
demo:
com.example.Sample
类的方法 setSample
接受一个 java.lang.Object
类型的参数,并且会把该参数强制转换成com.example.Sample
类型。
package com.test;public class Sample { private Sample instance; public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;
import java.io.InputStream;
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
public 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";
}
}
上图使用了类 FileSystemClassLoader
的两个不同实例来分别加载类 com.example.Sample
,得到了两个不同的 java.lang.Class
的实例,接着通过 newInstance()
方法分别生成了两个类的对象 obj1
和 obj2
,最后通过 Java 的反射 API 在对象 obj1
上调用方法 setSample
,试图把对象 obj2
赋值给 obj1
内部的 instance
对象。 从运行结果可以看到,运行时抛出了 java.lang.ClassCastException
异常。虽然两个对象 obj1
和 obj2
的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。
关于图中标注,我本机使用jdk1.7测试的。如果跟ibm原文那样,就是用箭头的loadclass方法,相当于都被父类加载器加载了,可以输出看到都是AppClassLoader,不会被自定义的FileSystemClassLoader加载,此时不会抛异常。改成findclass才是自定义的加载器,才会抛异常。说道这里,关于自定义加载器,不建议覆写loadClass()方法,容易导致系统默认的加载器不能正常工作。
3 在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是什么?
JVM规范中规定在不指定父类加载器的情况下, 默认采用系统类加载器作为其父加载器, 所以在使用该自定义类加载器时, 需要加载的类不能在类路径中, 否则的话依据类加载器的代理/委托原则, 待加载类会由系统类加载器加载,
4常见异常 ClassNotFoundException 的原因:
在看前面介绍类加载机制时候会发现,真正完成类的加载工作是通过调用 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方法不会被重复调用。
*************总结******************
梳理类加载这块,理解背后的机制,有助于理解JVM实现,有助于理解java的动态性,
对于实际应用来说,就是便于排查问题,更多的是根据反射,实现热加载等高级特性。
碍于篇幅,本文相关的缺少tomcat容器及 线程上下文类加载器(context class loader)。