Java程序并不是一个可执行文件,而是由很多的Java类组成,其运行是由JVM来控制的。而JVM从内存中查找到类,而真正将类加载进内存的就是ClassLoader,可以说我们每天都在接触ClassLoader,但是很多时候我们没有明白其执行的流程和原理。
01
为什么需要ClassLoader?
ClassLoader的有以下的作用:
- 从本地系统加载类文件,甚至是从网络上加载Java类文件,例如Applet,加载Class是程序执行的前提。
- 进行应用的隔离,不同的应用使用ClassLoader加载的类实现相互隔离不可见,典型的例子如tomcat,启动的时候会启动一个JVM,Tomcat下面会部署多个应用,但是多个应用之间的类是相互不可见的,也不能相互调用,这就是依靠自定义的ClassLoader WebappX进行隔离的。
- 自定义ClassLoader在执行非信任代码前验证数字签名。
- 自定义ClassLoader根据用户提供的密码解密代码,从而可以对.class文件加密,避免被反编译。
- 根据用户的需要动态的创建类,增强代码能力。
02
Java ClassLoader运行机制
Java提供了三个ClassLoader,分别是BootStrapClassLoader、ExtClassLoader和AppClassLoader。
1、BootStrapClassLoader
启动类装载器,主要加载jre的lib目录下的Java类,使用C++编写,是JVM自带的类加载器,用来装载核心类库。Java程序可以通过以下代码查看这个类加载器加载了哪些jar包:
URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (int i = 0; i < urls.length; i++) { System.out.println(urls[i].toExternalform()); }
执行结果如下:
file:/C:/Program%20Files%20(x86)/Java/jre7/lib/resources.jar file:/C:/Program%20Files%20(x86)/Java/jre7/lib/rt.jar file:/C:/Program%20Files%20(x86)/Java/jre7/lib/sunrsasign.jar file:/C:/Program%20Files%20(x86)/Java/jre7/lib/jsse.jar file:/C:/Program%20Files%20(x86)/Java/jre7/lib/jce.jar file:/C:/Program%20Files%20(x86)/Java/jre7/lib/charsets.jar file:/C:/Program%20Files%20(x86)/Java/jre7/lib/jfr.jar file:/C:/Program%20Files%20(x86)/Java/jre7/classes
可以明显看出BootStrapClassLoader在jre目录下加载了lib目录的特定jar和classes目录下的所有的类,这其实是在JVM中定义的,加载类的路径源代码定义如下:
static const char classpathFormat[] = "%/lib/rt.jar:" "%/lib/i18n.jar:" "%/lib/sunrsasign.jar:" "%/lib/jsse.jar:" "%/lib/jce.jar:" "%/lib/charsets.jar:" "%/classes";
有时候我们希望使用BootStrapClassLoader加载额外的一些指定类或者jar包,此时可以在执行java启动命令的时候指定-Xbootclasspath参数。
-Xbootclasspath参数有三种使用方式:
- 直接使用-Xbootclasspath参数。完全取代基本核心的Java class 搜索路径,如果使用这种方式,需要自己全新编写核心类或者重新制定核心类jar包路径,基本不使用。
- 使用-Xbootclasspath/a。以冒号作为path分隔符,指定包含类的目录路径,jar或者zip的路径,此参数表示将这些额外的类附加到默认的类路径中,先Load默认的jar文件,然后再Load -Xbootclasspath/a:path指定的jar文件,这种方式不会导致覆盖系统提供的默认类。可以编写执行类A,由类A引用类B,将B类打成jar包B.jar,执行以下命令,如果能够正常查找到类B,证明加载成功。
java -verbose:class -Xbootclasspath/a:B.jar com.fantuantech.A
- 使用-Xbootclasspath/p。以冒号作为path分隔符,指定包含类的目录路径,jar或者zip的路径,此参数表示先加载参数指定的jar文件,然后再去加载系统默认的jar和类文件,一旦系统在参数指定的路径中load了全权限定名与JRE提供的默认的类文件相同的文件的时候,将不会去load JRE提供的相同文件,所以这个参数会造成覆盖JRE提供的默认文件的情况,官方是不建议这么做的。
2、ExtClassLoader
加载$JAVA_HOME/jre/lib/ext目录下的类或者是其他任何通过java.ext.dirs系统属性指定的目录下的Java类。这是通过实现了sun.misc.Launcher$ExtClassLoader接口实现的。如果有自定义的公共类希望通过ExtClassLoader自动加载的话,可以采用以下两种方式:
- java -Djava.ext.dirs=/home/externalDir,在程序启动的时候通过参数指定扩展类加载器额外加载的类路径
- 将额外的jar包放在jre/lib/ext目录下,当AppClassLoader加载这些类加载不到时会委托ExtClassLoader加载,ExtClassLoader就会去这个目录下查找对应的类并加载。
ExtClassLoader的parent是BootStrapClassLoader,但是由于BootStrapClassLoader是由c++编写的,通过native方式加载的。而并不是由java编写的,所以当调用classLoader的getParent()方法时,获取到的是null。
3、AppClassLoader
加载java.class.path(一般映射到系统classPath)指定的目录下的所有Java类,实现sun.misc.Launcher$AppClassLoader接口。在程序执行时通过-classpath或者-cp或者-Djava.class.path可以指定系统类路径。
03
Java ClassLoader双亲委派机制
既然Java同时存在多种ClassLoader,那么这些ClassLoader什么关系,Class类的加载顺序是怎么样的,会不会存在冲突呢?为了解决这些问题,Java ClassLoader采用了双亲委派机制,如下图所示:
双亲委派机制是指某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类,否则就会出现冲突。
ClassLoader有一些重要的方法,主要是以下几个:
- loadCass
loadClass(String name ,boolean resolve)其中name参数指定了JVM需要的类的名称,该名称以包表示法表示,如Java.lang.Object;resolve参数告诉方法是否需要解析类,在初始化类之前,应考虑类解析,并不是所有的类都需要解析,如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要解析。这个方法是ClassLoader 的入口点 - defineClass
这个方法接受类文件的字节数组并把它转换成Class对象。字节数组可以是从本地文件系统或网络装入的数据。它把字节码分析成运行时数据结构、校验有效性等等。 - findSystemClass
findSystemClass方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用defineClass将字节数组转换成Class对象,以将该文件转换成类。当运行Java应用程序时,这是JVM 正常装入类的缺省机制。 - resolveClass
resolveClass(Class c)方法解析装入的类,如果该类已经被解析过那么将不做处理。当调用loadClass方法时,通过它的resolve 参数决定是否要进行解析。 - findLoadedClass
当调用loadClass方法装入类时,调用findLoadedClass 方法来查看ClassLoader是否已装入这个类,如果已装入,那么返回Class对象,否则返回NULL。如果强行装载已存在的类,将会抛出链接错误。
其中类加载主要是loadClass方法,以下是方法的定义,可以看到查找类主要分为4个步骤:
- findLoadedClass判断类是否已经被加载。
- 如果类未被加载则通过parent.loadClass加载类。
- 如果父ClassLoader为空,则调用findBootstrapClass0从BootstrapClassLoader去加载。
- 如果还是找不到,则调用findClass由当前加载器去加载相应的类。
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException { // First, check if the class has already been loaded Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // If still not found, then invoke findClass in order to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
采用双亲委派机制有以下的好处:
- 安全
不允许任意的创建类替换Java的基础类,例如创建一个java.lang.Integer,新建的整个Integer是不会被加载的,因为根据双亲委派原则,会委托给BootStrapClassLoader去加载这个类,而BootstrapClassLoader是JVM实现的,无法更改,它默认去加载jre/lib目录下的类,包括了java.lang.Integer。 - 隔离
每个类在JVM中的唯一表示与类的权限定名以及类加载器有关,可以说一个类加载器加上一个类的全限定名唯一确定了一个类,通过自定义不同的ClassLoader,即使加载了相同限定名的类也不会造成冲突。比如tomcat的StandardClassLoader就是使用ClassLoader来为每一个应用做隔离。
04
自定义ClassLoader
这里我们编写一个简单的案例用来说明如何自定义ClassLoader,代码仅供演示,先创建一个目录,所有演示代码都在同个目录下:
1、创建test目录
mkdir /Users/lucas-os/workspace/test
2、自定义类MyClassLoader,继承ClassLoader
public class MyClassLoader extends ClassLoader { private String path; public MyClassLoader (String path) { this.path = path; } @Override protected Class findClass (String name) throws ClassNotFoundException { System.out.println(getSystemClassLoader().getName()+","+getSystemClassLoader().getParent().getName()); String classPath = path+name+".class"; InputStream inputStream = null; ByteArrayOutputStream outputStream = null; try { inputStream = new FileInputStream(classPath); outputStream = new ByteArrayOutputStream(); int temp = 0; while((temp = inputStream.read()) != -1){ outputStream.write(temp); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { try { outputStream.close(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } byte[] bytes = outputStream.toByteArray(); Class clazz = defineClass(name,bytes,0,bytes.length); resolveClass(clazz); return clazz; } }
3、自定义HelloWorld类,用于被ClassLoader加载
public class HelloWorld { public HelloWorld(){ System.out.println("Hello ClassLoader!"); } }
4、自定义Test类,作为程序入口
源代码如下:
public class Test { public static void main(String[] args) { MyClassLoader myClassLoader = new MyClassLoader("/Users/lucas-os/workspace/test/"); try { Class clazz = myClassLoader.findClass("HelloWorld"); clazz.getConstructor().newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
依次使用以下命令编译和执行代码:
javac HelloWorld.java javac Test.java java -classpath /Users/lucas-os/workspace/test/ Test
执行结果如下所示,可以看到当前SystemClassLoader是app,其父ClassLoader是platform,程序正常打印出了"class HelloWorld"证明类被正常加载。
app,platform /Users/lucas-os/workspace/test/HelloWorld.class Hello ClassLoader!