什么是类的加载?

  • 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内
  • 然后在堆区创建一个 java.lang.Class对象,用来封装类在方法区内的数据结构。
  • 类的加载的最终产品是位于堆区中的 Class对象Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口

在这里插入图片描述

  • 类加载器并不需要等到某个类被首次主动使用时再加载它,JVM 规范允许类加载器在预料某个类将要被使用时就预先加载它
  • 如果在预先加载的过程中遇到了.class文件确实或存在错误,类加载器必须在程序首次主动使用该类时才报告错误LinkageError,如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

JVM整体架构

在这里插入图片描述

  • JVM被分为三个主要的子系统:(1)类加载子系统(2)运行时数据区(3)执行引擎
  • 如果我们想自己写一个Java虚拟机的话,主要考虑的就是类加载器和执行引擎

类加载子系统的过程

  • 类加载的过程包括了加载、验证、准备、解析、初始化五个阶段
  • 在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)
  • 另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

在这里插入图片描述

  • 字节码文件存在于本地硬盘上,可以理解为设计师画在纸上的模板,在执行的时候要先把字节码文件加载到 JVM 当中,再根据这个模板实例化出 n 个一模一样的实例。

  • 字节码文件加载到 JVM 中,被称为 DNA元数据模板,存放在方法区当中

  • 在字节码文件到JVM中,最后称为元数据模板,此过程需要一个运输工具将硬板上的数据运输到内存中,这个工具就是ClassLoader 类装载器
    在这里插入图片描述

  • Java 的动态类加载功能是由类加载器子系统处理。当它在运行时首次引用一个类时,它加载、连接、初始化该类的字节码文件。

  • ClassLoader 只负责将字节码文件加载进内存,至于是否可以运行,则由执行引擎决定

  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

  • 这里可能会搞混堆区和方法区,Class对象是存放在堆区的,不是方法区元数据并不是类的Class对象!!类的元数据才是存在方法区的

  • Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的

  • JVM 在加载 class 的时候,创建 instanceKlass(JVM中的数据结构) 表示其元数据,包括常量池、字段、方法等,存放在方法区中;

  • new 一个对象时,JVM 创建 instanceOopDesc,来表示这个对象,存放在堆区

类的加载

类的加载过程主要完成三件事:

  • 通过类的全限定名来获取定义此类的二进制字节流
  • 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
  • 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。

首先我们来看一下下面这一段简单的代码,它的加载过程是怎样的呢?

public class HelloLoader {
    public static void main(String[] args) {
        System.out.println("我已经被加载啦");
    }
}

在这里插入图片描述

  • 类由类加载器(ClassLoader)进行加载。
  • 类加载器分为启动类加载器(Bootstrap ClassLoader)自定义类加载器(User-Defined ClassLoader)
  • 尽管从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器
  • 但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类java.lang.ClassLoader的类加载器都划分为自定义类加载器
  • 在程序中,我们最常见的类加载器始终只有三个:Bootstrap ClassLoaderExtensionClassLoaderSystemClassLoader

在这里插入图片描述

【注意】这四者彼此之间是包含关系,不是上层和下层,也不是子系统的继承关系,是采用组合实现的

我们可以根据一段代码来查看类加载器

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);

        // 获取其上层的:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);

        // 试图获取 根加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);

        // 获取自定义加载器
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);
        
        // 获取String类型的加载器
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);
    }
}
  • 我们可以看到,根加载器,也就是引导类加载器无法直接通过代码直接获取,输出的是一个 null
  • 那是因为引导类加载器使用的不是 Java 代码,而是 C++ 实现的(仅限于HotSpot,也有其他虚拟机是使用 Java 实现的)
  • 而其他可以获得到的加载器对象都是由 Java 语言实现的,独立于虚拟机之外,并且全部都是继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类
  • 另外我们通过获取String类型的加载器,发现也是 null
  • 说明String类型是通过根加载器进行加载的,也就是说Java的核心类库都是使用根加载器进行加载的。
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
null
sun.misc.Launcher$AppClassLoader@18b4aac2 // 这个就是 SystemClassLoader
null 

加载字节码文件的方式

  1. 从本地系统中直接加载
  2. 通过网络获取.class文件,典型场景:Web Applet
  3. 从 zip 压缩包中读取,成为日后 jar、war 格式的基础
  4. 运行时计算生成,使用最多的是:动态代理技术
  5. 由其他文件(数据库文件等)生成,典型场景:JSP 应用从专有数据库中提取.class文件,比较少见
  6. 将Java源文件动态编译为.class文件,,例如从加密过的字节码文件中获取,典型的防Class文件被反编译的保护措施

类加载器的种类

