1. JVM类的加载

Java的类加载,就是把字节码格式“class”文件中的二进制数据加载到JVM的方法区,并在JVM的堆区建立一个java.lang.Class对象的实例,用来封装Java类相关的数据和方法。类加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且提供访问方法区内数据结构的接口。
类加载器不需要等到某个类被“首次主动使用”时再加载,JVM规范允许类加载器在预料某个类将要被使用时就预先加载,如果在预先加载的过程中遇到.class文件缺失或错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError),如果此类一直未被主动使用,则不会报告错误。
image.png
加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip、jar等归档文件中加载
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

2. 类的生命周期

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

2.1 加载

查找并加载类的二进制数据

加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成三件事情

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

2.2 连接

2.2.1 验证

确保被加载的类的正确性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全,主要有四个阶段

  • 文件格式验证: 验证字节流是否符合Class文件格式的规范
  • 元数据验证:对字节码描述的信息进行语义分析,保证其符合语言规范
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
  • 符号引用验证:确保解析动作能正确执行

验证阶段不是必须的,可以通过 -Xverifynone 来关闭类验证

2.2.2 准备

为类的静态变量分配内存,并将其初始化为默认值

准备阶段是正式为类变量(静态成员变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配,对于该阶段有几点需要注意:

  • 此时进行内存分配的只包括类变量(static),不包括实例变量,实例变量会在对象实例化随着对象一块分配在Java堆中。
  • 这里所设置的初始值一般是数据类型的默认值(如0、0L、null、false),而不是被代码中被显式地赋予的值,如果是 final 修饰的变量则会被赋值,编译时javac将会为value生成ConstantValue属性,可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。
  • 除final修饰的变量,被赋值的动作会在初始化阶段执行(执行clinit时)

2.2.3 解析

把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

2.3 初始化

为类的静态变量赋值

Java中对类变量进行初始值设定有两种方式

  • 声明类变量是指定初始值
  • 使用静态代码块为类变量指定初始值

JVM初始化步骤:

  • 如果这个类没有被加载和连接,则进行加载和连接
  • 如果此类的直接父类没有被初始化,则先初始化其父类
  • 如果类中有初始化语句,则系统依次执行这些初始化语句

类的初始化时机:只有主动使用类时才会导致类的初始化,类的主动使用包括以下六种

  • 创建类的实例,即new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如Class.forName(“com.test.Test”))
  • 初始化某个类的子类,则其父类也会被初始化
  • JVM启动时被标明为启动类的类

2.4 结束生命周期

在以下四种情况下,JVM会结束生命周期

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

3. 类加载器(ClassLoader)

Code:

  1. public class ClassLoaderTest {
  2. public static void main(String[] args) {
  3. ClassLoader loader = Thread.currentThread().getContextClassLoader();
  4. System.out.println(loader);
  5. System.out.println(loader.getParent());
  6. System.out.println(loader.getParent().getParent());
  7. }
  8. }

OutPut:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. sun.misc.Launcher$ExtClassLoader@1b6d3586
  3. null

没有获取到ExtClassLoader的父Loader, 原因时BootstrapClassLoader是用C语言实现的,找不到一个确定的返回方式,于是返回null.
加载层次关系如下:
image.png

3.1 ClassLoader解析

Java中父类加载器不是通过继承,而是通过组合来实现的
ClassLoader

  1. public abstract class ClassLoader {
  2. // 每个类加载器都有个父加载器
  3. private final ClassLoader parent;
  4. public Class<?> loadClass(String name) {
  5. // 查找一下这个类是不是已经加载过了
  6. Class<?> c = findLoadedClass(name);
  7. // 如果没有加载过
  8. if( c == null ){
  9. // 先委托给父加载器去加载,注意这是个递归调用
  10. if (parent != null) {
  11. c = parent.loadClass(name);
  12. }else {
  13. // 如果父加载器为空,查找 Bootstrap 加载器是不是加载过了
  14. c = findBootstrapClassOrNull(name);
  15. }
  16. }
  17. // 如果父加载器没加载成功,调用自己的 findClass 去加载
  18. if (c == null) {
  19. c = findClass(name);
  20. }
  21. return c
  22. }
  23. protected Class<?> findClass(String name){
  24. //1. 根据传入的类名 name,到在特定目录下去寻找类文件,把.class 文件读入内存
  25. ...
  26. //2. 调用 defineClass 将字节数组转成 Class 对象
  27. return defineClass(buf, off, len);
  28. }
  29. // 将字节码数组解析成一个 Class 对象,用 native 方法实现
  30. protected final Class<?> defineClass(byte[] b, int off, int len){
  31. ...
  32. }
  33. }
  • JVM的类加载器是分层次的,它们有父子关系,每个类加载器持有一个parent字段,指向父加载器
  • defineClass是个工具方法,它的职责是调用native方法把Java类的字节码解析成一个Class对象,所谓的native方法就是由C语言实现的方法,Java通过JNI机制调用
  • findClass方法的主要职责就是找到”.class”文件,可能来自文件系统或者网络,找到后把”.class”文件读到内存得到字节码数组,然后调用defineClass方法得到Class对象
  • loadClass是个public方法,说明它才是对外提供服务的接口。首先检查这个类不是是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载,这是一个递归调用,即子加载器持有父加载器的引用,当一个类加载器需要加载一个Java类是,会先委托父加载器去加载,父加载器在自己的加载路径中搜索Java类,当父加载器在自己的加载范围内找不到时,会交给子加载器加载,这就是双亲委派机制

