类加载机制总结

咱们知道java要运行须要编译和运行,javac将java源代码编译为class文件。而虚拟机把描述类的数据从class文件中加载到内存,并对数据进行校验、转换解析、初始化,最终造成能够被虚拟机直接使用的java类型,这就是类加载机制,他在运行期间完成。java

JVM加载class文件到内存有两种方式:web

  • 隐式加载:虚拟机自动加载须要的类
  • 显式加载:代码中经过调用ClassLoader类来加载,例如Class.forName()、this.getClass.getClassLoader().loadClass()或者本身实现ClassLoader的findClass()

接下来先来看三个例子

以前的我只知道在对象建立以前会先初始化静态的东西,也知道从父类开始初始化,但一直不懂为何会是这样的顺序,直到我了解了虚拟机是如何实现类加载的。在开始真正了解类加载以前,咱们先来看三个例子。数组

第一个

class SuperClass {
    static{
        System.out.println("SuperClass Init");
    }
    public static int value = 123;
}

class SubClass extends SuperClass{
    static{
        System.out.println("SubClass Init");
    }
}

public class NotInitialization{
    public static void main(String agrs[]){
        System.out.println(SubClass.value);
    }
}
复制代码

输出:tomcat

SuperClass Init
123
复制代码

这道例子彷佛很简单,他告诉咱们对于静态字段,只有直接定义这个字段的类才会被初始化,因此,即便这里是经过子类来引用父类的静态属性,他也不会使子类发生初始化,而至于加载和验证,虚拟机并无明确规范,各步骤的做用下文会谈安全

第二个

class SuperClass {
    static{
        System.out.println("SuperClass Init");
    }
    public static int value = 123;
}

class SubClass extends SuperClass{
    static{
        System.out.println("SubClass Init");
    }
}

public class NotInitialization{
    public static void main(String agrs[]){
        SuperClass[] sca = new SuperClass[10];
    }
}
复制代码

输出:bash

//无输出
复制代码

