深入浅出Java虚拟机(JVM)-类加载机制

文章基于学习《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》和宋红康老师《JVM系列》课程进行总结。

深入浅出Java虚拟机(JVM)系列文章

深入浅出Java虚拟机(JVM)-认识JVM

深入浅出Java虚拟机(JVM)-JVM内存区域

1、引言

Class文件中的描述信息都需要被加载到虚拟机中之后才能被运行和使用。虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、解析、和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。本文将从类在什么时候被加载,类加载过程,类加载器这几个方面讲解。

2、类加载时机

类的完整生命周期包括:加载、验证、准备、解析、初始化、使用、卸载七个阶段。其中验证、准备和解析统称为连接。如图:

加载、验证、准备、初始化、卸载这五个阶段顺序是确定的,而解析阶段则不一定:它有可能发生在初始化之后,这为了支持Java语言的运行时绑定。

类加载的第一阶段:加载,虚拟机没有约束在什么时候开始。但是对初始化阶段,虚拟机做了严格规定,在以下四种情况会触发初始化:

  1. 使用new关键字实例化对象、读或设置类静态变量(静态常量除外,被final修饰,在编译时放入常量池)、调用类静态方法。
  2. 对类进行反射调用,如果类没有进行初始化,则需要先初始化。
  3. 当初始化一个类时,发现其父类没有初始化,则需要先初始化其父类。
  4. 当虚拟机启动时,用户指定的要执行的主类(包含main方法的类),虚拟机会先初始化这个主类。

3、类加载过程

3.1、加载

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

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

字节流可以有很多方式获取,例如:

  1. 从Zip包中获取(JAR、WAR)
  2. 从网络中获取(Web Applet)
  3. 运行时计算生成(动态代理)
  4. 由其他文件生成(JSP应用)
  5. 从数据库中读取(SAP Netweaver)
  6. ......

3.2、

验证时连接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机安全。如果虚拟机不检查输入的字节流,很可能会因为载入了有害的字节流导致系统崩溃,所以验证时虚拟机对自身保护的一项重要工作。

验证阶段主要会完成四个阶段的检验过程:文件格式校验元数据校验字节码校验符号引用校验

3.3、准备

准备阶段是正式为类变量(被static修饰的变量,而不是实例变量)在方法区分配内存并设置类变量初始值的阶段。例如:

java 复制代码
    public static int value = 123;

变量value在准备阶段过后的初始值为0而不是123,

如果类变量value定义如下:

java 复制代码
public static final int value = 123;

编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue的设置将value赋值为123。

基本数据类型的零值

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

3.4、解析

解析是将常量池内的符号引用替换为直接引用的过程。

解析主要针对类或接口、字段、类方法、接口方法四类符号引用进行。

3.5、初始化

初始化阶段就是执行类构造器方法()的过程

  • 此方法不需要定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
  • 构造器方法中指令按语句在源文件中出现的顺序执行;
  • ()不同于类的构造器。(构造器是虚拟机视角下的())
  • 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
  • 虚拟机必须保证一个类的()方法在多线程下被同步加锁。
java 复制代码
    public class IinitTest {
    ​
        public static void main(String[] args) {
            System.out.println(Sub.B);
        }
    }
    ​
    class Parent {
        public static  int A = 1;
    ​
        static {
            A=2;
        }
    }
    ​
    class Sub extends Parent {
        public static  int B = A;
    }

结果:

shell 复制代码
    2

上面的结果说明:父类的()方法先于子类的()方法执行

4、类加载器

4.1、虚拟机自带的加载器

从Java虚拟机角度讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,另外一种就是所有其他的类加载器,独立于虚拟机外部,并且都是继承自抽象类java.lang.ClassLoader。

从开发人员角度看,系统提供了三种类加载器:

  • 启动类加载器(Bootstrap ClassLoader)

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

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

    • java语言编写,由sun.misc.Launcher$AppClassLoader实现。
    • 继承自ClassLoader类。
    • 父类加载器为扩展类加载器。
    • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
    • 该类加载器是程序中默认的类加载器,一般来说,Java应用程序的类都由它来完成加载。
    • 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
java 复制代码
        public class ClassLoaderTest {
        ​
            public static void main(String[] args) {
                // 应用类加载器
                ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
                System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
                // 获取其上层:扩展类加载器
                ClassLoader extClassLoader = systemClassLoader.getParent();
                System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@4554617c
        ​
                // 获取其上层:获取不到启动类加载器
                ClassLoader bootstrapClassLoader = extClassLoader.getParent();
                System.out.println(bootstrapClassLoader); // null
        ​
                // 对于用户自定义类:默认使用应用类加载器
                ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
                System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
        ​
                // String类使用启动类加载器
                ClassLoader classLoader1 = String.class.getClassLoader();
                System.out.println(classLoader1); // null
            }
        }

4.2、双亲委派模型

工作原理

  1. 如果一个类加载器收到了类加载请求,它不会自己先去加载,而是把这个请求委派给父类的加载器去执行;
  2. 如果父类加载器还存在其他父类加载器,则进一步向上委托,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成加载任务,就返回成功,如果父类加载器无法完成此加载任务,子加载器才会尝试自己加载。
java 复制代码
    package java.lang;
    ​
    public class String {
    ​
        public static void main(String[] args) {
    ​
        }
    }

结果:

为什么要使用双亲委派模型

  • 避免类被重复加载
  • 保护程序安全,防止核心API被篡改

4.3、沙箱安全机制

自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载JDK自带的文件(rt.jar包中java/lang/String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护。

相关推荐
期待のcode3 分钟前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐5 分钟前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲15 分钟前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红16 分钟前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥17 分钟前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v28 分钟前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地40 分钟前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209251 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei1 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot
记得开心一点嘛2 小时前
Redis封装类
java·redis