3.2 分类

3.2.1 JVM视角

站在JVM的角度,只存在两种不同的类加载器:

  • 启动类加载器:它使用C++实现,是JVM的一部分
  • 其他类加载器:使用Java语言实现,全部继承抽象类Java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类

3.2.2 开发人员视角
  • 启动类加载器:BootstrapClassLoader,由C语言实现,负责加载存放在JDK\jre\lib, 或被-Xbootclasspath参数指定的路径中的,所有的java.*开头的类均被BootstrapClassLoader加载,启动类无法被Java程序直接引用。
  • 拓展类加载器:ExtClassLoader,负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库,(如javax.*开头的类),开发者可以直接使用扩展类加载器
  • 系统类加载器:AppClassLoader, 负责加载用户类路径(ClassPath)所指定的类,可以直接使用该类加载器,如果应用程序中没有自定义过类加载器,AppClassLoader为默认加载器
  • 自定义类加载器: 加载自定义路径下的类
    • 执行非置信代码之前,自动验证数字签名
    • 动态地创建符合用户特定需要的定制化构建类
    • 从特定的场所取得java class, 比如数据库或者网络

这些类加载器的工作原理一样,区别是他们的加载路径不同,也就是说findClass这个方法查找的路径不同。双亲委派机制就是为了保证一个Java类在JVM中是唯一的,假如你不小心写了一个与JRE核心类同名的类,比如Object类,双亲委派机制保证能加载的是JRE中的Object类,因为AppClassLoader在加载自定义的Object类时,会委托给ExtClassLoader去加载,而ExtClassLoader会委托给BootstrapClassLoader, BootstrapClassLoader发现已经加载过,会直接返回。
类加载器的父子关系不是通过继承实现的,而是通过parent成员变量指向的。

3.3 类加载机制

  • 全盘负责: 当一个类加载器负责某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
  • 父类委托:先让父类加载器加载,当父类加载器无法加载时使用子类加载器加载
  • 缓存机制:缓存机制保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区中不存在,才会读取该类对应的二进制数据,并将其转换为Class对象,存入缓存区中。这就是修改Class后需要重启JVM的原因。

3.4 双亲委派模型的意义:

  • 防止内存中出现多份相同的字节码
  • 保证Java程序安全稳定运行

4. 类的加载

类加载有三种方式

  • 命令行启动应用时由JVM初始化加载
  • 通过Class.forName()方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载
  1. public class Test {
  2. static {
  3. System.out.println("静态初始化块执行");
  4. }
  5. }
  6. public class ClassLoaderTest {
  7. public static void main(String[] args) throws ClassNotFoundException {
  8. ClassLoader loader = Thread.currentThread().getContextClassLoader();
  9. System.out.println(loader);
  10. // 使用 ClassLoader.loadClass() 来加载类,不会执行初始化块
  11. loader.loadClass("Test");
  12. // 使用Class.forName()来加载类,默认会执行初始化块
  13. Class.forName("Test");
  14. // 使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态代码块
  15. Class.forName("Test", false, loader);
  16. }
  17. }

Output:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. 静态初始化块执行

4.1 Class.forName()和ClassLoader.loadClass区别

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass(): 只将.class文件加载到jvm中,当newInstance时才会执行static块

5. 自定义类加载器

通常情况下,可以直接使用系统类加载器,由时候也需要自定类加载器,比如通过网络来传输Java类的字节码,为保证安全,经过加密处理,这时就需要自定义类加载器了。

  1. import java.io.*;
  2. public class MyClassLoader extends ClassLoader {
  3. private String root;
  4. protected Class<?> findClass(String name) throws ClassNotFoundException {
  5. byte[] classData = loadClassData(name);
  6. if (classData == null) {
  7. throw new ClassNotFoundException();
  8. } else {
  9. return defineClass(name, classData, 0, classData.length);
  10. }
  11. }
  12. private byte[] loadClassData(String className) {
  13. String fileName = root + File.separatorChar
  14. + className.replace('.', File.separatorChar) + ".class";
  15. try {
  16. InputStream ins = new FileInputStream(fileName);
  17. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  18. int bufferSize = 1024;
  19. byte[] buffer = new byte[bufferSize];
  20. int length = 0;
  21. while ((length = ins.read(buffer)) != -1) {
  22. baos.write(buffer, 0, length);
  23. }
  24. return baos.toByteArray();
  25. } catch (IOException ex) {
  26. ex.printStackTrace();
  27. }
  28. return null;
  29. }
  30. public String getRoot() {
  31. return root;
  32. }
  33. public void setRoot(String root) {
  34. this.root = root;
  35. }
  36. }
  37. public class ClassLoaderTest {
  38. public static void main(String[] args) throws ClassNotFoundException {
  39. MyClassLoader classLoader = new MyClassLoader();
  40. classLoader.setRoot("C:\\Users\\Desktop\\Code\\class\\src\\MyClassLoader.java");
  41. Class<?> testClass = null;
  42. try {
  43. testClass = classLoader.loadClass("Test");
  44. Object object = testClass.newInstance();
  45. System.out.println(object.getClass().getClassLoader());
  46. } catch (ClassNotFoundException e) {
  47. e.printStackTrace();
  48. } catch (InstantiationException e) {
  49. e.printStackTrace();
  50. } catch (IllegalAccessException e) {
  51. e.printStackTrace();
  52. }
  53. }
  54. }

一般不要重写loadClass方法,会破坏双亲委派模式