java.lang包—类加载器ClassLoader类

注意:

  • 类加载器的知识与JVM内存模型紧密相连,要学好这块的知识,一定要掌握JVM的内存模型。
  • 关于JVM内存模型,推荐阅读:JVM—内存模型JMM

目录

一、什么是ClassLoader?

二、类加载过程

三、类加载器种类及范围

四、ClassLoader加载类的原理


一、什么是ClassLoader?

大家都知道,当我们写好一个Java程序之后,不是管是CS还是BS应用,都是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

重点:class加载到内存指的是加载到JVM内存中,且class文件中的方法存储在JVM的方法区。

二、类加载过程

java.lang包—类加载器ClassLoader类_第1张图片

加载将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在内存上创建一个java.lang.Class对象用来封装类在方法区内的数据结构作为这个类的各种数据的访问入口

「验证」主要是为了确保class文件中的字节流包含的信息是否符合当前JVM的要求,且不会危害JVM自身安全,比如校验文件格式、是否是cafe baby魔术、字节码验证等等。

准备类变量(也叫静态变量)是被static修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。实例变量不会在这个阶段分配内存,它会在对象实例化的时候随着对象一起分配在java堆中。比如:

//类变量age会在准备阶段过后为 其分配四个(int四个字节)字节的空间,并且设置初始值为0,而不是26。若是final的,则在编译期就会设置上最终值。
private static int age = 26;

「解析」JVM会在此阶段把类的二进制数据中的符号引用替换为直接引用

初始化初始化阶段即虚拟机执行类构造器clinit() 方法的过程,对类的静态方法、静态变量和静态代码块初始化的过程。到了初始化阶段,才真正开始执行类定义的Java程序代码(或者说字节码 )。比如准备阶段的那个age初始值是0,到这一步就设置为26。

「使用」对象都出来了,业务系统直接调用阶段。

「卸载」用完了,可以被GC回收了。

类加载过程最重要的3个阶段是:加载、准备、初始化,简单总结这三个过程就是如下这个过程:

  • 加载:类文件加载到JVM方法区中
  • 准备:在JVM方法区中为静态变量开辟内存地址,初始值全部为0或null
  • 初始化:为静态变量赋值,执行静态方法和静态代码块的代码

三、类加载器种类及范围

java.lang包—类加载器ClassLoader类_第2张图片

1、BootStrap ClassLoader:

最顶层类加载器,他的父类加载器是个null,也就是没有父类加载器。负责加载jvm的核心类库,比如java.lang.*等,从系统属性中的sun.boot.class.path所指定的目录中加载类库。他的具体实现由Java虚拟机底层C++代码实现。

System.out.println(System.getProperty("sun.boot.class.path"));

打印结果:

  • C:\Program Files\Java\jdk1.6.0_22\jre\lib\resources.jar;
  • C:\Program Files\Java\jdk1.6.0_22\jre\lib\rt.jar;
  • C:\Program Files\Java\jdk1.6.0_22\jre\lib\sunrsasign.jar;
  • C:\Program Files\Java\jdk1.6.0_22\jre\lib\jsse.jar;
  • C:\Program Files\Java\jdk1.6.0_22\jre\lib\jce.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\charsets.jar;
  • C:\Program Files\Java\jdk1.6.0_22\jre\classes
  • URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();  

2、Extension ClassLoader

父类加载器是Bootstrap ClassLoader。从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的JAVA_HOME/jre/lib/ext/子目录(扩展目录)下加载类库,如果把用户的jar文件放在这个目录下,也会自动由扩展类加载器加载。继承自java.lang.ClassLoader

3、App ClassLoader

父类加载器是Extension ClassLoader。从环境变量classpath或者系统属性java.class.path所指定的目录中加载类。继承自java.lang.ClassLoader

4、自定义类加载器(User ClassLoader)

除了上面三个自带的以外,用户还能制定自己的类加载器,但是所有自定义的类加载器都应该继承自java.lang.ClassLoader。比如热部署、tomcat都会用到自定义类加载器。

// sun.misc.Launcher

public class Launcher {
    // Bootstrap类加载器的加载路径,在static静态代码块里用的
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    
    // AppClassLoader 继承 ClassLoader
    static class AppClassLoader extends URLClassLoader {
        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            // java.class.path
            final String var1 = System.getProperty("java.class.path");
        }
    }
    
    // ExtClassLoader 继承 ClassLoader
    static class ExtClassLoader extends URLClassLoader {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            // java.ext.dirs
            String var0 = System.getProperty("java.ext.dirs");
        }
    }   
}

四、ClassLoader加载类的原理

1、原理介绍

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,他首先会从自己缓存里查找是否之前加载过这个class,加载过直接返回,没加载过的话他不会自己亲自去加载,他会把这个请求委派给父类加载器去完成,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。如下图所示,红色框表示各个类加载器:

正向调用过程: .class文件先申请Custom加载器加载,Custom加载器缓存中有的话直接返回,没有则调用App加载器进行加载;App加载器缓存中有的话直接返回,没有则调用Ext加载器;Ext加载器缓存中有的话直接返回,没有则调用Bootstrap加载器。

反向调用过程:Bootstrap加载器缓存中有的话直接返回,没有则回调Ext加载器进行加载;Ext进行加载,加载到返回成功,加载不到回调App加载器;App进行加载,加载到返回成功,加载不到回调Custom加载器;Custom进行加载,加载到返回成功,加载不到返回失败;

