1、类加载子系统的作用

  1. 类加载子系统Class Loader SubSystem,负责从文件系统或者网络中加载Class文件,Class文件开头由特定的标识,是一个魔术:cafebabe。
  2. 类加载器只负责class文件的加载,至于是否可运行,则由执行引擎决定。
  3. 加载到类信息存放于方法区。除了类信息,方法区还会存放运行时常量池信息,还可能包括字符串字面量和数字常量。常量池再运行时加载到内存中,即运行时常量池。

image.png

2、类加载的过程(类加载机制)

JVM负责把描述类的数据从Class文件加载到内存系统中,并对类的数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型(字节码类型),这个过程被称为Java的类加载机制。
类加载的过程大致可以分为:加载、链接、初始化三个阶段,其中,链接过程又细分为验证、准备、解析三个阶段。链接阶段细分后的三个阶段,其顺序是不确定的,这三个阶段通常交互进行。解析阶段通常会在初始化之后再开始,这时为了支持Java语言的运行时绑定特性(也被称为动态绑定)。
下面来具体看下这几个阶段都会做什么:

  1. 加载:

在加载阶段,JVM主要完成这三件事:

  • 在这一阶段,会通过一个类的全限定名获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为运行时数据区的方法区的数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,这个对象就代表了这个数据结构的访问入口(即这个个类的访问入口)。

获取二进制字节流的方式:

  • 本地系统获取;
  • 网络获取:Web Applet;
  • zip压缩包获取:jar、war;
  • 运行时计算生成:动态代理;
  • 由其他文件生成:jsp;
  • 专有数据库提取.class文件,比较少见;
  • 加密文件中获取,防止class文件被反编译的保护措施。

这里要注意,数组的加载不需要通过类加载器来创建,它是直接在内存中分配,但是数组的元素类型,最终还是要靠类加载器来完成加载。

  1. 链接:
    1. 验证:

加载阶段之后的下一个阶段就是验证阶段。在加载阶段,内存中已经生成了一个Class对象,这个对象是访问其代表的数据结构的入口,所以验证阶段的目的是为了确保Class文件的字节流中的内容符合当前虚拟机的要求,确保被加载类的正确性,不会威胁虚拟机的安全。
验证阶段主要分为四个阶段的检验:
文件格式验证:
验证魔数是是否以0xCAFEBABE开头;
验证主、次版本号是否在当前Java虚拟机的接受范围之内;
验证常量池的常量中是否有不支持的常量类型;
验证指向常量的各种索引值中是否有执行不存在的常量或者不符合类型的常量;
验证Class文件中各个部分及文件本身是否有被删除的或附加的其他信息;
其他验证;
元数据验证:
这一阶段主要是对字节码描述的信息进行语义分析,以确保描述的信息符合《Java语言规范》:
验证类是否有父类,除了Object类之后,所有的类都要有父类;
验证类的父类是否继承了不允许被继承的类(final修饰的类不可以被继承);
如果这个类不是抽象类,验证这个类是否实现了其父类或接口中要求实现的所有方法;
验证类的字段、方法是否与父类产生矛盾,例如覆盖了final字段、出现了不符合规定的重载(方法参数都一样,但是返回值不同);
字节码验证:
这是最复杂的一个阶段,这个阶段主要确定程序语意是否合法、是否符合逻辑。主要是对类的方法体(Class文件中的Code属性)进行校验分析。包括:
确保操作数栈的数据类型和实际执行时的数据类型一致;
保证任何跳转指令,不会跳出到方法体外的字节码指令上;
保证方法体重的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是不可以吧父类数据类型赋值给子类等诸如此不安全的类型转换;
保证运行时不会做出危害虚拟机的行为;
还有其他验证;
如果没有通过字节码验证,就说明验证出现了问题。但是不一定通过了字节码验证, 就能保证程序是安全的。
符号引用验证:
符号引用验证时最后一个阶段的校验行为,它发生在虚拟机将符号引用转换为直接引用的时候。这个转化将在链接阶段的第三个阶段:解析阶段中发生。符号引用验证可以看做是对类自身以外的各类信息进行匹配性校验,主要包括:
符号引用中的字符串全限定名是否能找到对应的类;
符号引用的类、字段方法的可访问性是否可被当前类访问;
指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
这一阶段主要确保解析行为能否正常执行,如果无法通过符号引用验证,就会出现类似IllegalAccessErrot、NoSuchFieldError、NoSuchmethodError等错误。

