Java类加载器(Java Classloader)是Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中,用于加载系统、网络或者其他来源的类文件。Java源代码通过javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。
我们以上图为例子,比如我们创建一个ClassLoaderTest.java文件运行,经过javac编译,然后生成ClassLoaderTest.class文件。这个java文件和生成的class文件都是存储在我们的磁盘当中。但如果我们需要将磁盘中的class文件在java虚拟机内存中运行,需要经过一系列的类的生命周期(加载、连接(验证-->准备-->解析)和初始化操作,最后就是我们的java虚拟机内存使用自身方法区中字节码二进制数据去引用堆区的Class对象。
通过这个流程图,我们就很清楚地了解到类的加载就是由java类加载器实现的,作用将类文件进行动态加载到java虚拟机内存中运行。
实现不同项目或者同一个项目上的不同版本的jar包隔离,避免集群错误或者冲突的产生。
对于Java应用程序来说,热部署就是在运行时更新Java类文件。在基于Java的应用服务器实现热部署的过程中,类装入器扮演着重要的角色。类装入器不能重新装入一个已经装入的类,但只要使用一个新的类装入器实例,就可以将类再次装入一个正在运行的应用程序。这样我们应用正在运行的时候升级软件,却不需要重新启动应用。
我们可以对字节码文件进行加密,然后再使用特定的ClassLoader解密文件内容,再加载这些字节码文件。这样就能够实现对我们的代码项目加密保护,别人无法进行反编译查看源代码信息。
在简单了解的类加载器的概念作用之后,我们可以看看java中类加载器的分类。类加载器大致分为两种,一种是JVM自带的类加载器,分别为引导类加载器、扩展类加载器和系统类加载器。另外一种就是用户自定义的类加载器,可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。
引导类加载器(BootstrapClassLoader),底层原生代码是C++语言编写,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类)。
这里以我的电脑中jdk1.7为例,这里我们看到/jre/lib/rt.jar目录,这里面的类都是由BootstrapClassLoader来加载。
之后我们使用Object类为例,看看其是否存在父加载器。因为Object类是所有子类的父类,归属于BootstrapClassLoader。发现并没有父类加载器,结果为null。
扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库。Java虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载java类。
以我的电脑为例,这个目录下的jar包都是由ExtensionsClassLoader进行加载。
这里我们可以选择一个zipfs.jar包,然后查看下其jar中的ZipPath类加载器,发现存在于ExtensionsClassLoader。
App类加载器/系统类加载器(AppClassLoader),由sun.misc.Launcher$AppClassLoader实现,一般通过通过(java.class.path或者Classpath环境变量)来加载Java类,也就是我们常说的classpath路径。通常我们是使用这个加载类来加载Java应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。
这里我们使用本身写的ClassLoaderTest类进行测试,发现存在于AppClassLoader。
自定义类加载器(UserDefineClassLoader),除了上述java自带提供的类加载器,我们还可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器。具体实现方法我们等下单独讲解。
通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class文件加载到内存当中生成class对象进行使用,且加载过程使用的是双亲委派模式,及把需要加载的类交由父加载器进行处理。
概念有点绕,我们可以先看类加载器委派关系
如上图类加载器层次关系,我们可以将其称为类加载器的双亲委派模型。但注意的是,他们之间并不是"继承"体系,而是委派体系。当上述特定的类加载器接到加载类的请求时,首先会先将任务委托给父类加载器,接着请求父类加载这个类,当父类加载器无法加载时(其目录搜素范围没有找到所需要的类时),子类加载器才会进行加载使用。这样可以避免有些类被重复加载。
1、这样就是能够实现有些类避免重复加载使用,直接先给父加载器加载,不用子加载器再次重复加载。
2、保证java核心库的类型安全。比如网络上传输了一个java.lang.Object类,通过双亲模式传递到启动类当中,然后发现其Object类早已被加载过,所以就不会加载这个网络传输过来的java.lang.Object类,保证我们的java核心API库不被篡改,出现类似用户自定义java.lang.Object类的情况。
假设我们创建一个java.lang包,自定义一个TestObject类,但由于双亲委派机制的存在,是不允许加载运行的。
除了上述BootstrapClassLoader,其他类加载器都是继承了CLassLoader类,我们就一起看看其类的核心方法。以下代码都是截取了其方法的源码。
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; }
在loadClass方法中,它先使用了findLoadedClass(String)方法来检查这个类是否被加载过。
接着使用父加载器调用loadClass(String)方法,如果父加载器为null,类加载器加载jvm内置的加载器。