深入理解JVM——(四)类加载机制

294 阅读12分钟

一、什么是类的加载

类的加载是将该类.class二进制数据读取到内存中,将其数据放在方法区内,然后再中创建一个java.lang.Class对象,用来封装方法区内的数据结构。

类的加载的最终产品是位于堆中的class对象,class对象封装了类在方法区数据结构,并为程序员提供访问方法区数据结构的接口。

img

类加载器不需要等到某个类“被首次使用”时才加载,JVM允许加载器预料某个类将被使用时就预先加载它。

如果类加载过程中遇到了.class文件的错误或缺失,类加载器只有首期调用此类时才会报错,如果一直没有主动使用,那么便不会报错。

二、类的生命周期

img

类的加载过程包含五个阶段:加载、验证、准备、解析、初始化

五阶段中加载,验证,准备,初始化四个阶段的开始顺序固定,解析阶段在某些情况下会在初始化之后开始,这是为了支持Java的动态绑定。需要注意的是,这里说的按顺序开始并不是按顺序执行或结束,这些阶段是交叉混合运行的。

2.1 加载

加载阶段主要进行查找并加载类的二进制数据,需要完成以下三件事情:

  1. 通过一个类的全限定类名获取此类的二进制字节流
  2. 将字节流表示的静态存储结构转化为方法区的运行时存储结构
  3. 在内存中生成class对象,并做为方法区数据类型的入口

对于类加载的其他阶段,加载阶段是可控性最强的阶段,即开发人员既可以使用系统的类加载器加载,也可以使用自定义的加载器加载。

2.2 连接

2.2.1 验证

验证阶段主要确保被加载的类正确性

验证是连接阶段的第一步,目的是确保Class文件中的字节流包含的信息符合虚拟机的要求,并不会危害虚拟机本身。验证阶段大致完成4个校验动作:

  • 文件格式校验:判断字节流是否符合Class文件格式
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
  • 符号引用验证:确保解析动作能正确执行

验证阶段非常重要,但不是必须的,它对程序运行没有影响,可以考虑采用-Xverifynone参数来关闭大部分的类验证证措施,以缩短虚拟机类加载的时间。

2.2.2 准备

准备阶段为类的静态变量分配内存,并初始化默认值

准备阶段是正式为类变量分配内存并设置变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量(被static修饰的变量),使用的方法区中的内存。不包括实例变量,实例变量会在对象实例化时随着对象分配在java堆中

  2. 初始值一般为 0 值或null,例如下面的类变量 value 被初始化为 0 而不是 123。

    public static int value = 123;
    

    这时候尚未开始执行任何java方法,把value赋值为123的动作将在初始化阶段才会被执行。

  3. 如果类变量是常量(被static final修饰),那么会按照表达式来进行初始化,而不是赋值为 0。

    public static final int value = 123
    

2.2.3 解析

解析阶段是将常量池中符号引用替换为直接引用的过程。

  • 符号引用:符号引用指用一组符号来描述所引用的目标,符号可以是约定好的任何形式字面量,引用的目标不一定加载到内存中
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。引用的目标一点在内存中。

2.3 初始化

初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 () 方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

JVM初始化步骤

① 假如这个类还没有被加载和连接,则程序先加载并连接该类。

② 假如该类的直接父类还没有被初始化,则先初始化其直接父类。

③ 假如类中有初始化语句,则系统依次执行这些初始化语句。

类初始化时机:只有当对类主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用一个类的静态方法
  • 反射(如Class.forName(“com.shengsiyuan.Test”)
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

以上六种情况称为主动使用,其他的情况均称为被动使用,被动使用不会导致初始化。

主动引用示例

对于类而言,初始化子类会导致父类(不包括接口)的初始化

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static final int value = 123;
}

class SubClass extends SuperClass {
    public static int i = 3;
    static {
        System.out.println("sub");
    }
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SubClass.i);
    }
}