java.lang包—类加载器ClassLoader类_第3张图片 双亲委派模型

源码在java.lang.ClassLoader#loadClass(java.lang.String, boolean)

3、为啥要有双亲委派

防止内存中出现多份同样的字节码,安全。比如自己重写个java.lang.Object并放到Classpath中,没有双亲委派的话直接自己执行了,那不安全。双亲委派可以保证这个类只能被顶层Bootstrap Classloader类加载器加载,从而确保只有JVM中有且仅有一份正常的java核心类。如果有多个的话,那么就乱套了。比如相同的类instance of可能返回false,因为可能父类不是同一个类加载器加载的Object。

4、为什么需要破坏双亲委派模型

1)Jdbc

Jdbc为什么要破坏双亲委派模型?

以前的用法是未破坏双亲委派模型的,比如Class.forName("com.mysql.cj.jdbc.Driver");而在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver文件中指明当前使用的Driver是哪个,然后使用的时候就不需要我们手动的去加载驱动了,我们只需要直接获取连接就可以了。Connection con = DriverManager.getConnection(url, username, password );

首先,理解一下为什么JDBC需要破坏双亲委派模式,原因是原生的JDBC中Driver驱动本身只是一个接口,并没有具体的实现,具体的实现是由不同数据库类型去实现的。例如,MySQL的mysql-connector-*.jar中的Driver类具体实现的。原生的JDBC中的类是放在rt.jar包的,是由Bootstrap加载器进行类加载的,在JDBC中的Driver类中需要动态去加载不同数据库类型的Driver类,而mysql-connector-*.jar中的Driver类是用户自己写的代码,那Bootstrap类加载器肯定是不能进行加载的,既然是自己编写的代码,那就需要由Application类加载器去进行类加载。这个时候就引入线程上下文件类加载器(Thread Context ClassLoader),通过这个东西程序就可以把原本需要由Bootstrap类加载器进行加载的类由Application类加载器去进行加载了。

2)Tomcat

Tomcat为什么要破坏双亲委派模型?

因为一个Tomcat可以部署N个web应用,但是每个web应用都有自己的classloader,互不干扰。比如web1里面有com.test.A.class,web2里面也有com.test.A.class,如果没打破双亲委派模型的话,那么web1加载完后,web2在加载的话会冲突。

因为只有一套classloader,却出现了两个重复的类路径,所以tomcat打破了,他是线程级别的,不同web应用是不同的classloader。

  • Java spi 方式,比如jdbc4.0开始就是其中之一。
  • 热部署的场景会破坏,否则实现不了热部署。

5、如何破坏双亲委派模型

重写loadClass方法,别重写findClass方法,因为loadClass是核心入口,将其重写成自定义逻辑即可破坏双亲委派模型。

6、如何自定义一个类加载器

只需要继承java.lang.Classloader类,然后覆盖他的findClass(String name)方法即可,该方法根据参数指定的类名称,返回对应 的Class对象的引用。

7、热部署原理

采取破坏双亲委派模型的手段来实现热部署,默认的loadClass()方法先找缓存,你改了class字节码也不会热加载,所以自定义ClassLoader,去掉找缓存那部分,直接就去加载,也就是每次都重新加载。

五、常见笔试题

1)以下代码的输出结果是什么?

public class Test1 {
    static {
        // 编译没报错
        i = 2;
        // 编译报错Illegal forward reference
        System.out.println(i);
    }
    private static int i =1;
    public static void main(String[] args) {
    }
}

答案:编译报错,如下图所示(IDea会直接提示报错,想编译的话在命令行中编译吧)

java.lang包—类加载器ClassLoader类_第4张图片

代码分析:准备阶段为变量i在JVM分配内存地址。初始化阶段为变量赋值,在这段代码中存在两个赋值动作,一个是int i =1   还有一个是i = 2 ,这两个动作都是在类加载的初始化阶段执行的,那么i最后的值到底是1还是2呢?这个是不好确定的,所以JVM不允许前向引用(这个其实就是c语言里面的不允许前向引用,毕竟JVM底层也是c写的)。去掉语句System.out.println(i);编译通过

 

2):输出结果是什么?

public class Test2 {
    private static Test2 test2 = new Test2();
    private static int value1;
    private static int value2 = 3;

    private Test2() {
        value1 ++;
        value2 ++;
    }

    public static void main(String[] args) {
        // 1
        System.out.println(test2.value1);
        // 3
        System.out.println(test2.value2);
    }
}

答案 :1、3。因为类加载的准备过程中会先准备类变量(也就是静态变量),此时test2=null,value1=0,value2=0,然后进入初始化阶段,先执行test2=new Test2(),会执行构造器,结果是value1 = 1,value2 = 1,然后执行value1和value2这两句,value1没变化,value2被重新赋值成了3,所以结果1和3。

3)如果把private static Test2 test2 = new Test2();放到private static int value2 = 3;下面的话结果就是1和4了。

public class Test3 {
    private static int value1;
    private static int value2 = 3;
    private static Test3 test3 = new Test3();
    
    private Test3() {
        value1 ++;
        value2 ++;
    }
    
    public static void main(String[] args) {
        // 1
        System.out.println(test3.value1);
        // 4
        System.out.println(test3.value2);
    }
}

读后有收获可以支付宝请作者喝奶茶 

 

你可能感兴趣的:(后端—开发语言—Java)