一:概述
类加载过程包括以下五大步骤:
1.加载:通过类的完全限定名(包名 + 类名)查找此类的class文件,并创建一个Class对象。
2.验证:校验class文件的正确性,class文件加载后,最基本的是不能破坏虚拟机的正常运行,这就需要校验;校验包括文件格式(魔数)校验、元数据校验、字节码校验、符号引用校验。
3.准备:为类变量(static)分配内存空间(在方法区/元数据区分配),并对他们进行初始化,是初始化,不是赋代码里面的值;final类型修饰的static变量除外,这种类型的数据在编译期就分配了空间。
4.解析:将常量池中的符号引用转换成直接引用。符号引用是用一组符号来引用目标,这个符号可以是任何字面量;而直接引用是指向目标的指针、相对偏移量或者一个间接定位到目标的句柄。
5.类加载的最后阶段,初始化该类。若该类有父类,则对父类进行初始化。
二:类加载器类型
类加载器的任务是根据一个全限定类名读取目标class文件的二进制流到虚拟机中,然后创建Class对象。虚拟机提供了三种类加载器:引导加载器(Bootstrap加载器)、扩展加载器(Extension)和系统加载器(System加载器,也称为应用类加载器)。
启动类加载器(Bootstrap类加载器):
启动类加载器主要用于加载虚拟机本身需要用到的类,他是虚拟机自身的一部分。他用于%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等,另外可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中;注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
扩展类加载器(Extension类加载器):
它负责加载加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录,开发者可以直接使用标准扩展类加载器。下面的代码能都查看扩展类加载器记载的类路径:
//ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
//加载/lib/ext目录中的类库
String s = System.getProperty("java.ext.dirs");
File[] dirs;
if (s != null) {
StringTokenizer st =
new StringTokenizer(s, File.pathSeparator);
int count = st.countTokens();
dirs = new File[count];
for (int i = 0; i < count; i++) {
dirs[i] = new File(st.nextToken());
}
} else {
dirs = new File[0];
}
return dirs;
}
系统类加载器:
系统类类加载器也称为应用类加载器;他负责加载系统类路径java -classpath或者-D java.class.path下的类库,也是我们经常用到的classpath的路径;一般情况下,该类加载器是程序中默认的类加载器。通过ClassLoader.getSystemClassLoader()方法可以获取到该类加载器。
在日常开发中,类的加载几乎全部右上面三种类加载器配合执行。必要的时候,我们也可以自定义自己的类加载器。类的加载是按需加载,只有真正用到该类是,才会把该类加载到内存,并创建相应的Class对象;而加载的过程通过双亲委派的方式加载。
三:双亲委派模式
双亲委派模式要求除了顶级的加载器之外,其他的加载器都要有父类加载器,这个父类的意思不是java里面的类继承,而是采用组合关系来复用父类加载器的代码,类加载器的关系图如下(盗图):双亲委派的工作原理是:如果一个类加载器收到了一个加载类的请求,他并不会自己马上去加载该类,而是委托给他爹去加载;如果他爹还存在父类加载器;那么就委派他爷爷去加载,如此类推,一直委托到顶级加载器;如果他爹能加载,那么就加载吧;如果他爹加载不了,就只能自己动手丰衣足食了,这就是双亲委派。为什么要采用这种模式呢?
双亲委派的优势:
采用这种模式的好处是java类随着他的类加载器一起天然具备了一种带有优先级的层次关系,通过这种层次关系,可以避免类的重复加载;当父类加载了该类,那么子类就没必要再加载该类;其次是考虑到安全因素,java核心api肯定是不能随意被串改的,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
四:源码分析
ClassLoader是一个抽象类,先来看下API对该类的描述:接下来看下loadClass方法,看他是怎么实现双亲委派的:
/**
* Loads the class with the specified binary name.
* This method searches for classes in the same manner as the {@link
* #loadClass(String, boolean)} method. It is invoked by the Java virtual
* machine to resolve class references. Invoking this method is equivalent
* to invoking {@link #loadClass(String, boolean) loadClass(name,
* false)}.
*
* @param name
* The binary name of the class
*
* @return The resulting Class object
*
* @throws ClassNotFoundException
* If the class was not found
*/
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
name就是类的全限定名,loadClass调用了重载的loadClass方法,第二个参数是指是否解析加载后生成的Class对象,默认不解析:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//给类加载器加锁;getClassLoadingLock
//的作用返回一个锁对象,具体过程一会分析
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//首先调用findLoadedClass检查目标类是否已经被加载过
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();
}
}
//如果需要解析Class对象的话,那就解析他
if (resolve) {
resolveClass(c);
}
//最终返回Class对象
return c;
}
}
双亲委派就是这么实现的,每个类加载器都持有父加载器的引用;每次加载的时候就递归调用父加载器的findClass方法去加载,一直委托到启动加载器,如果启动记载其都加载不了,那么自己加载;上面的方法有几个难点,下面一一分析:
//参数className就是要加载的类
protected Object getClassLoadingLock(String className) {
//将当前类加载器赋值给lock
Object lock = this;
//parallelLockMap是一个ConcurrentHashMap,他是在类加载器
//被创建的时候初始化,不过要不要初始化是有条件的,如果该类加载器
//不具备并行加载的能力,那么就不初始化;一旦初始化了,说明该类加
//器具有并行加载的能力。这个时候就要去parallelLockMap找与传进
//来的类的对应的锁对象,这些类和他对应的锁都存在了这个集合里面
if (parallelLockMap != null) {
//创建一个新的锁,Object类型
Object newLock = new Object();
//putIfAbsent是ConcurrentHashMap的方法,线程安全,跟HashMap
//的put方法类似,就是往集合里面存入键值对,若干键存在,那么就更新
//并返回老的value,否则就插入并返回空;
lock = parallelLockMap.putIfAbsent(className, newLock);
//如果ConcurrentHashMap里面没有这个key,那么
//lock就是空,此时就刚刚创建的newLock赋值给lock
if (lock == null) {
lock = newLock;
}
}
//返回锁对象
return lock;
}
综上分析,getClassLoadingLock就是获取一个和待加载类绑定的锁对象。接下来分析findLoadedClass,这个方法的作用是判断待加载的类是否已经被加载过,如果加载过,那么返回这类的Class对象:
protected final Class> findLoadedClass(String name) {
//对类名进行校验,比如不能为空等
if (!checkName(name))
//如果类名不符合要求,那么返回空
return null;
//调用findLoadedClass0
return findLoadedClass0(name);
}
findLoadedClass比较简单,首先校验类名是否合法,接着调用findLoadedClass0,这个方法是native方法,看不到代码,就此打住。
如果待加载的类没有被加载过,父加载器也加载不了,那么就自己调用findClass去加载类;虽然类加载类是从loadClass开始的,但是实际上加载类是在findClass方法里面进行的,一般自定义类加载器是重写findClass方法,而不是loadClass方法,因为loadClass方法已经实现了双亲委派的逻辑,这个逻辑我们不需要重写,所以我们只需要重写findClass方法即可。这个类是空实现,留待各个加载器的子类自己实现。
//空实现;上面说了,类加载是在findClass里面进行
//的,这里使用空实现,给了各个类加载器自己发挥的空间
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
除了上面提到的几个方法,还有两个方法非常重要:defineClass和resolveClass。defineClass方法的作用是将byte字节流解析成JVM能够识别的Class对象,ClassLoader中已实现该方法逻辑,无需我们自己重写(自己重写的要求有点高);resolveClass的作用是解析Class对象,也可以理解成类加载过程中的链接那一步骤,这两个方法都很难,不做分析,我们开发过程中也不太会去重写这两个方法。
五:Demo验证
下面通过一个demo来理解下各个类加载器之间的关系:
public class TestLoader extends ClassLoader{
public static void main(String[] args) {
TestLoader tl = new TestLoader();
//自定义类加载器的父类加载器
System.out.println("自定义类加载的父类加载器 : " + tl.getParent());
System.out.println("系统默认加载器 : " + ClassLoader.getSystemClassLoader());
System.out.println("系统默认类加载器的父类加载器 : " +
ClassLoader.getSystemClassLoader().getParent());
System.out.println("扩展类加载器的父类加载器 : " +
ClassLoader.getSystemClassLoader().getParent().getParent());
System.out.println("系统默认类加载器的父类 : " +
ClassLoader.getSystemClassLoader().getParent().getClass().getSuperclass().getName());
}
}
输出结果如下:
自定义类加载的父类加载器 : sun.misc.Launcher$AppClassLoader@4e25154f
系统默认加载器 : sun.misc.Launcher$AppClassLoader@4e25154f
系统默认类加载器的父类加载器 : sun.misc.Launcher$ExtClassLoader@33909752
扩展类加载器的父类加载器 : null
系统默认类加载器的父类加载器 : java.net.URLClassLoader
可以看到,自定义类加载器的父加载器是系统默认加载器AppClassLoader;AppClassLoader的父加载器是ExtClassLoader;而ExtClassLoader的父加载器是空;另外注意下,ExtClassLoader的父类(不是父加载器)是URLClassLoader,乱入一个URLClassLoader是什么鬼?其实URLClassLoader是ClassLoader的子类,他重写了findClass和defineClass方法,既然系统默认类加载器都继承自该类,我们自定义类加载器有什么理由不去继承URLClassLoader而去继承CloadClass呢?
类的唯一性
在刚学java的时候,我们一般都认为包名 + 类名就能唯一确定一个类,但是这种说法是不严谨的,请看例子;创建同一类型的加载器的两个对象去加载同一个类:
public class TestLoader extends ClassLoader{
//该类加载器查找的路径
private String dir;
//构造函数
public TestLoader(String dir) {
this.dir = dir;
}
public static void main(String[] args) throws ClassNotFoundException {
//注意,类加载器加载的是class文件,所以用eclipse测试的时候
//要把src改成bin,要不然会抛出ClassNotFoundException异常
String rootDir = "/Users/tushihao/eclipse-workspace/Test/bin/";
//创建两个自定义的类加载器,对象不同,但类型一样
TestLoader t1 = new TestLoader(rootDir);
TestLoader t2= new TestLoader(rootDir);
//通过两个类加载器去加载同一个类(传入类名和包名),这里可能会抛异常
try {
Class> c1 = t1.loadClass("testclassloader.Test");
Class> c2 = t2.loadClass("testclassloader.Test");
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
} catch (ClassNotFoundException e) {
System.out.println("class not found");
}
}
输出结果如下:
865113938
865113938
卧槽,不同类加载器加载同一个类,结果相同,这脸打的好痛。什么原因导致的呢?还记得loadClass的代码吗?
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//给类加载器加锁;getClassLoadingLock
//的作用返回一个锁对象,具体过程一会分析
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//首先调用findLoadedClass检查目标类是否已经被加载过
Class> c = findLoadedClass(name);
......
从代码可以看出,t1去加载Test的class文件后,会把加载的结果缓存起来;t2再去加载;t2加载的第一步是看缓存里面有没有Test,如果有,就直接返回;否则就自己去找,这里并没有判断类加载器是不是同一个,所以才出现了上面的结果。要想不查缓存,要么重写loadClass方法,要么重写findClass,然后直接去调用findClass,因为findClass是不会去查缓存的;考虑到重写loadClass还要自己写一套维持双亲委派的逻辑,不值当,所以这里选择直接调用findClass,这样的话就必须重写这个方法了,因为ClassLoader的findClass是空实现,不重写就会抛出ClassNotFoundException:
//重写findClass方法
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
//通过getClassData去读取class文件到byte数组
byte[] classData = getClassData(name);
//如果没读到肯定要抛出异常给你尝尝
if (classData == null) {
throw new ClassNotFoundException();
}
else {
//读到了的话就调用系统的defineClass方法构建Class对象
return defineClass(name, classData, 0, classData.length);
}
}
//读取class文件的数据
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 dir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
上面就重写了findClass方法,下面把main方法改成下面这样:
public static void main(String[] args) throws ClassNotFoundException {
//注意,类加载器加载的是class文件,所以用eclipse测试的时候
//要把src改成bin,要不然会抛出ClassNotFoundException异常
String rootDir = "/Users/tushihao/eclipse-workspace/Test/bin/";
//创建两个自定义的类加载器
TestLoader t1 = new TestLoader(rootDir);
TestLoader t2= new TestLoader(rootDir);
//通过两个类加载器去加载同一个类(传入类名和包名),这里可能会抛异常
try {
Class> c1 = t1.findClass("testclassloader.Test");
Class> c2 = t2.findClass("testclassloader.Test");
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
} catch (ClassNotFoundException e) {
System.out.println("class not found");
}
}
输出结果如下:
1975012498
1808253012
可以看到,不同的加载器加载同一个类,加载的结果就不一样了,终于不被打脸了。所以,根据包名 + 类名,不一定能唯一确定一个类。
六:自定义加载器的必要性
通过前面的Demo可知,要自定义一个类加载器,可以继承ClassLoader或者URLClassLoader;如果继承自ClassLoader,那么需要自己重写findClass方法,也就是自己去找指定位置的class文件,把数据读出来(byte数组类型),转换成Class对象(转换过程已经在系统方法ClassLoader中实现,无需重写);如果继承自URLClassLoader,那么连findClass方法都不用重写了(当然,也可以重写);那么自定义类加载器的意义何在?
1.当class文件不在classpath下时,系统类加载器无法找到该class文件,此时就需要我们自己写一个类加载器去加载指定路径下的class文件并创建Class对象了。
2.当一个class文件是通过网络传输过来时,此class文件可能被加密,此时需要先对此class文件进行解密才能被使用,这就需要自定义一个类加载器进行解密,然后加载到内存去。
3.当实现热部署功能时(一个class文件通过不同的类加载器产生不同的class对象从而实现热部署),需要自定义一个类加载器,这是很常见的需求。
下面再看一个完整的读取网络传输的class文件并创建对象的demo:
public class NetClassLoader extends ClassLoader {
//网络上class文件的URL
private String url;
public NetClassLoader(String url) {
super();
this.url = url;
}
public static void main(String[] args) {
}
//重写findClass方法
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
byte[] data = getClassDataFromNet(name);
//找不到文件就死给你看
if(data == null) {
throw new ClassNotFoundException();
}else {
//调用系统方法创建Class对象
return defineClass(name,data, 0, data.length);
}
}
private byte[] getClassDataFromNet(String className) {
//获取网络上的class文件的路径
String path = classNameToPath(className);
//下载class文件并转化成byte数组
try {
URL url = new URL(path);
InputStream is = url.openStream();
ByteArrayOutputStream bas = new ByteArrayOutputStream();
int buffer = 4096;
byte[] bf = new byte[buffer];
int readNum = 0;
while((readNum = is.read(bf)) != -1) {
bas.write(bf,0,readNum);
}
//解密
decrypt(bas);
return bas.toByteArray();
}catch (Exception e) {
// TODO: handle exception
}
return null;
}
//解密方法
private void decrypt(ByteArrayOutputStream bas) {
//假装我已经解密了
}
private String classNameToPath(String className) {
return url + "/" + className.replace(".", "/") + ".class";
}
}
摘自:https://blog.csdn.net/javazejian/article/details/73413292 (略有改动)