类加载子系统负责把二进制字节码(Class文件)装载到内存,并对数据进行校验、转换解析、初始化,最终生成被虚拟机直接使用的Java类型。

类加载子系统的作用

类加载子系统的组成:
copy.png

作用:

  • 类加载子系统负责从文件系统或者网络中加载 class 文件,class 文件在文件开头有特定的文件标识(0xCAFEBABE)
  • ClassLoader 只负责 class 文件的加载。至于它是否可以运行,则由 Execution Engine 决定
  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class文件中常量池部分的内存映射)
  • Class 对象是存放在堆区的。

类加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载验证准备解析初始化使用卸载七个阶段。(验证、准备和解析又统称为连接,为了支持 Java 语言的运行时绑定,所以解析阶段也可以是在初始化之后进行的。以上顺序都只是说开始的顺序,实际过程中是交叉的混合式进行的,加载过程中可能就已经开始验证了)

copy.png

1. 类加载

类加载过程中需要一个叫做类加载器的工具,作为一个搬运的角色,将class文件装载到JVM中,类加载一般分为:引导类加载器扩展类加载器应用类加载器用户自定义的加载器

2. 连接(Linking)

验证(Verify)
目的是确保Class文件中的字节流中包含的信息符合当前虚拟机要求,保证被加载类的正确性,主要包含四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证。

准备(Prepare)
为类变量分配内存并且设置该类变量的默认初始值,如基础类型的初始值为:

数据类型 零值(初始值)
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

所有的引用类型,包含对象,包装类型均为null。
final修饰的static在会直接初始化为想要的值,类变量(static修饰)的则会在准备阶段初始化为零值,放在方法区中,实例变量则不会初始化,会在初始化时随着变量放到堆中。

解析
当通过准备阶段之后,JVM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。

3. 初始化

用户定义的Java代码开始执行,JVM会根据语句的执行顺序对类对象进行初始化,一般遇到以下的5中情况会触发初始化:

  • 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用 java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

除了以上的几种情况,其他使用Java类的方式被看作时对类的被动使用,不会触发类的初始化。

4. 使用

当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

5. 卸载

当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存。

类加载器

类加载器主要负责将Java字节码加载到内存中,生成一个java.lang.Class对象。主要分为如下几类:

启动类加载器(引导类加载器,Bootstrap ClassLoader)
这个类加载使用 C/C++ 语言实现,嵌套在 JVM 内部
它用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jarresource.jarsun.boot.class.path路径下的内容),用于提供 JVM 自身需要的类,没有父加载器
加载扩展类和应用程序类加载器,并指定为他们的父类加载器
出于安全考虑,Bootstrap 启动类加载器只加载名为java、Javax、sun等开头的类

扩展类加载器(Extension ClassLoader)
Java 语言编写,由 sun.misc.Launcher$ExtClassLoader 实现
派生于 ClassLoader
父类加载器为启动类加载器
java.ext.dirs系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载

应用程序类加载器(也叫系统类加载器,AppClassLoader)
Java 语言编写,由 sun.misc.Lanucher$AppClassLoader实现
派生于 ClassLoader
父类加载器为扩展类加载器
它负责加载环境变量 classpath 或系统属性java.class.path指定路径下的类库
该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载的
通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

用户自定义类加载器
在 Java 的日常应用程序开发中,类的加载几乎是由 3 种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。

为什么要使用用户自定义类加载器:

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源(可以从数据库、云端等指定来源加载类)
  • 防止源码泄露(Java 代码容易被反编译,如果加密后,自定义加载器加载类的时候就可以先解密,再加载)

如何实现自定义类加载器?
首先继承抽像类java.lang.ClassLoader
JDK1.2之后,覆盖findClass()方法,编写自己的查找类的方式;
,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

举栗子:
自定义类加载器的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class MyClassLoader extends ClassLoader {
/**
* 自己实现类的查找逻辑
*
* @param name 名称
* @return /
* @throws ClassNotFoundException 未找到
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String resource = "E:/programCode/work";
File source = new File(resource,name.replace(".","/").concat(".class"));
if(!source.exists()) {
throw new ClassNotFoundException(name+" not found!");
}
try(FileInputStream fileInputStream = new FileInputStream(source)) {
System.out.println("自定义类加载器读取类文件...");
byte[] bytes = new byte[fileInputStream.available()];
fileInputStream.read(bytes);
return super.defineClass(name,bytes,0,bytes.length);
}catch (Exception e) {
System.out.println("class not found!");
}
return super.findClass(name);
}
}

使用自定义类加载器调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
try {
Class<?> aClass = classLoader.loadClass("com.abumaster.javabase.jvm.LoadDemo");
System.out.println("类名称:"+aClass.getName());
System.out.println("类加载器:"+aClass.getClassLoader());

// 实例化一个对象i
Object o = aClass.getConstructors()[0].newInstance();
// 调用print方法方法
aClass.getMethod("print",String.class).invoke(o,"what?");


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

双亲委派机制

Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要使用该类的时候才会将它的 class 文件加载到内存生成 class 对象。而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交给父类处理,它是一种任务委派模式。

原理

几种类加载器及其结构:
copy.png

说明:

  1. 启动类加载器,由C++实现,java中无法直接获取,加载jre/lib下的类;
  2. 扩展类加载器,由Java实现,可以在Java中获取,加载jre/lib/ext下的类;
  3. 系统类加载器/应用程序类加载器,一般常与其打交道,用于加载程序类路径下的类,可以通过ClassLoader.getSystemClassLoader返回;
  4. 自定义类加载器,用户自定义类的加载路径及其加载方式,满足特殊的业务需求,比如Jar包加密。

注:通过类的全路径和加载这个类的类加载器确定同一个类。

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

优势:

  • 避免类的重复加载,JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就属于两个不同的类(比如,Java中的Object类,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,如果不采用双亲委派模型,由各个类加载器自己去加载的话,系统中会存在多种不同的 Object 类)
  • 保护程序安全,防止核心 API 被随意篡改,避免用户自己编写的类动态替换 Java 的一些核心类,比如我们自定义类:java.lang.String
  • 保证Java核心代码的安全,当自定义一个系统类的时候,则会优先使用引导类加载器加载,提供一种沙箱安全机制。

破坏双亲委派模型

为什么要破坏双亲委派?

因为在某些情况下父类加载器需要委托子类加载器去加载class文件或者jar包。受到加载范围的限制,父类加载器无法加载到需要的文件。
Driver接口为栗,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager启动类加载器加载,只能记载JAVA_HOMElib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。
利用破坏双亲委派来实现代码热替换(每次修改类文件,不需要重启服务)。因为一个 Class 只能被一个ClassLoader 加载一次,否则会报 java.lang.LinkageError。当我们想要实现代码热部署时,可以每次都new 一个自定义的 ClassLoader 来加载新的 Class文件。JSP 的实现动态修改就是使用此特性实现。