首先是虚拟机自带的加载器

  1. 启动类加载器(引导类加载器、Bootstrap ClassLoader),
    (1)这个类加载使用 C/C++ 语言实现的,嵌套在 JVM 内部
    (2)负责加载存放在 JAVA_HOME\jre\lib目录下,或被 -Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar、resources.jar,所有的java.开头的类均被BootstrapClassLoader加载)
    (3)并不继承自java.lang.ClassLoader,没有父加载器。
    (4)加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
    (5)出于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类
  2. 扩展类加载器(Extension ClassLoader)
    (1)Java 语言编写,由sun.misc.Launcher$ExtClassLoader实现。
    (2)派生于 ClassLoader 类
    (3)父类加载器为启动类加载器(注意不是父类,是父类加载器,没有继承关系)
    (4)从java.ext.dirs系统属性所指定的目录中加载类库(如javax.开头的类),或从 JDK 的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
  3. 应用程序类加载器(系统类加载器,AppClassLoader)
    (1)java 语言编写,由sun.misc.Launchers$AppClassLoader实现
    (2)派生于ClassLoader类
    (3)父类加载器为扩展类加载器
    (4)它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
    (5)该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载
    (6)通过classLoader#getSystemclassLoader()方法可以获取到该类加载器

如果我们有一些特殊的需求,可以自己实现自定义类加载器,实现步骤如下:

  • 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
  • 在JDK1.2之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写loadClass()方法,从而实现自定义的类加载类,但是在 JDK1.2 之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
  • 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

在日常的应用开发中,使用虚拟机自带的三种类加载器其实已经是足够的了,那么为什么还要实现自定义的类加载器呢?

  • 隔离加载类:例如一些框架中可能会使用到一些第三方的 Jar包,那么为了防止Jar包之间的命名冲突,那么在相同的包名的情况下,就需要使用不同的类加载器区加载类,防止因冲突而无法加载。
  • 修改类加载的方式:在这儿过程中引导类加载器是一定要使用到的,但是其他的加载器就不是必须要使用的,那么通过自定义加载器就可以在需要的时候再使用加载器,实现动态加载
  • 扩展加载源:加载源即字节码的来源,可以通过自定义加载器,实现不同的字节码文件的加载方式
  • 防止源码泄露:Java的代码是非常容易反编译的,通过字节码文件很容易获得代码,那么为了防止源码泄露,就可以将源码的字节码加密,然后在加载的时候再解密,这样就能防止源代码的泄露

举例:写一个自己的类加载器

先看看源码是怎么写ClassLoader的
在这里插入图片描述

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 首先判断该类型是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
            	// 如果没有被加载,就委托给父类加载器或者委派给启动类加载器加载
                long t0 = System.nanoTime();
                try {
                	// 存在父类加载器,就委托给父类加载器
                	// 不存在就检查是否是由启动类加载器加载的类
                	// 通过调用本地方法 native Class findBootstrapClass(String name)
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                    // 如果没有找到类就从非空的父类加载器抛出异常
                }

                if (c == null) {
                	// 仍然没有找到,就执行 findClass 来找这个类
                	// 即:如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    // 这是 定义类加载器,记录状态
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            // 解析类
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • 通常情况下,我们都是直接使用系统类加载器。
  • 但是,有的时候,我们也需要自定义类加载器。
  • 比如应用是通过网络来传输 Java 类的字节码,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。
  • 自定义类加载器一般都是继承自 ClassLoader类,从上面对 loadClass方法来分析来看,我们只需要重写 findClass 方法即可。
  • 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式
import java.io.FileNotFoundException;

public class MyselfClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            byte[] result = getClassCustomFromCustomPath(name);
            if(result == null){
                throw new FileNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }

        }catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException(name);
    }

    private byte[] getClassCustomFromCustomPath(String name){
        // 从自定义路径中加载指定类 根据name全限定类名,获得字节数组
        // 可以采用自定义的方式来加密生成的字节码文件,然后在这里读取的时候,再进行解密操作即可
        return null;
    }
}

ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)

在这里插入图片描述

  • 查看源码可以发现ExtClassLoaderAppClassLoader都是sun.misc.Launcher的内部类,且都继承了URLClassLoader

在这里插入图片描述

  • 继续查看继承关系可发现URLClassLoader继承了SecureClassLoader,而SecureClassLoader又继承了ClassLoader
    在这里插入图片描述

  • 也就是说:sun.misc.Launcher 它是一个 java 虚拟机的入口应用

获取 ClassLoader 的途径

  1. 获取当前ClassLoader:clazz.getClassLoader()
  2. 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
  3. 获取系统的ClassLoader:ClassLoader.getSystemClassLoader()
  4. 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()