运行结果:

super
sub
3

说明:初始化子类会导致父类的初始化,并且父类的初始化在子类初始化的前面。

被动引用实例

① 子类引用父类静态字段(非final),不会导致子类初始化

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("sub");
    }
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}

运行结果:

super
123

说明:并没有初始化子类,虽然使用SubClass.value,但实际使用的是子类继承父类的静态字段,不会初始化SubClass。即只有直接定义了这个字段的类才会被初始化。

② 对于类或接口而言,使用其常量字段(final、static)不会导致其初始化

package com.demo;

class SuperClass {
    static {
        System.out.println("super");
    }
    
    public static final int value = 123;
}

public class TestInit {
    public static void main(String[] args) {
        System.out.println(SuperClass.value);
    }
}

运行结果:

123

说明:类或接口的常量并不会导致类或接口的初始化。因为常量在编译时进行优化,直接嵌入在TestInit.class文件的字节码中。

③ 通过数组定义引用类,不会触发此类的初始化

package com.demo;
 
class SuperClass1{
    
    public static int value = 123;
    
    static
    {
        System.out.println("SuperClass init");
    }
}

public class TestMain
{
    public static void main(String[] args)
    {
        SuperClass1[] scs = new SuperClass1[10];
    }
}

运行结果为空

对于接口而言,初始化子接口不会导致父接口的初始化,只有在真正使用到父接口的时候(如使用父接口中定义的常量),才会初始化

2.4 结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

三、类加载器

寻找类加载器,先来一个小例子

package com.neo.classloader;
public class ClassLoaderTest {
     public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}

运行后,输出结果:

sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null

从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。

这几种类加载器的层次关系如下图所示:

img

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  • 启动类加载器:这个类加载器用 C++ 实现,是虚拟机自身的一部分。
  • 所有其他类的加载器:这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象java.lang.ClassLoader

从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  • 启动类加载器Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的
  • 扩展类加载器Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
  • 应用程序类加载器Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

除此之外,我们还可以加入自定义的类加载器,可以实现以下功能:

  1. 在执行非置信代码之前,自动验证数字签名。
  2. 动态地创建符合用户特定需要的定制化构建类。
  3. 从特定的场所取得java class,例如数据库中和网络中。

JVM类加载机制

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  • 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

四、类的加载

类加载有三种方式:

  • 命令行启动应用时候由JVM初始化加载
  • 通过Class.forName()方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载

例子:

package com.neo.classloader;
public class loaderTest { 
        public static void main(String[] args) throws ClassNotFoundException { 
                ClassLoader loader = HelloWorld.class.getClassLoader(); 
                System.out.println(loader); 
                //使用ClassLoader.loadClass()来加载类,不会执行初始化块 
                loader.loadClass("Test2"); 
                //使用Class.forName()来加载类,默认会执行初始化块 
                //Class.forName("Test2"); 
                //使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块 
                //Class.forName("Test2", false, loader); 
        } 
}

demo类

public class Test2 { 
        static { 
                System.out.println("静态初始化块执行了!"); 
        } 
}

分别切换加载方式,会有不同的输出结果。

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

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块.
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

五、双亲委派模型

img

上图即双亲委派模型

5.1 双亲委派机制:

  1. AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载.
  4. ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException

5.2 好处

使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。

  • 系统类防止内存中出现多份同样的字节码
  • 保证Java程序安全稳定运行

5.3 源码实现

ClassLoader源码分析:

public Class<?> loadClass(String name)throws ClassNotFoundException {
        return loadClass(name, false);
}

protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
        // 首先判断该类型是否已经被加载
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
            try {
                if (parent != null) {
                     //如果存在父类加载器,就委派给父类加载器加载
                    c = parent.loadClass(name, false);
                } else {
                //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
             // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

参考:

jvm系列(一):java类的加载机制

Java 虚拟机

深入理解虚拟机之虚拟机类加载机制