美文网首页
JVM类加载总结

JVM类加载总结

作者: 白花蛇草可乐 | 来源:发表于2019-09-28 16:04 被阅读0次

    JVM类加载总结

    1、概述

    类加载的过程,就是将类的字节码装载到内存方法区的过程(方法区的相关知识参看Java内存模型)。

    与C语言这样需要在运行前就进行链接(Link)的语言不通,Java语言中类型的加载、链接、初始化都是在程序运行期间完成的。

    这种策略为Java应用程序提高了极大的动态灵活性。

    Java虚拟机(JVM)中用来完成类加载的具体实现,是类加载器。

    类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例。每个实例用来表示一个java类。通过该实例的newInstance()方法可以创建出一个该类的对象。

    (我们通常会说方法区中存的是类,实际上存的也是实例,只不过是特殊实例,是Class这个类的实例)

    加载类的全流程如下图所示(简书这货居然不支持流程图和甘特图,蛋疼……)。

    类加载流程图

    2、类加载流程

    类加载的流程主要分为加载、链接、初始化三个阶段。其中链接又细分为验证、准备、解析三个阶段。

    2-1、加载(Loading)

    加载阶段jvm主要做三件事

    1. 通过类的“全限定名”获取量二进制字节流

      这里会说成是“二进制字节流”,是因为class文件的来源非常广。除了最常见的jar、war文件,还可以从网络获取,可以运行时冻土工程,由其他文件生成(比如jsp),甚至直接从数据库读取。

    2. 将字节流变成方法区的运行时数据结构

    3. 生成代表这个类的java.lang.Class对象(这个对象也放在方法区中),作为方法区中其对应的类型数据的访问入口

    2-2、验证(Verification)

    保证读入的class文件流是合法的——符合当前jvm版本的要求,更重要的是不会危及jvm安全。

    毕竟java编译并不是class文件的唯一来源,而且class文件也是很容易篡改的。

    2-3、准备(Preparation)

    在方法区中,为类变量分配内存并分配初始值。

    注意这里的初始值指的是“零值”,比如数值为各种0(0、0L、0.0f),String为null。

    比如

    public static int classint = 123;
    

    那么在这一阶段 classint 的值为0。因为现在还没有执行任何java方法,赋值123这个动作是在类构造器的<clinit>()方法中的。

    唯一的特例:

    public static final int classint = 123;
    

    使用final修饰的变量实际上就是常量(ConstantValue属性),其赋值与java方法无关,在准备阶段会直接赋值。

    2-4、解析(Resolution)

    将常量池中的“符号引用”替换为“直接引用”的过程。

    • 符号引用:以一组符号来描述所引用的目标。与虚拟机当前内存状况无关,需要引用的目标未必已经加载。
    • 直接引用:直接指向目标的指针、相对偏移量或者是间接定位用的句柄。

    A:“班费交给谁?”

    B:“交给班长”

    A:“谁是班长,坐在哪儿?”

    B:“我也不知道,反正我知道要交给班长” —— 符号引用

    C:“我知道,他坐在第二排靠窗的座位” —— 直接引用

    2-5、初始化(Initialization)

    初始化,就是执行类构造器<clinit>()方法的过程。

    所谓的<clinit>()方法,是编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并而成的。顺序为该语句在源文件中的顺序。

    同时,虚拟机会保证首先执行父类的<clinit>()方法,然后才是子类的<clinit>()。
    所以最先执行的是Object的<clinit>();
    并且父类的静态代码块一定先于子类被执行。

    一个很重要的特性(其实是面试时常问):一个类只会被初始化一次。

    3、类的主动引用(何时触发类的初始化)

    以外几种场景(动作)被称为类的主动引用

    • 最常见的场景:使用new关键字实例化对象
    • 读取或者为一个类的静态变量赋值(但是final修饰的除外)
    • 调用一个类的静态方法
    • 使用反射对类进行引用
    • 初始化一个类时,如果其父类还没有被初始化(复习一下上一节的知识哈),则先对父类进行类加载
    • 虚拟机启动时,会先初始化包含main()方法的那个类(主类)

    接口比较特殊:子接口初始化时,并不要求父接口完成初始化。

    4、类的被动引用(何时不触发类的初始化)

    • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化(只有真正声明这个静态变量的类才会被初始化)
    • 通过数组定义类,不会触发此类的初始化 A[] a = new A[10];
    • final修饰的常量,编译期间存入常量池,引用它不会触发定义常量所在的类(本质上并没有直接引用定义常量的类)。
    • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化(这个参数是告诉虚拟机,是否要对类进行初始化)
    • ClassLoader默认的loadClass方法,也不会触发初始化动作。

    5、类加载器

    最开始就提过,jvm中用来完成类加载的具体实现,就是类加载器。类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例。

    在jvm中,任意一个类,都是通过 “加载它的类加载器” + “类本身” 来确定其唯一性的。

    也就是说,即使对于同一个类,被不同的类加载器加载过两次,对于jvm来说也是不相等的。

    java中的类加载器有以下几种:

    5-1、启动类加载器 Bootstrap ClassLoader

    使用 C++编写的。(其他类加载器都是使用Java编写,继承自 java.lang.ClassLoader)

    负责加载:

    存放在<JAVA_HOME>\lib目录下的,或者被 -Xbootclasspath 参数指定的路径中的,

    并且是虚拟机识别的类库(仅按照文件名识别,比如rt.java,名字不符合的类库即使放在lib目录中也不被加载)。

    5-2、扩展类加载器 Extension ClassLoader

    负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量指定路径中的类库

    5-3、应用程序类加载器 Application ClassLoader

    这个类加载器是ClassLoader中getSystemClassLoader()方法的返回值,一般也称为系统类加载器。

    负责加载用户类路径(ClassPath)上指定的所有类库。

    应用程序中如果没有自定义过自己的类加载器,默认使用这个类加载器。

    6、双亲委派模式

    6-1、什么是双亲委派模式

    只看UML图的话,ExtClassLoader和AppClassLoader是同级的,不存在继承、依赖关系(两者都存放在Launcher类中)

    image

    (Bootstrap ClassLoader是C++编写的,不在上图中)

    但实际上,在类加载的时候,各个类加载器之间是有先后关系的。

    jvm加载类时,调用类加载器的顺序是这样的(自顶向下尝试加载类/自底向上委派):

    双亲委派模式

    每一个类加载器在获得类加载请求时,自己不动手,都优先向自己的“上级”发起类加载请求,一直到最基本的启动类加载器;

    然后从最上层开始,逐级判断这个类应不应该是自己负责加载的,如果是就加载,不是就将请求打回,由自己的下一级进行判断。

    也就是说,不管什么类,都是一定由最上级(parents)的类加载器首先进行判断是否加载,下级只负责将请求上传,工作不被甩到自己头上是绝不会主动去干的。这种模式被称为“双亲委派模式”。

    下面是该模式的时序图

    双亲委派时序图

    6-2、双亲委派的好处

    Java类随着他的类加载器一起具备了一种带有优先级的层次关系。

    比如Object存放在rt.jar里面,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。

    否则系统中会出现多个不同的Object类。

    可以自己尝试一下自定义一个 java.lang.Object,然后用自定义类加载器去加载(破坏掉双亲委派机制,不让委派给其他类加载器)。这样一个Object可以被加载,但是由于其类加载器不同,jvm仍然不会将它当做所有类的基类来对待。

    6-3、解读源码

    我们来看一下 Launcher 类的源码,理解一下双亲委派是如何工作的。

    (注意一下,下文的“父、类加载器”,不要理解成“父类、加载器”,仅仅指双亲委派时的顺序,无关继承关系)

    public class Launcher {
        private static Launcher launcher = new Launcher();
        
        // 启动类加载器要读取被 -Xbootclasspath 参数指定路径中的类库,所以这里读取系统参数中的路径名
        private static String bootClassPath = System.getProperty("sun.boot.class.path");
        
        private ClassLoader loader;
    
        public Launcher() {
            // 创建扩展类加载器对象。其中会读取系统参数 System.getProperty("java.ext.dirs")
            ClassLoader extcl;
            try {
                extcl = ExtClassLoader.getExtClassLoader();
            } catch (IOException e) {
                throw new InternalError("Could not create extension class loader", e);
            }
    
            // 创建应用程序类加载器。参数中使用了扩展类加载器的对象,是为了设定亲子关系,将其设定为自己的父类加载器(上一级)
            try {
                loader = AppClassLoader.getAppClassLoader(extcl);
            } catch (IOException e) {
                throw new InternalError("Could not create application class loader", e);
            }
    
            Thread.currentThread().setContextClassLoader(this.loader);
            ………………
        }
    
    

    上面一段代码中,两级类加载器调用的 .getxxxClassLoader()方法,其实都去调用了Launcher的父类(URLClassLoader)的构造方法,它们需要传的参数,是自己的父类加载器。

    这里,扩展类加载器传的是null,应用程序类加载器传的是扩展类加载器。

    之所以会传null,可以直接看基类 ClassLoader 的 loadClass() 方法

        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 这里会调用一个native方法findLoadedClass0,检查类是否已经被加载
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        // 这个parent就是上面那段代码中类加载器实例化时指定的父类加载器
                        if (parent != null) {
                            // 父类加载器非空时,直接调用其loadClass方法。那么appClassLoader就会调用extClassLoader的loadClass方法,向上委派
                            c = parent.loadClass(name, false);
                        } else {
                            // 这里就是给扩展类加载器传准备的了,父类加载器为空时,就是用BootstrapClassLoader去加载类
                            // (BootstrapClassLoader不是java写的,这里没法创建java对象,只能是null)
                            // 这个方法是一个native方法
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        ………………
                    }
    
                    if (c == null) {
                        // 谁都无法加载的话,开始调用findClass方法
                        long t1 = System.nanoTime();
                        c = findClass(name);
                        …………………
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    

    上面代码里有一个有意思的地方,“谁都无法加载的话,开始调用findClass方法”。而这个findClass方法里面是啥呢?

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

    直接抛异常哈哈

    实际上,这是给用户自定义类加载器留的接口,需要自定义的时候,重写findClass()方法即可。

    (如果想人为破坏双亲委派模式的话,还需要自己重写loadClass()方法——这也是个protected方法)

    7、补充:关于非静态方法

    类加载时,不光静态方法,实际上非静态方法也会被加载(同样加载到方法区),

    只不过要调用到非静态方法需要先实例化一个对象,然后用对象调用非静态方法。

    因为如果让类中所有的非静态方法都随着对象的实例化而建立一次,会大量消耗资源,所以才会让所有对象共享同一个非静态方法,然后用this关键字指向调用非静态方法的对象。

    相关文章

      网友评论

          本文标题:JVM类加载总结

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