JVM 类加载机制

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

类加载由三种方式

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

其中,Class.forName()ClassLoader.loadClass()区别

  1. Class.forName():将类的.class文件加载到 JVM 中之外,还会对类进行解释,执行类中的 static 块;

  2. ClassLoader.loadClass():只干一件事情,就是将.class文件加载到 JVM 中,不会执行 static 中的内容,只有在 newInstance才会去执行 static 块。

  3. Class.forName(name,initialize,loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

连接

  • 连接又分为三个步骤:校验(Verify)、准备(Prepare)、解析(Resolve)

校验

  • 校验的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全,即确保被加载的类的正确性
  • 如:Class文件在文件开头有特定的文件标识:CA FE BA BE
    在这里插入图片描述

主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 文件格式验证:基于字节流验证,验证字节流符合 Class 文件格式的规范,例如:是否以 0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。。验证通过后,字节流才会进入内存的方法区进行存储
  • 元数据验证:基于方法区的存储结构验证,对字节码描述的信息进行语义验证(注意:对比 javac 编译阶段的语义分析),确保不存在不符合 java 语言规范的元数据信息,例如这个类是否有父类,除了 java.lang.Object之外。
  • 字节码验证:基于方法区的存储结构验证,通过对数据流和控制流的分析,确定程序语义是合法的、符合逻辑的;保证被检验类的方法在运行时不会做出危害虚拟机的动作。
  • 符号引用验证:基于方法区的存储结构验证,发生在解析阶段,确保能够将符号引用成功的解析为直接引用,其目的是确保解析动作能正确执行。换句话说就是对类自身以外的信息进行匹配性校验。

准备

  • 准备阶段:为类变量(静态变量)分配内存并且为其设置默认初始值,即零值。
  • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
  • 对于该阶段有以下几点需要注意:
    • 这时候进行内存分配的包括类变量(static),而包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
    • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是在 Java 代码中被显式地赋予的值。

举个例子

public class HelloApp {
    private static int a = 1;  // 准备阶段为0,在下个阶段,也就是初始化的时候才是1
    public static void main(String[] args) {
        System.out.println(a);
    }
}
  • 上面的变量 a在准备阶段会赋予初始值,但不是1,而是0,因为这时候尚未开始执行任何Java方法
  • 而把 a赋值为1private static 指令是在程序编译之后,存放于类构造去<clinit>()方法之中的,所以赋值为1的动作将在初始化阶段才会执行
  • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
  • 这里不会为实例变量分配初始化(因为这时候还没有创建对象,还只是类的加载过程),类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。

这里要注意几点

  1. 对基本数据类型来说,对于类变量(static)全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  2. 对于同时被staticfinal修饰的常量,必须声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  3. 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null
  4. 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值
  5. 如果类字段的字段属性表中存在 ConstantValue属性,即同时被finalstatic修饰(编译时Javac将会为value生成ConstantValue属性),那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值,可以理解为:static final常量在编译期就将其结果放入了调用它的类的常量池中

finalstaticstatic final的区别就是:
final:表明是一个常数,初始化可以在类加载的时候,也可以在运行的时候,但是初始化后就不能被改变
static:表示所有对象都只有一个值,初始化在类加载的时候,但是初始化之后可以被改变
static final:表示一旦给值,就不能修改,且只有一个

解析

  • 将常量池内的符号引用转换为直接引用的过程
  • 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
  • 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的 class 文件格式中
  • 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。对应常量池中的CONSTANT_Class_ infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info
  • 主要有以下四种:类或接口的解析,字段解析,类方法解析,接口方法解析

举个例子

  • com.sbbic.Person类中引用了com.sbbic.Animal
  • 在编译阶段,Person类并不知道Animal的实际内存地址,因此只能用com.sbbic.Animal来代表Animal真实的内存地址
  • 在解析阶段,JVM 可以通过解析该符号引用,来确定com.sbbic.Animal类的真实内存地址(如果该类未被加载过,则先加载)

初始化

  • 初始化,类加载的最后阶段,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。在 Java 中对类变量进行初始值设定有两种方式:
    (1)声明类变量是指定初始值
    (2)使用静态代码块为类变量指定初始值
  • JVM初始化步骤
    (1)假如这个类还没有被加载和连接,则程序先加载并连接该类
    (2)假如该类的直接父类还没有被初始化,则先初始化其直接父类
    (3)假如类中有初始化语句,则系统依次执行这些初始化语句
  • 类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,(文章末尾有介绍类的主动使用和被动使用)

初始化步骤中的特点 / 特性

  • 初始化阶段就是执行类构造器方法<clinit>()的过程
  • 此方法(即<clinit>())不需定义,是 javac 编译器自动收集类中的所有类变量的赋值动作静态代码块中的语句合并而来。 也就是说,当我们代码中包含 static 变量的时候,就会有 <clinit>()方法
  • <clinit>()构造器方法中指令按语句在源文件中的出现的顺序执行
  • 若该类具有父类,JVM 会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。
  • 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁,这也是单例模式中静态内部类实现的原理。

initclinit 区别,参考自 https://blog.csdn.net/xiaojin21cen/article/details/107442238

执行时机不同

  • init 是对象构造器方法,程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行 init 方法,
  • clinit 是类构造器方法(类的加载过程), jvm 进行类的加载 —–> 验证 —-> 解析 —–> 初始化 ,其中,初始化阶段,jvm 会调用 clinit 方法。

执行目的不同

  • init 是 instance 实例构造器,对非静态变量解析初始化;
  • clinit 是 class 类构造器对静态变量,静态代码块进行初始化。

【注意:任何一个类在声明后,都有生成一个构造器,默认是空参构造器】
在这里插入图片描述

例1:指令按顺序执行 以及 非法的前向引用

public class ClassInitTest {
    private static int num = 1;
    static {
        num = 2;
        number = 20;
        System.out.println(num);
        // 这里要注意了 number 的初始化还没有完成
        // 也就是 声明在后面,这里就不可以调用,可以赋值但是不可以调用
        System.out.println(number);  //报错,非法的前向引用
    }
	// 这里会先在连接的准备阶段分配内存空间 赋初始值 0
	// 然后在初始化结点就会被赋值,但是会按照顺序执行,从上往下
	// 在初始化阶段执行静态代码块语句会被赋值为 20
	// 然后该行代码执行,赋值为 10
    private static int number = 10; 

    public static void main(String[] args) {
        System.out.println(ClassInitTest.num); // 2
        System.out.println(ClassInitTest.number); // 10
    }
}

在这里插入图片描述

如果将语句上移,看看会发生什么变化。

在这里插入图片描述

查看原来的字节码的<clinit>()

在这里插入图片描述

例2:涉及到父类的变量赋值过程

public class ClinitTest1 {
    static class Father {
        public static int A = 1;
        static {
            A = 2;
        }
    }

    static class Son extends Father {
        public static int b = A;
    }

    public static void main(String[] args) {
        System.out.println(Son.b);
    }
}
  • 输出结果为 2,也就是说首先加载ClinitTest1的时候,会找到main方法,然后执行Son的初始化,但是Son继承了Father,因此还需要执行Father的初始化,同时将A赋值为2。我们通过反编译得到Father的加载过程,首先我们看到原来的值被赋值成1,然后又被赋值成2,最后返回

例3:虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。

public class DeadThreadTest {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 线程t1开始");
            new DeadThread();
        }, "t1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 线程t2开始");
            new DeadThread();
        }, "t2").start();
    }
}
class DeadThread {
    static {
        if (true) {
            System.out.println(Thread.currentThread().getName() + "\t 初始化当前类");
            while(true) {

            }
        }
    }
}