是的,运行以后并无输出,但他触发了一个叫“[Lorg.fenixsoft.classloading.SuperClass”的类初始化,而建立动做由字节码指令newarray触发,从这里,咱们也就直到建立一个对象数组的真实状况了服务器

第三个

class ConstClass{
    static{
        System.out.println("ConstClass init");
    }

    public static final String WORD = "Hello";
}

public class NotInitialization{
    public static void main(String agrs[]){
        System.out.println(ConstClass.WORD);
    }
}
复制代码

输出:网络

Hello
复制代码

这里WORD做为一个常量,他在编译阶段就已经生成,意思是说编译阶段通过常量传播优化,已经将他存储到了NotInitialization类的常量池中,之后全部对它的引用都是NotInitialization对常量池的引用,这就是为何不初始化类。数据结构

类初始化

下面来总结一下五种必须对类初始化的状况:多线程

  • 遇到new,getstatic,putstatic,invokestatic这四条字节码指令(后三者能够简单理解为对静态属性或方法的调用)
  • 使用java.lang.reflect包的方法对类进行反射调用时
  • 初始化类时的父类没有初始化时初始化父类
  • 虚拟机启动时,用户须要执行的主类(main方法的那个类)
  • JDK1.7动态语言支持时(类型检查在运行时而不是编译时,java.lang.invoke包,这里多说一句,动态语言和反射又有所不一样)

以上,都是类第一次发生初始化的状况,而对于接口的初始化,他和类的不一样就是只有在真正使用到父接口的时候才会初始化父接口。

类加载过程

下面来具体看一下类加载的全过程分别要作哪些事情

image

加载

这个时期须要完成三件事:

  1. 经过一个全限定名来获取一个此类的二进制流
  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构
  3. 在内存生成一个表明这个类的java.lang.Class对象,做为方法区这个类的各类数据的访问接口

这里,非数组类的加载阶段和数组类有些不一样:

  • 对于非数组类
    • 加载阶段能够经过系统提供的引导类加载器完成,也能够由用户自定义的类加载器去完成,也可本身控制字节流的获取方式(重写一个类加载去的loadClass方法)
  • 对于数组类
    • 若数组组件类型是引用类型,数组在加载该组件类型的类加载器的类名称空间上被标识
    • 若组件类型不是引用类型,将把数组标记为与引导类加载器关联
    • 数组类的可见性和它的组件类型可见性一致,则默认为public

说直白加载的做用就是找到.class文件并把这个文件包含的字节码读取到内存中

验证

这一步的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,且不会危害虚拟机自身的安全,大概分为四部验证

  • 文件格式验证:是否符合Class文件格式的规范,且能被当前版本虚拟机处理,保证输入的字节流能正确地解析并存储与方法区以内,格式上符合一个Java类型信息的要求
  • 元数据验证:对类的元数据信息进行语义校验,保证在不符合Java语言规范的元数据信息
  • 字节码验证:经过数据流和控制流分析,肯定程序语义合法,符合逻辑
  • 符号引用验证:确保解析动做正常执行,若没法经过符号引用验证,抛出java.lang.IncompatibleClassChangeError异常的子类

准备

为类变量分配内存并设置类变量初始化值,在方法区进行分配,如int为0,boolean为false,reference为null

解析

将常量池内的符号引用替换为直接引用的过程

问,什么是符号引用,什么是直接引用?

个人理解:

符号引用就是一个字符串,这个字符串有足够的信息能够找到相应的位置。直接引用就是偏移量,经过偏移量能够直接在内存区域找到方法字节码的起始位置。

解析主要包括对类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这些符号引用进行

初始化

在类中包含的静态初始化器都被执行,在这一阶段末尾静态字段被初始化为默认值,初始化遵照下面几条原则(其中是类初始化的字节码指令)

  • 静态初始化块中只能访问到定义在静态语句块以前的变量;定义在他以后的变量,在前面的静态语句块能够赋值,不能访问
  • 虚拟机保证在父类的在子类的以前执行
  • 虚拟机保证一个类的方法在多线程环境被正确的加锁、同步

下面来看几个例子

public class Test {
    static {
        i = 0;
        //System.out.println(i);
    }
    static int i;
}
复制代码

上面注释的那一行会报错,由于在静态初始化块中只能访问到定义在静态语句块以前的变量;定义在他以后的变量,在前面的静态语句块能够赋值,不能访问,说明了第一条

public class Test {
    static class DeadLoopClass{
        static{
            if (true){
                System.out.println(Thread.currentThread() + "init DeadLoopClass");
                while(true){
                }
            }
        }
    }

    public static void main(String agrs[]){
        Runnable script = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + "run over");
            }
        };
        Thread t1 = new Thread(script);
        Thread t2 = new Thread(script);
        t1.start();
        t2.start();
    }
}
复制代码

输出

Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass
复制代码

他会打印上面的语句并会发生阻塞,这个例子说明了初始化的时候会保证类会被正确加锁

类加载器

接下来咱们具体看一下类加载器有哪些特色,它的做用就是动态加载类到Java虚拟机的内存空间中,就是上文说的“经过一个类的全限定名来获取描述此类的二进制字节流”,而且这个动做是放到Java虚拟机外部实现的,就是说应用程序本身决定如何去获取须要的类

类与类加载器

在JVM中标识两个class对象是否为同一个类对象存在两个必要条件

  • 类的完整类名必须一致,包括包名
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
什么是类加载的动态性?

一个应用程序老是由n多个类组成,Java程序启动时,并非一次把全部的类所有加载后再运行,它老是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销

双亲委派模型

类加载器能够大体分为三类:

  • 启动类加载器(Bootstrap ClassLoader):这个加载器是C++写的,他在Java虚拟机启动后初始化,负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类
  • 扩展类加载器(Extension ClassLoader):由sum.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%/jre/lib/ext,此路径下的全部classes目录以及java.ext.dirs系统变量指定的路径中类库
  • 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径上所指定的类库,父类为ExtensionClassLoader
  • 自定义类加载器
    image

那么什么是双亲委派模型呢?咱们先来看一下他的工做过程。

若是一个类加载器收到了类加载请求,它并不会本身先去加载,而是把这个请求委托给父类的加载器去执行,若是父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,若是父类加载器能够完成类加载任务,就成功返回,假若父类加载器没法完成此加载任务,子加载器才会尝试本身去加载,这就是双亲委派模式。

注意,这里叫双亲不是由于继承关系而是组合关系

双亲委派模型的好处

很容易想到,双亲委派模型的层级能够避免重复加载,尤为是java的核心类库不会被替换,例如本身定义了一个java.lang.Integer,双亲委派模型不会去初始化他,而是直接返回加载过的Integer.class。固然,若是强行用defineClass()方法(这个方法将byte字节流解析成JVM可以识别的Class对象)去加载java.lang开头的类也不会成功,会抛出安全异常

