类加载原理简介

2020-06-08  本文已影响0人  站在海边看远方

类加载器(ClassLoader),用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。

类加载器,负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。

类加载的过程

从下面这张图上来看一下类加载的过程


image.png

类加载过程分为3步

其中连接过程内部又分为3个步骤

使用和卸载属于类加载完成之后的步骤,不属于类加载过程。
我们下面来看一下这3个过程分别做了什么工作

加载

加载(Loading)属于类加载过程的第一步,在这一步,虚拟机主要完成了下面这3件事

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

虚拟机规范多上面这3点并不具体,因此是非常灵活的。比如:"通过全类名获取定义此类的二进制字节流" 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)、从远端或者网络上读取等等。

连接

连接(Linking)阶段会做3件事,做必要的验证和数据准备,以及符合引用的解析

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段大致会完成4个阶段的检验动作:

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用 -Xverifynone 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

准备阶段为类的静态变量分配内存,并将其初始化为默认值

准备阶段,是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

解析

解析步骤是把类中的符号引用转换为直接引用

解析阶段,是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作,主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。

初始化

初始化可以为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要是对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:

1、声明类变量是指定初始值。
2、使用静态代码块为类变量指定初始值。

类加载器介绍

3个重要的内置类加载器

JAVA的类加载是通过类加载器完成的,JVM有3个重要的类加载器,我们通过一张图来了解一下类加载器的层次结构


类加载器层次结构
  1. Bootstrap ClassLoader :根类加载器,负责加载 Java 的核心类,包括%JAVA_HOME%/lib目录下的jar包和类或者或被 -Xbootclasspath参数指定的路径中的所有类。
    它不是 java.lang.ClassLoader 的子类,而是由 JVM 自身使用C++语言实现的。
  2. Extension ClassLoader :扩展类加载器,扩展类加载器的加载路径是 JDK 目录下 jre/lib/ext ,或被 java.ext.dirs 系统变量所指定的路径下的jar包。
    扩展加载器的 getParent() 方法返回 null ,实际上扩展类加载器的父类加载器是根加载器,只是根加载器并不是 Java 实现的。
  3. AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用classpath下的所有jar包和类。

代码示例

我们通过一个示例来看一下类加载器的父类加载器

public class ClassLoaderTest {
    public static void main(String[] args) {

        System.out.println(ClassLoaderTest.class.getName()+"'s classLoader is:" +ClassLoaderTest.class.getClassLoader());
        System.out.println(ClassLoaderTest.class.getName()+"'s parent is: "+ClassLoaderTest.class.getClassLoader().getParent());
        System.out.println(ClassLoaderTest.class.getName()+"'s grandParent is: "+ClassLoaderTest.class.getClassLoader().getParent().getParent());
    }
}
Snipaste_2020-06-04_19-51-48.png

可以看到ExtClassLoader的父类加载器是null,null并不是代表ExtClassLoader没有父类加载器,而是Bootstrap ClassLoader,是用C++实现的。

父子类加载器关系实现的方式

这个类加载器的模型要求除了顶层的Bootstrap ClassLoader之外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码,看一下ClassLoader类的定义


image.png

jvm如何判断2个类是否相同

Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

比如一个 Java 类 com.example.Sample ,编译之后生成了字节代码文件 Sample.class 。两个不同的类加载器 ClassLoaderA 和 ClassLoaderB 分别读取了这个 Sample.class 文件,并定义出两个 java.lang.Class 类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException 。

双亲委派模型

说到类加载和类加载器,就不得不提双亲委派模型。
每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 双亲委派模型 。

双亲委派模型工作流程

  1. 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。

每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。

2.加载的时候,首先会把该请求委派该父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 BootstrapClassLoader 作为父类加载器。

从上面类加载器的层次关系一图中可以看出,类加载过程是自底项上检查类是否被加载,自顶向下尝试加载类。

双亲委派模型源码解析

相关代码在 java.lang.ClassLoader#loadClass(java.lang.String, boolean) 方法里

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查类是否已被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //如果父加载器不为空,尝试用父类加载器去加载
                        c = parent.loadClass(name, false);
                    } else {
                        //如果父加载器为空,使用Bootstrap去加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                     //抛出异常说明父类加载器无法完成加载请求
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果仍未发现,尝试自己去加载类
                    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;
        }
    }

代码相对来说比较简单

双亲委派模型的好处

根据双亲委派模型的定义,是优先使用父类加载器进行加载,一层一层向上找,如果实在找不到了就自己尝试加载,这样有2个好处

如何破坏双亲委派模型

有时候,为了一些特殊目的,要破坏双亲委派模型,我们只要自定义一个类去继承java.lang.ClassLoader,然后重写loadClass()方法,不要去使用父类加载器加载即可。

总结

本文从类加载的流程说起,详细介绍了类加载的步骤,然后讲到了双亲委派模型,了解了一下loadClass()方法,类加载在面试中算是一个比较重要,但是并不太难的知识点,深入了解类加载机制对平时的工作和面试帮助会有不少帮助。

参考文章:
类加载过程
虚拟机类加载机制

最后,留2道思考题:

  1. 假设我要加载一个从远端或者网站上的一个类,应该用哪个classLoader?
  2. 如果一个字段同时被但是如果同时被final和static修饰,他在哪个阶段被设置值?

最后欢迎扫描二维码关注我的公众号:站在海边看远方

qrcode_for_gh_f47e9bf5c35d_258.jpg
上一篇下一篇

猜你喜欢

热点阅读