输出结果为:

线程t1开始
线程t2开始
线程t2 初始化当前类
  • JVM加载的时候,只调用clinit加载一次,而且加载的执行过程是同步加锁的
  • 假设我们有两个线程想加载一个类,其中一个线程一直在加载(代码手动设置),让他直在加载;这个时候另一个线程也去加载这个类,那么从上面输出结果可以看出另一个线程无法加载,也就是这个类只能被加载一次
  • 这也就是单例模式中使用静态内部类的原理: 因为类的 static 修饰的代码都是被clinit 来执行的,但是虚拟机已经保证在多线程下会对 clinit 的执行加锁,从而保证线程安全,也就保证了静态内部类实现单例的线程安全

结束生命周期

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

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

其他

如何判断两个Class对象是否相同?

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致(即全限定类名),包括包名。
  • 加载这个类的ClassLoader(指 ClassLoader实例对象)必须相同。

换句话说,在 JVM 中,即使这两个类对象来自于同一个Class文件,被同一个虚拟机所加载,但只要加载它们的 ClassLoader 实例对象不同,那么这两个类对象也是不相等的。

  • JVM 必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。
  • 如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息一部分保存在方法区
  • 当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

类的主动使用和被动使用

Java程序对类的使用方式分为:主动使用和被动使用

主动使用,又分为七种情况:

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(比如:Class.forName("com.package.classDemo")
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类
  7. JDK7开始提供的动态语言支持:
    java.lang.invoke.MethodHandle实例的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 句柄对应的类没有初始化,则初始化

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化

参考资料

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