类的加载
# 类的加载
# 类加载子系统作用

ClassLoader 只负责 class 文件的加载,至于它是否可以运行,则由 Execution Engine 决定。

# 类的加载过程

一、加载:
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
注意:数组类是如何创建加载的呢?
二、链接:
- 验证 (Verify):
- 目的在于确保 Class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备 (Prepare):
- 为类变量分配内存并且设置该类变量的默认初始值,即零值。
- 这里不包含用 final 修饰的 static,因为 final 在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中。
- 见下面
LoadClass.java
- 解析 (Resolve):
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着 JVM 在执行完初始化之后再执行。 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java 虚拟机规范》的 Class 文件格式中。 在解析阶段,jvm 根据字符串的内容找到内存区域中相应的地址,然后把符号引用替换成直接指向目标的指针、句柄、偏移量等,这些直接指向目标的指针、句柄、偏移量就被成为直接引用。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等。
- 见下面
Demo.java
三、初始化:
- 初始化阶段就是执行类构造器方法
<clinit>()的过程。 - 此方法不需定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。
<clinit>()不同于类的构造器。(关联:构造器是虚拟机视角下的<init>())- 若该类具有父类,JVM 会保证子类的
<clinit>()执行前,父类的<clinit>()已经执行完毕。 - 虚拟机必须保证一个类的
<clinit>()方法在多线程下被同步加锁。 - 见
Clinit.java
流程图

举例
public class HelloApp {
static{
num = 10;//变量赋值可以正常编译通过
//System.out.println(num);//编译器提示“非法前向引用"
}
static int num = 1;
public static void main(String[] args) {
System.out.println(HelloApp.num);
}
}
2
3
4
5
6
7
8
9
10
11
12
public class ClinitTest {
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 类的加载器
# 加载器的分类

JVM 支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
怎么算自定义呢?

这里的四者之间的关系是包含关系。不是上层下层,也不是子父类的继承关系。
# 为什么需要用户自定义类加载器?
在 Java 的日常应用程序开发中,类的加载几乎是由上述 3 种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
- 隔离加载类,避免类冲突
- 修改类加载的方式,根据实际情况在某个时间点按需动态加载
- 扩展加载源:网络、数据库、机顶盒
- 防止源码泄漏
# 双亲委派机制

工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
例子
package java.lang;
/**
* @author shkstart
* @create 2020 下午 9:24
*/
public class String {
public static void main(String[] args) {
System.out.println("hello!");
}
}
2
3
4
5
6
7
8
9
10
11
运行

优势
- 避免类的重复加载
- 保护程序安全,防止核心 API 被随意篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.ShkStart
# 破坏双亲委派机制
Java 开发者建议开发者们遵循此类加载器的实现方式。同时,Java 世界中的大部分类也遵循这个模型。
例外情况:Java 模块化出现。出现了 3 次较大规模的 “被破坏” 情况
第一次发生在双亲委派模型出现之前 —— 即 JDK 1.2 面世以前的 “远古” 时代。
类加载器的概念和抽象类 java.lang.ClassLoader 则在 Java 的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java 设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免 loadClass () 被子类覆盖的可能性,只能在 JDK 1.2 之后的 java.lang.ClassLoader 中添加一个新的 protected 方法 findClass (),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在 loadClass () 中编写代码。
双亲委派模型的第二次 “被破坏” 是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为 “基础”,是因为它们总是作为被用户代码继承、调用的 API 存在,但如果有基础类型又要调用回用户的代码,那该怎么办呢?
一个典型的例子便是 JNDI 服务,JNDI 现在已经是 Java 的标准服务,在 JDK 1.3 时加入到 rt.jar。JNDI 存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的 ClassPath 下的 JNDI 服务提供者接口(Service Provider Interface,SPI)的代码。

这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则。
Java 中涉及 SPI 的加载基本上都采用这种方式来完成,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等
双亲委派模型的第三次 “被破坏” 是由于用户对程序动态性的追求而导致的。如:** 代码热替换(Hot Swap)、模块热部署(Hot Deployment)** 等
IBM 公司主导的 JSR-291(即 OSGi R4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi 中称为 Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。在 OSGi 环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。
当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索:
- * 将以 java. 开头的类,委派给父类加载器加载。
- 否则,将委派列表名单内的类,委派给父类加载器加载。
- 否则,将 Import 列表中的类,委派给 Export 这个类的 Bundle 的类加载器加载。
- 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载。
- 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载。
- 否则,类查找失败。
说明:只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在平级的类加载器中进行的
# 沙箱安全机制
Java 安全模型的核心就是 Java 沙箱(sandbox),什么是沙箱?
沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的 Java 程序运行都可以指定沙箱

增加了安全策略:

增加了代码签名:

引入域:系统域和应用域
