JVM类加载总结

JVM类加载总结

1、概述

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

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

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

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

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

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

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

JVM类加载总结_第1张图片
类加载流程图

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这个动作是在类构造器的()方法中的。

唯一的特例:

public static final int classint = 123;

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

2-4、解析(Resolution)

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

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

A:“班费交给谁?”

B:“交给班长”

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

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

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

2-5、初始化(Initialization)

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

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

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

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

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)

负责加载:

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

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

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

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

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

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

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

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

6、双亲委派模式

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

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

JVM类加载总结_第2张图片
image

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

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

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

JVM类加载总结_第3张图片
双亲委派模式

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

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

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

下面是该模式的时序图

JVM类加载总结_第4张图片
双亲委派时序图

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类加载总结)