zoukankan      html  css  js  c++  java
  • JVM 类加载机制详解

    如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。

    加载

    加载主要是将.class文件(并不一定是.class。可以是ZIP包,网络中获取)中的二进制字节流读入到JVM中。
    在加载阶段,JVM需要完成3件事:
    1)通过类的全限定名获取该类的二进制字节流;
    2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    验证

    验证是连接阶段的第一步,主要确保加载进来的字节流符合JVM规范。 
    验证阶段会完成以下4个阶段的检验动作: 
    1)文件格式验证 
    2)元数据验证(是否符合Java语言规范) 
    3)字节码验证(确定程序语义合法,符合逻辑) 
    4)符号引用验证(确保下一步的解析能正常执行)

    准备

    准备阶段是正式为静态变量分配内存并设置初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:

    1
    public static int v = 8080;

    实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器<client>方法之中,这里我们后面会解释。
    但是注意如果声明为:

    1
    public static final int v = 8080;

    在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。

    解析

    解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的:

    • CONSTANT_Class_info
    • CONSTANT_Field_info
    • CONSTANT_Method_info

    等类型的常量。

    初始化

    初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。 
    注: 
    1)当有父类且父类未初始化的时候,先去初始化父类; 
    2)再进行子类初始化语句。

    什么时候需要对类进行初始化?
    1)使用new该类实例化对象的时候;
    2)读取或设置类静态字段的时候(但被final修饰的字段,在编译器时就被放入常量池的静态字段除外static final);
    3)调用类静态方法的时候;
    4)使用反射Class.forName(“xxxx”)对类进行反射调用的时候,该类需要初始化;
    5) 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化);
    6) 被标明为启动类的类(即包含main()方法的类)要初始化;
    7)当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

    以上情况称为对一个类进行主动引用,且有且只要以上几种情况需要对类进行初始化。

    注意以下几种情况不会执行类初始化:

    • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
    • 定义对象数组,不会触发该类的初始化。
    • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
    • 通过类名获取Class对象,不会触发类的初始化。
    • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
    • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

    类加载器

    虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:

    • 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOMElib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
    • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOMElibext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
    • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

    JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

    双亲委派模型的工作过程

    当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

    采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

    在有些情境中可能会出现要我们自己来实现一个类加载器的需求,由于这里涉及的内容比较广泛,我想以后单独写一篇文章来讲述,不过这里我们还是稍微来看一下。我们直接看一下jdk中的ClassLoader的源码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
    • 首先通过Class c = findLoadedClass(name);判断一个类是否已经被加载过。
    • 如果没有被加载过执行if (c == null)中的程序,遵循双亲委派的模型,首先会通过递归从父加载器开始找,直到父类加载器是Bootstrap ClassLoader为止。
    • 最后根据resolve的值,判断这个class是否需要解析。

    而上面的findClass()的实现如下,直接抛出一个异常,并且方法是protected,很明显这是留给我们开发者自己去实现的,这里我们以后我们单独写一篇文章来讲一下如何重写findClass方法来实现我们自己的类加载器。

    1
    2
    3
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
     

    破坏双亲委派模型

    双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。 

    若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了。 

    下面介绍两个例子来讲解破坏双亲委派模型的过程。

    • JNDI破坏双亲委派模型

    JNDI是Java标准服务,它的代码由启动类加载器去加载。但是JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。 

    为了解决这个问题,引入了一个线程上下文类加载器。 可通过Thread.setContextClassLoader()设置。 

    利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。

    • Spring破坏双亲委派模型

    Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。 那么Spring是如何访问WEB-INF下的用户程序呢? 

    使用线程上下文类加载器。 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器):setContextClassLoader(AppClassLoader)。 利用这个来加载用户程序。即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。

  • 相关阅读:
    web 调用网页版百度地图
    jquery的一些链式绑定事件
    li上设定样式display:inline-block时左边出现莫名空格以及其解决方法
    jquery-ui里日期插件的使用
    JavaScript中大于符号与小于符号的问题
    event对象的target属性和currentTarget属性
    git常用命令
    获取远程库时,出现错误的解决
    学习git遇到的问题的提出与总结
    Test Plan and Design Verification Environment
  • 原文地址:https://www.cnblogs.com/kaleidoscope/p/9792448.html
Copyright © 2011-2022 走看看