双亲委派模型代码实现

ClassLoader的loadClass(),只列出了关键的

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        //首先,检查请求的类是否已经被加载过了
        Class c = findLoadedClass(name);
        if (c == null){
            try{
                if (parent != null){
                    c = parent.loadClass(name,false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e){
                //若是父类加载器抛出ClassNotFoundException,说明父类加载器没法完成加载请求
            }
            if (c == null){
                //在父类加载器没法加载的时候
                //再调用自己的findClass方法来进行类加载
                c = findClass(name);
            }
        }
        if (resolve){
            //使用类的Class对象建立完成也同时被解析
            resolveClass(c);
        }
        return c;
    }
复制代码

ClassLoader的findClass(),

//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}
复制代码

ClassLoader的defineClass

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);
        }
    }
复制代码

ClassLoader的resolveClass()

protected final void resolveClass(Class<?> c) {
        if (c == null) {
            throw new NullPointerException();
        }
    }
复制代码

下面再来看一下关键方法的具体做用:

  • loadClass():该方法加载指定名称(包括包名)的二进制类型,resolve参数表明加载同时是否须要被解析
  • findClass():自定义的类加载逻辑写在findClass()方法中
  • defineClass():用来将byte字节流解析成JVM可以识别的Class对象(ClassLoader中已实现该方法逻辑)
  • resolveClass():该方法可使用类的Class对象建立完成也同时被解析

先看如下loadClass()方法,经过以上代码能够看到逻辑并不复杂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass(),若父加载器为空让启动类加载器为父加载器,若父类加载失败,抛出异常,再调用本身的findClass()方法

在JDK1.2以后,若是咱们自定义类加载器的话咱们将再也不重写loadClass(),由于ClassLoader已经实现loadClass(),而且用它来达到双亲委派的效果。咱们自定义类加载器须要重写的是findClass(),知道findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用本身的findClass()方法来完成类加载,这样就能够保证自定义的类加载器也符合双亲委托模式。

破坏双亲委派模型

双亲委派模型不是一个强制性的约束模型,双亲委派模型也有不太适用的时候,这时根据具体的状况咱们就要破坏这种机制,双亲委派模型主要出现过三次被破坏的状况

第一次:

由于双亲委派模型是在JDK1.2的时候出现的,因此,在JDK1.2以前,是没有双亲委派的,为了向前兼容,JDK1.2以后的java.lang.ClassLoader添加了一个新的protected的findClass()方法,这个方法的惟一逻辑就是调用本身的loadClass(),前文分析代码实现的时候咱们知道双亲委派模型就是根据loadClass()来实现的,因此为了使用双亲委派模型,咱们应当把本身的类加载逻辑写道findClass()中。

第二次:

咱们有一些功能是java提供接口,而其余的公司提供实现类,例如咱们的JDBC、JNDI(由多个公司提供本身的实现)因此像JDBC、JNDI这样的SPI(服务提供者接口),就须要第三方实现,这些SPI的接口属于核心库,由Bootstrap类加载器加载,那么如何去加载那些公司提供的实现类呢?这就是咱们的线程上下文类加载器,下图是总体大概的工做流程

image
这里,线程上下文加载器默认是父类加载器是ApplicationClassLoader

第三次:

第三次破坏委派双亲模型就是因为用户追求动态性致使的,“动态性”就是指代码热替换、模块热部署等,就是但愿程序不须要重启就能够更新class文件,最典型的例子就是SpringBoot的热部署和OSGi。这里拿OSGi举例,OSGi实现模块化热部署的关键就是它自定义类加载机制的实现,每个程序模块(OSGi中称为Bundle)都有本身的类加载器,当须要更换一个Bundle时,就把Bundle连同类加载器一块儿换掉实现热部署

因此,在OSGi环境下,类加载器再也不是层次模型,而是网状模型,如图

image

当OSGi收到一个类加载的时候会按照如下的顺序进行搜索:

  • 将以java.*开头的类委派给父类加载器加载
  • 不然,将委派列表名单内的类委派给父类加载器加载
  • 不然,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
  • 不然,查找当前Bundle的ClassPath,使用本身的类加载器加载
  • 检查Fragment Bundle中是否能够加载
  • 查找Dynamic Import列表的Bundle
  • 若以上都没有进行类加载,则加载失败

