• java深入学习2jvm_JVM的类加载机制(面试必备)


    虚拟机将描述类的二进制字节流(即Class文件)加载到内存中,并对其进行验证、准备、解析、初始化的过程,最终生成可以直接被虚拟机使用的Java类型(即已经校验合格且有clinit执行完clinit方法的Class对象),这就是JVM的类加载机制。

    程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载 class、还有 class 中的静态变量和常量到内存(方法区/元空间)当中的,只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

    class加载到JVM中到可运行被使用有几个阶段

    装载(loading)

    即找到class对应的字节码文件。
    对于数组类来说,它并没有对应的字 节流,而是由 Java 虚拟机直接生成的。对于其他的类来说,Java 虚拟机则需要借助类加载器来完 成查找字节流的过程。

    链接(linking)

    是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。

    验证:验证阶段的目的,在于确保被加载类能够满足 Java 虚拟机的约束条件。 通常而言,Java 编译器生成的类文件必然满足 Java 虚拟机的约束条件。
    准备:准备阶段的目的,则是为被加载类的静态字段分配内存。 除了分配内存外,部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。
    解析:非必须,如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析,解析成为实际引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

    初始化(initializing)

    只有当初始化完成之后,类才正式成为可执行的状态。

    如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中 对其赋值。如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会 被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的 直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 clinit

    只有一次

    为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。
    所以,类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化

    类的初始化何时会被触发呢?

    JVM 规范枚举了下述多种触发情况:

    1. 当虚拟机启动时,初始化用户指定的主类;
    2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;
    3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
    4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
    5. 子类的初始化会触发父类的初始化;
    6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;
    7. 使用反射 API 对某个类进行反射调用时,初始化这个类;
    8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

    Java 核心类库提供的三种类加载器

    BootStrap ClassLoader启动类加载器

    是由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来 指代。当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。
    在 Java 9 之前,启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。
    是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等

    除了启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中,方能 执行类加载。
    rt.jar、tools.jar和dt.jar作用

    Extension ClassLoader扩展类加载器

    扩展类加载器的父类加载器(不是继承的关系,是一个包含的关系)是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。
    负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。

    App ClassLoader系统类加载器

    应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程 序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所 指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。

    Java 9 的变化

    Java 9 引入了模块系统,并且略微更改了上述的类加载器1。扩展类加载器被改名为平台类加载器 (platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载 器加载之外,其他的模块均由平台类加载器所加载。

    双亲委托这种模型

    ClassLoader源码分析
    主要逻辑在loadClass(String name, boolean resolve)方法中。
    里面有个递归调用,当前类加载器如果存在父类加载器,就传入父类加载器然后继续调用该方法,此时会委派给父类加载器加载。
    所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。

    当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没找到该类,则把任务转交给Extension ClassLoader试图加载,如果也没找到该类,则转交给AppClassLoader进行加载,如果它也没有找到该类的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。
    如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象。

    为什么使用双亲委托这种模型呢?
    因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

    启动时如果加上如下系统参数,即可跟踪JVM类的加载-XX:+TraceClassLoading
    或者 -verbose:class JVM 参数,都可打印类加载的先后顺序

    命名空间的作用

    除了加载功能之外,类加载器还提供了命名空间的作用。
    在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

    JVM在搜索类的时候,又是如何判定两个class是相同的呢?
    JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

    Java中两种手动调用加载class到JVM中的方式

    默认会使用调用类的类加载器来进行类加载。

    Class.forName(“className”);

    其实这种方法调运的是:Class.forName(className, true, ClassLoader.getCallerClassLoader())方法
    参数一:className,需要加载的类的名称。
    参数二:true,是否对class进行初始化(需要initialize)
    参数三:classLoader,对应的类加载器

    ClassLoader.laodClass(“className”);

    其实这种方法调运的是:ClassLoader.loadClass(name, false)方法
    参数一:name,需要加载的类的名称
    参数二:false,这个类加载以后是否需要去连接(不需要linking)

    两种方式的区别

    forName("")得到的class是已经初始化完成的,加载并实例化JDBC驱动类时用到,因为JDBC驱动程序只有被注册后(将驱注册到DriverManageer上,之后才能通过DriverManager去获取相应的连接)才能被应用程序使用,这就要求驱动程序类必须被初始化,而不单单被加载。

    loadClass("")得到的class是还没有连接的。

    一般情况下,这两个方法效果一样,都能装载Class,但如果程序依赖于Class是否被初始化,就必须用Class.forName(name)了。

    自定义ClassLoader

    除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,来实现特殊的加载方式。比如我们可以对 class 文件进行加密,加载时再利用自定义的类加载器对其解密。
    这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,

    看到的相关的

    更多类加载请看

  • 相关阅读:
    阿里云自定义镜像保存规则
    linux常用命令
    SQL
    redis集群
    AOP中的通知
    mysql8.0无法给用户授权或提示You are not allowed to create a user with GRANT的问题
    ---Linux命令
    feign
    spring clud / dubbo
    Ajax嵌套问题
  • 原文地址:https://www.cnblogs.com/cashew/p/11597651.html
Copyright © 2020-2023  润新知