验证阶段对于虚拟机来说非常重要,如果能够通过验证,就说明你的程序在运行时不会产生任何影响。

  1. 准备:

准备阶段,就是为类变量分配内存,并且设置该类变量初始值(即:零值)的阶段。

  1. 1. 下面是通常情况下,基本数据类型和引用数据类型的初始值:

image.png

  2.  这里的内存分配不包括被final修饰的类变量,因为final修饰的类变量在编译时就会分配,在准备阶段会显式初始化。

如: public static final int value = 666; 这里面编译时就会把value的值设置为666.

  3. 这里要注意,类变量,是类中用了static修饰了的字段。而没有用static修饰的字段是实例变量,在准备阶段不会为实例变量分配初始值。类变量会被分配在方法区中,而实例变量会随着对象一起分配在Java堆中。
  1. 解析:

解析阶段,是JVM将常量池内的符号引用替换为直接引用的过程。
符号引用:用一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可。符号引用和虚拟机的布局无关。
直接引用:直接引用可以直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。直接引用和虚拟机的布局有关,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的 。如果有了直接引用,那么直接引用的目标一定被夹在到了内存中。
在编译的时候,每一个Java类都会被编译程一个.class文件,但是在编译的时候虚拟机并不知道文件中所引用类的地址,所以就用符号引用来代替。而在这个解析阶段,就是为了把这个符号引用转化为真正的地址的阶段。
解析阶段分为:类或接口的解析、字段解析、方法解析、接口方法解析。对应常量池中的CONSTANT_Class_info、Constant_Fieldref_info、CONSTANT_Methodref_info。
事实上,解析操作往往会伴随着JVM在执行完初始化阶段之后再执行。

  1. 初始化:

初始化阶段是类加载过程的最后一个阶段,在之前的阶段中,都是JVM占主导,但是到了初始化阶段,是把主动权移交给应用程序。

  1. 初始化阶段是执行类构造器方法()的过程。()方法不需要定义,是javac编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并而来的。如果类中没有类变量和静态代码块,那么也不会有方法。
  2. 类构造器方法()中的指令按照语句在源文件中出现的顺序执行。
  3. 类构造器方法()不同于类的构造器,类的构造器在虚拟机视角下是方法,方法可以说是对象构造器方法。init是对象实例构造器,对非静态变量解析初始化,而clinit时Class类构造器,对静态变量、静态代码块进行初始化。 一个类可以有多个方法,但是只能有一个方法。
  4. 若该类具有父类,JVM会保证子类的方法执行前,父类的方法已经执行完毕。
  5. 虚拟机必须保证一个类的方法在多线程下被同步加锁。

Java虚拟机规范严格规定了,有且只有六种情况,必须对类进行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时;它们对应的操作:
    1. 使用new关键字实例化对象;
    2. 读取或者设置一个类型的静态字段(final修饰的,已经在编译器将结果放入常量池的静态字段除外);
    3. 调用一个类型的静态方法的时候;
  2. 对类型进行反射调用,如果类型没有经过初始化,则需要触发初始化;
  3. 初始化类的时候,发现其父类没有初始化,则需要触发其父类的初始化;
  4. 虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会初始化这个主类;
  5. 在使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandler实例最后解析的结果为REF_getstatic、REF_putstatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,需要先对其进行初始化。
  6. 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的方法)时,如果有这个接口的实现类发生了初始化,那么该接口要在其之前被初始化。

只是设计类加载的话,上面几个阶段已经是完整的类加载过程了,不过类加载完成后,还有使用和卸载两个阶段。
使用:
使用阶段就是初始化之后的代码由JVM来动态地调用执行;
卸载:
当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载。由JVM自带的类加载器所加载的类,始终不会被卸载。JVM会始终引用自带的类加载器,而这些类加载器则会始终引用它们所加载到Class对象,因此这些Class对象始终是可触及到。而由用户自定义的类加载器所加载的类是可以被卸载的。