以上前两点仍符合双亲委派规则,其他都是平级类加载器查找

Tomcat的类加载器模式

前文咱们了解了Java中类加载器的运行方式;但主流的Web服务器都会有本身的一套类加载器,为何呢?由于对于服务器来讲他要本身解决一些问题:

  • 部署在同一个Web容器上的两个Web应用程序所使用的Java类库能够实现相互隔离。两个不一样的应用程序可能会依赖同一个第三方类库的不一样版本,不能要求一个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库能够互相独立使用。
  • 部署在同一个Web容器上的两个Web应用程序所使用的相同的类库相同的版本能够互相共享。例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,若是把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到Web容器的内存,若是类库不能共享,虚拟机的方法区就会很容易出现过分膨胀的风险
  • Web容器须要尽量地保证自身的安全不受部署的Web应用程序影响。Web容器也有用Java实现的,那么确定不能把Web容器的类库和程序的类库弄混
  • 支持jsp的web容器,要支持热部署。咱们知道运行jsp时实际上会先将jsp翻译成servlet,再编译为.class再在虚拟机运行起来再返回给客户端。而咱们在编写jsp时,当tomcat服务器正在运行的时候,咱们直接在jsp中修改代码时并不须要重启服务器,这就是达到了动态加载类的效果。

显然,若是Tomcat使用默认的类加载机制是没法知足上述要求的

  1. 没法加载两个相同类库的不一样版本的,由于默认类加载只在意权限定类名,第一条不行
  2. 能够实现
  3. 默认类加载只在意权限定类明,因此第三条不行
  4. 前文咱们说过,JVM肯定是否为同一个类对象会要求类和类加载器都相同,默认的确定不行,但咱们能够想到当改变jsp代码的时候就改一次类加载器

接下来来看Tomcat的类加载器:

image
++一个WebAppClassLoader下可能还对应多个JspClassLoader++

再来讲说Tomcat的目录结构:

  • /common目录中:类库可被Tomcat和全部的Web应用程序共同使用。
  • /server目录中:类库可被Tomcat使用,对全部的Web应用程序都不可见。
  • /shared目录中:类库可被全部的Web应用程序共同使用,但对Tomcat本身不可见。
  • /WebApp/WEB-INF目录中:类库仅仅能够被此Web应用程序使用,对Tomcat和其余Web应用程序都不可见。

再来看一下具体每一个类加载器的加载流程:

CommonClassLoader能加载的类均可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader本身能加载的类则与对方相互隔离。WebAppClassLoader可使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并经过再创建一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

Tomcat 6.x把/common、/server和/shared三个目录默认合并到一块儿变成一个/lib目录,这个目录里的类库至关于之前/common目录中类库的做用

如今咱们再来看Tomcat时如何解决以前的四个问题的:

  • 部署在同一个Web容器上的两个Web应用程序所使用的Java类库能够实现相互隔离:各个WebAppClassLoader实例之间相互隔离
  • 部署在同一个Web容器上的两个Web应用程序所使用的相同的类库相同的版本能够互相共享:能够放在Common或Shared目录下让这些程序共享
  • Web容器须要尽量地保证自身的安全不受部署的Web应用程序影响:CatalinaClassLoader加载web服务器须要的类库,WebAppClassLoader只能获得SharedClassLoader的类库
  • 支持jsp的web容器,要支持热部署:每当改变jsp时,更新JasperClassLoader
问:前文说到若是咱们服务器上有十个Spring组织的程序,咱们能够把Spring放到Common或者Shared目录下共享,但Spring要进行类管理确定要访问到用户程序,即访问到不在他加载范围的用户程序,这要怎么实现呢?

前文咱们说过破坏委托模型,这里就是一个例子,能够采用线程上下文加载器,让父类加载器请求子类加载器完成加载类做用

常见加载类异常错误分析

ClassNotFoundException

这个错误是说当JVM加载指定文件的字节码到内存时,找不到相应的字节码。解决办法为在当前classpath目录下找有没有指定文件(this.getClass().getClassLoader().getResource("").toString()能够查看当前classpath)

NoClassDefFoundError

