一、简述类加载器子系统作用
作用:
类加载器子系统主要负责读取Java字节码文件(.class
文件开头都有特定的文件标识),并转换成java.lang.Class的实例,每个实例就代表的一个Java类。通过实例的newInstance()
方法就可以创建此Java类的对象。
概述图:
二、类加载器子系统的各个环节
1. Loading (加载)
- 通过包名+类名来获取此类的二进制字节流。
- ClassLoading只负责class文件的加载,至于文件是否可以正常运行是由
ExectionEngine(执行引擎)
决定的。 - 将这个字节流所代表的的类信息(静态存储结构)将存放在
方法区
的内存空间中。方法区
中除了存放类信息还存放常量池信息(包含字符串常量和数字常量)。 - 在内存中生成一个代表这个类的 java.lang.Class 实例,作为方法区这个类的各种数据的访问入口。
2. Linking (链接)
-
验证
目的在于确保 class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。
-
文件格式检验
检验字节流是否符合 class 文件格式的规范,并且能被当前版本的虚拟机处理。
检验可能包含以下几种:是否以魔数开头(0xCAFEBABE)、主次版本号是否在虚拟机的处理范围之内,常量池中的常量是否有不被支持的类型(检验常量 tag 标志)、class文件中各个部分以及文件本身是否被删除或附加什么信息等等。 只有通过文件格式检验的二进制字节流才能进入内存的方法区进行存储,所以后面的3个检验阶段都是基于方法区的存储结构进行的,不会再操作字节流。
-
元数据检验
对字节码描述的信息进行语义分析,以保证其描述的内容符合 Java 语言规范的要求。
验证点包括:是否有父类(除了object)、父类是否继承了不可被继承的类(被 final 修饰的类)、非抽象类是否实现了其父类或接口之中要求实现的所有方法、类中的方法和字段是否与父类产生矛盾(覆盖了父类的 final 字段、出现不合规矩的方法重载等)。 元数据检验主要是对类的元数据信息进行语义校验,保证不符合 Java 语言规范的元数据信息不存在。
-
字节码检验
通过数据流和控制流分析,确定程序语义是合法、符合逻辑的。第二阶段是对元数据信息中的数据类型做了检验,这一阶段是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事情。检验点包括:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证指令跳转不会跳转到方法体之外的地方、保证方法体内的类型转换都是有效的。 事实上,即便是经过字节码检验后的方法体也不一定是安全的。
-
符号引用检验
最后一个检验发生在虚拟机将符号引用转化为直接引用时,这个转化动作将在连接的第三阶段(解析阶段)中发生的。符号引用检验可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
校验点:符号引用中通过字符串描述的全限定名是否能找到对应的类、在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段、符号引用中的类、字段、方法的访问权限是否能让当前类访问到等。 符号引用检验的目的是确保解析动作的正常执行,如果无法通过符号引用检验,将会抛出 java.lang.IncompatibleClassChangeError 异常的子类,如 IllegalAccessError、NoSuchfiledError、NoSuchMethodError 等。
-
-
Prepare(准备)
- 为变量分配内存且设置该变量的默认初始值,即零值。
- 这里不包含用 final 修饰的 static,因为 final 在编译的时候就会分配,准备阶段显式初始化。
- 这里不会为实例变量分配初始化,类变量分配在方法区中,而实例变量会随着对象一起分配到堆中。
3. Initialization (初始化)
- 初始化阶段就是执行类构造器方法 () 的过程,此方法不需定义是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来、构造器方法中指令按语句在源文件中出现的顺序执行。
- () 不同于类的构造方法(构造方法是虚拟机视角下的(),即类的构造器函数)。
- 若该类具有父类,JVM 会保证子类的() 执行前,父类的 () 已经执行完毕。
- 虚拟机必须保证一个类的 () 方法在多线程下被同步加锁(类的加载是同步的)。
三、类加载器的类别(Class Loader)
jvm支持俩种类加载器,分别是引导类加载器(Boot Strap ClassLoader),和自定义类加载器(User-Defined ClassLoader)
1. 引导类加载器 (Boot Strap ClassLoader)
- 此加载器并非java语言编写是由其他语言(C/C++)编写,是JVM的一部分。
- 主要负责加载java核心类库(JAVA_HOME/jre/lib/rt.jar、resources.jar。是用于提供JVM需要使用的类)。
- 此加载器也会加载包名为:
java、javax、sun
开头的类,包括扩展类加载器
和系统类加载器
。
2. 自定义类加载器(User-Defined ClassLoader)
- 自定义类加载器分别有:
扩展类加载器(Extension ClassLoader)
和系统类加载器(Application ClassLoader)
。这俩个类加载器都间接的继承了ClassLoader抽象类。- 扩展类加载器负责加载:
JAVA_HOME /lib/ext
或者系统变量-Djava.ext.dir
位置中的类库。- 系统类加载器默认加载
用户自定义的类
。- 用户自定义类加载器的用处:
- 隔离加载类
- 修改类加载方式
- 扩展加载源
- 防止源码泄露
- 关于ClassLoader抽象类中的非抽象方法:
方法名称 描述 getParent() 返回该类加载器的超类加载器 loadClass(String name) 加载名称为name的类,返回java.lang.Class类的实例 findClass(String name) 查找名称为name的类,返回java.lang.Class类的实例 findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回java.lang.Class类的实例 defineClass(String name, byte[] b, int off, int less) 把字节数组b中的内容转换为一个Java类,返回java.lang.Class类的实例 resolveClass(Class<?> c) 连接指定的Java类
获取类加载器
clazz.getClassLoader(); //获取当前类的加载器 Thread.currentThread().getContextClassLoader(); //获取当前线程类加载器 ClassLoader.getSystemClassLoader(); //获取系统类加载器
四、加载机制
-
Java虚拟机对class文件采用按需加载方式,只需要加载程序会用到的class文件。
-
Java采用的双亲委派机制。
-
工作原理:
a. 如果一个类加载器收到了加载请求,它会将加载请求提交给父类加载器去执行。
b. 如果父类加载器之上还有父类,则会继续向上委派,依次递归到最顶层父类加载器。
c. 如果父类加载器可以对此Class进行加载,就返回成功。如果不能加载就由子类加载器负责加载。
-
-
优点
a . 避免类的重复加载。
b. 保护程序安全,防止核心API被随意篡改 。
- 沙箱安全机制:由引导类加载器加载的过程中会先加载jdk自带的文件,可以防止自定义的类干涉java核心类。
五、JVM区分Class类对象
- 根据类的完整类名必须一致,包括包名。
- 加载这个类的加载器(指ClassLoader实例对象)必须相同。
- JVM必须知道一个类型是由启动加载器加载的在世有用户类加载器加载的。如果一个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这俩个类型的类加载器是相同的。
六、类的使用
类的使用主要分为主动与被动
-
主动使用:
- 创建类的实例。
- 访问某个类或接口的静态变量、给某个类或接口的静态变量赋值。
- 调用类静态变量。
- 反射获取类。
- 初始化一个拥有父类的Class。
- 代码的启动类。
-
被动使用:
除了以上情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。