类加载

作者: jection | 来源:发表于2019-01-06 19:33 被阅读0次

    类的生命周期

    java类执行过程 类的生命周期图

    类加载过程包括:加载-验证-准备-解析-初始化。
    这个过程顺序并不是固定的,最多仅仅代表它们开始的顺序,实际上这五个过程是交叉进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

    注意:解析阶段有可能在初始化阶段之后再开始。

    生命周期过程详解

    1. 加载:一、将class文件加载在内存中;二、将静态数据结构(数据存在于class文件的结构)转化成方法区中运行时的数据结构(数据存在于JVM时的数据结构);三、在堆中生成一个代表这个类的java.lang.Class对象,作为数据访问的入口。
    2. 验证:确保加载的类符合JVM规范与安全。
    3. 准备:为static变量在方法区中分配空间,设置变量的初始值。例如static int a=3,在此阶段会a被初始化为0,其他数据类型参考成员变量声明。
    4. 解析:虚拟机将常量池的符号引用转变成直接引用。例如"aaa"为常量池的一个值,直接把"aaa"替换成存在于内存中的地址。
      • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形 式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用 的目标并不一定已经加载到内存中。
      • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在。
    5. 初始化:初始化阶段是执行类构造器<clinit>()方法。在类构造器方法中,它将由编译器自动收集类中的所有类变量的赋值动作(准备阶段的a正是被赋值a)和静态变量与静态语句块static{}合并,初始化时机后续再聊。
    6. 使用:正常使用。
    7. 卸载:GC把无用对象从内存中卸载。

    类初始化时机

    只有当对类主动使用的时候才会导致类的初始化。

    主动引用(发生类初始化过程)
    1. 创建类的实例,也就是new的方式
    2. 访问某个类或接口的静态变量,或者对该静态变量赋值
    3. 调用类的静态方法
    4. 反射(如Class.forName(“com.shengsiyuan.Test”))
    5. 初始化某个类的子类,则其父类也会被初始化
    6. Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
    被动引用(不会发生类的初始化)
    1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
    2. 定义对象数组,不会触发该类的初始化。
    3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
    4. 通过类名ClassName.class获取Class对象,不会触发类的初始化。
    5. Class.forName("类名全路径")加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
    6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

    类加载器

    主要的类加载器有

    1. 启动类加载器(Bootstrap ClassLoader):由C++实现,没有父类,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
    2. 扩展类加载器(Extension ClassLoader):由Java语言实现,父类加载器为null,负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
    3. 应用程序类加载器(Application ClassLoader):由Java语言实现,父类加载器为ExtClassLoader,负责加载用户路径(classpath)上的类库。
    4. 自定义加载器:可以通过继承java.lang.ClassLoader或URLClassLoader实现自定义的类加载器,父类加载器为AppClassLoader。
    双亲委派模式
    image.png

    JVM使用的是双亲委派模式的类加载机制,这种机制的好处有:

    1. 避免类的重复加载
    2. 保证核心基础jar包加载的安全问题。

    加载过程大致可以分为两个过程:

    1. 查找过程,首先从收到请求的加载器开始,从下到上查找是否加载过该类,从下到上的顺序:自定义类加载器->应用类加载器->拓展类加载器->启动类加载器;
    2. 加载过程,如果找不到加载过该类,则从上到下,从启动类加载器开始尝试从自己负责的路径下加载该类,如果加载失败则由下级加载器加载,从上到下的顺序:启动类加载器 -> 拓展类加载器 ->应用类加载器 -> 自定义类加载器。
    加载器类图
    image.png

    从类图可以看出

    • 拓展类加载器ExtClassLoader和系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。
    • sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的。
    • ExtClassLoader并没有重写loadClass()方法,而AppClassLoader重载了loadCass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。
    自定义加载器

    方式

    1. 继承URLClassLoader,不需要重写 findClass()等方法,继承双亲委派模式。
    2. 继承ClassLoader,只重写findClass方法,这种方式继承双亲委派模式。
    3. 继承ClassLoader,重写loadClass方法,不使用双亲委派模式,自定义类加载模式。

    自定义加载器场景:

    1. 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。
    2. 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。
    3. 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。

    注意:由于双亲委托机制,classpath目录下的类默认由Application类加载器加载,所以,自定义加载器如果没有重写loadClass方法去加载classpath下的类是不会成功的,当然,可以通过直接调findClass()绕过双亲委托来加载。

    双亲委派模型的破坏者1——线程上下文加载器
    image.png

    在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。

    加载方式
    1. 显式加载:指的是在代码中通过调用ClassLoader加载class对象,比如代码中通过Class.forName()、this.getClass.getClassLoader.LoadClass(),自定义类加载器中的findClass()方法等。
    2. 隐式加载:不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,除了显式加载的,其它都是隐式加载。
    ClassLoader关键方法
    • loadClass(String),该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,所以,自定义加载器可以覆盖loadClass方法覆盖双亲委派模式。
    • findClass(String) ,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
    • defineClass(byte[] b, int off, int len) ,是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象。
    • resolveClass(Class≺?≻ c) ,类解析阶段的操作方法,给Classloader链接一个类。

    注意:
    如果直接调用defineClass()或findClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

    重复类加载

    Java中来自不同Jar包中的相同的类名(包名,类名)在加载时类加载器将按照Class Path中的顺序加载,相同的类名仅仅会加载一次,顺序排在前面的类会被加载,加载成功后,后面再遇到相同类会忽略。
    因此,最终所使用的类取决于ClassLoader对类的的选择,即Maven往Class Path打包的顺序。

    类卸载

    类加载后JVM内存图

    JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

    1. 该类所有的实例都已经被GC。
    2. 该类的java.lang.Class对象没有在任何地方被引用。
    3. 加载该类的ClassLoader实例已经被GC。

    所以

    • 由Java虚拟机自带的类加载器(启动类加载器、扩展类加载器、应用程序类加载器)所加载的类,在虚拟机的生命周期中,始终不会被卸载。因为Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。
    • 由用户自定义的类加载器加载的类是可以被卸载的,如下图使用自定义加载器加载的类和对象,只要左侧三个引用变量置为null,类加载器生命周期结束、加载的Class对象生命周期结束、Sample对象生命周期结束,方法区的类信息也会被卸载。

    是否同个类

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

    1. 类的完整类名必须一致,包括包名。
    2. 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

    获取Class对象的三种方式

    Class类是反射机制的起源,我们得到Class类对象有3种方法:
    第一种:通过类名获得
      Class<?> class = ClassName.class;
    第二种:通过类名全路径获得:
      Class<?> class = Class.forName("类名全路径");
    第三种:通过实例对象获得:
      Class<?> class = object.getClass();

    Class.forName()和ClassLoader.loadClass()区别

    • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
    • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

    注:
    Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

    相关文章

      网友评论

          本文标题:类加载

          本文链接:https://www.haomeiwen.com/subject/zlighqtx.html