这种错误出现的状况就是使用了new关键字、属性引用某个类、继承某个接口或实现某个类或某个方法参数引用了某个类,这时虚拟机隐式加载这些类发现这些类不存在的异常。解决这个错误的办法就是确保每一个类引用的类都在当前的classpath下面

UnsatisfiedLinkError

多是在JVM启动的时候不当心在JVM中的某个lib删了

ClassCastException

没法转型,这个可能对于初学者来讲会很常见(好比说我,哈哈),解决办法时转型前先用instanceof检查是否是目标类型再转换

ExceptionInInitializerError

这个异常是因为类加载过程当中静态块初始化过程失败所致使的。因为它出如今负责启动程序的主线程中,所以你最好从主类中开始分析,这里说的主类是指你在命令行参数中指定的那个,或者说是你声明了public static void main(String args[])方法的那个类。这个异常很大可能会伴随NoClassDefFoundError,因此出现NoClassDefFoundError时咱们先看ExceptionInInitializerError出现没。

自定义类加载器

接下来咱们要本身写一个类加载器,在开始写以前,咱们要知道为何须要咱们本身写类加载器呢?

  • 咱们须要的类不必定存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径),对于自定义路径中的class类文件的加载,咱们须要本身的ClassLoader
  • 有时咱们不必定是从类文件中读取类,多是从网络的输入流中读取类,这就须要作一些加密和解密操做,这就须要本身实现加载类的逻辑,固然其余的特殊处理也一样适用。
  • 能够定义类的实现机制,实现类的热部署,如OSGi中的bundle模块就是经过实现本身的ClassLoader实现的。

下面咱们开始自定义类加载器吧

自定义File类加载

package SelfClassLoader;

import java.io.*;

public class FileClassLoader extends ClassLoader {
    private String rootDir;

    public FileClassLoader(String rootDir){
        this.rootDir = rootDir;
    }

    /**
     * 编写findClass方法的逻辑
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @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 className
     * @return
     */
    private byte[] getClassData(String className){
        //读取类文件的字节
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int bytesNumRead = 0;
            // 读取类文件的字节码
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 类文件的完整路径
     * @param className
     * @return
     */
    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

    /**
     * 读取文件
     */
    public static void main(String[] args) throws ClassNotFoundException {
        String rootDir="C:\\java\\JVM\\JVMInstruction\\src";
        //建立自定义文件类加载器
        FileClassLoader loader = new FileClassLoader(rootDir);

        try {
            //加载指定的class文件,加上包名
            Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj");
            System.out.println(object1.newInstance().toString());

            //输出结果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


}
复制代码

咱们经过getClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法建立了类的class对象。在main方法中调用了loadClass()方法加载指定路径下的class文件,因为启动类加载器、拓展类加载器以及系统类加载器都没法在其路径下找到该类,所以最终将有自定义类加载器加载,即调用findClass()方法进行加载。

还有一种方式是继承URLClassLoader类,而后设置自定义路径的URL来加载URL下的类,这种方式更常见

package SelfClassLoader;

import java.io.File;
import java.net.*;

public class PathClassLoader extends URLClassLoader {

    private String packageName = "net.lijunfeng.classloader";

    public PathClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public PathClassLoader(URL[] urls) {
        super(urls);
    }

    public PathClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException{
        Class<?> aClass = findLoadedClass(name);
        if (aClass != null){
            return aClass;
        }
        if (!packageName.startsWith(name)){
            return super.loadClass(name);
        } else {
            return findClass(name);
        }
    }

    public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
        String rootDir="C:\\java\\JVM\\JVMInstruction\\src";
        //建立自定义文件类加载器
        File file = new File(rootDir);
        //File to URI
        URI uri=file.toURI();
        URL[] urls={uri.toURL()};

        PathClassLoader loader = new PathClassLoader(urls);

        try {
            //加载指定的class文件
            Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj");
            System.out.println(object1.newInstance().toString());

            //输出结果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

总结

  • 咱们知道了类加载器的做用就是将.class文件存到内存当中去(两种)
  • 知道了它具体的步骤和每步的功能
  • 了解了Java三种类加载器以及双亲委派模型和破坏双亲委派模型
  • 了解了Tomcat的类加载机制须要解决的问题以及是怎么解决的
  • 了解了常见的类加载的异常和大体解决思路
  • 而且也知道了类加载机制大体的代码实现
  • 最后咱们写了一个自定义类加载器
相关文章
相关标签/搜索