问题:JVM中,对象是如何创建的?
答: 我们在代码中使用new关键字,直接new出来一个对象。当虚拟机遇到一个new指令时,首先会去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用所代表的类是否已经被加载、解析和初始化(因为此时很可能不知道具体到类是什么,所以这里用的是符号引用)。
如果发现这个类没有经过类加载的过程,就执行相应的类加载过程。
类检查完成后,接下来虚拟机将会为新生对象分配内存。对象所需的大小在类加载完成后便可以确定。
分配内存相当于是把一块固定的内存从堆中划分出来。划分出来之后,虚拟机会将分配到的内存空间都初始化零值,如果使用了TLAB(本地线程分配缓冲),之意向初始化工作可以提前在TLAB分配时进行。这一步操作保证了对象实例在Java代码中可以不赋值就能直接使用。
接下来,Java虚拟机还会对对象进行必要的设置,比如确定对象是哪个类的实例、对象的hashcode、gc分带年龄信息。这些信息都放在对象的对象头(Object Header)中。
如果上面的工作都做完,从虚拟机的角度来说,一个新的对象就创建完毕了。但是对于程序员来说,对象的创建才刚刚开始。因为构造函数,也就是Class文件中的()方法还没有执行,所有的字段是默认的零值。new指令之后才会执行()方法,然后按照程序员的医院对对象进行初始化,这样一个对象才可能被完整的构造出来。

3、类加载器分类

类加载子系统 - 图3
类加载器有四种:

  • 启动类加载器Bootstrap ClassLoader:
    • 由C/C++语言实现,嵌套在JVM内部;
    • 用来加载Java核心类库,rt.jar,resource.jar,sun.boot.class.path路径下的内容;
    • 加载拓展类和应用程序类加载器,并指定为它们的父级别类加载器;
    • 出于安全考虑,Bootstrap ClassLoader启动类加载器支架在包名为java、javax、sun等开头的类;
  • 扩展类加载器Extension ClassLoader:
    • 由Java语言编写,由sun.misc.Launcher$ExtClassLoader实现;
    • 派生于ClassLoader类;
    • 父类加载器为启动类加载器;
    • 从java.ext.dirs系统属性所致定的目录中加载器类库,或从jre/lib/ext子目录下加载类库;
  • 应用程序类加载器App ClassLoader:
    • 又叫系统类加载器System ClassLoader;由Java语言编写,由sun.misc.Launcher$AppClassLoader实现;
    • 派生于ClassLoader类;
    • 父类加载器为扩展类加载器;
    • 负责加载环境变量classpath或系统属性java.class.path指定路径下的类库;
    • 该类加载器时程序中默认的类加载器,一般来说,Java应用到类都是由它来完成加载;
  • 用户自定义类加载器User Defined ClassLoader:

用户可以自定义类加载器。

注意:从虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),它是虚拟机自身的一部分;另一种是所有的其他类加载器,这些加载器都是由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。

关于ClassLoader:
它是一个抽象类,除了启动类加载器,其他类加载器都继承自它。
类加载子系统 - 图4

4、类加载机制

JVM的类加载机制,主要有三种,分别是:双亲委派模型、全盘负责和缓存机制。

4.1、双亲委派模型

类加载子系统 - 图5
双亲委派模型?
Java虚拟机对Class文件采用的是按需加载,而且加载Class文件时,Java虚拟机使用的是双亲委派模式。
那么什么是双亲委派模式呢?
就是把请求交由父类处理:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,二十八这个请求委托给父类的加载器去执行;
如果父类的加载器还存在其父类加载器,则进一步向上委托,请求最终将达到顶层的启动类加载器;
如果父类的加载器可以完成类加载任务,就成功返回,倘若父类的加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
双亲委派模式的优势:

  • 避免类的重复加载;
  • 保护程序安全,防止核心API被篡改。
  • 这是一种沙箱安全机制,保证对Java核心源码的保护。

补充1:
JVM必须知道一个类型是由启动类加载器加载的,还有由于用户加载器加载的。我们之前提到过,从虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),它是虚拟机自身的一部分;另一种是所有的其他类加载器,这些我们看作是用户类加载器。 在这个分类基础上,如果是用户类加载器加载的类,JVM会将这个类加载器的一个应用作为类型信息的一部分,保存到方法区中。当解析一个类型到另一个类型的引用时,JVM需要保证这两个类型的类加载器时相同的。

问题1:能不能自己写一个类叫java.lang.String?
可以写,但是自己写这个类没有意义,由于双亲委派模型,它不会被加载。实际加载到还是jdk中的String类(jdk中的String类是由启动类加载器加载的)。

问题2:在JVM中表示两个Class对象,是否为同一个类,需要什么条件?
存在两个必要条件:

  1. 类的完整类名必须一致,包括包名;
  2. 加载这个类的类加载器必须相同;

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

4.2、全盘负责

所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

4.3、缓存机制

保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。