深入理解JVM——第二篇:类加载子系统

本文详细剖析了Java虚拟机的类加载过程,包括加载器、双亲委派模型、验证、准备和解析等关键步骤,并探讨了初始化阶段的执行顺序。重点讲解了类加载器的作用、双亲委派模型的运作原理,以及打破模型的场景如WebAppClassLoader的应用。
摘要由CSDN通过智能技术生成

1、类加载概述

        虚拟机将 Class 文件加载到内存,并对数据进行校验、解析、初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为:Java 虚拟机的类加载机制。

2、类加载的过程

        一个类从被加载到 JVM 内存中到卸载出内存为止,整个生命周期为 加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析三个部分统称为连接。如下图:

加载

        加载阶段主要内容:通过一个类的全限定名来获取定义此类的二进制字节流,然后把类相关信息的静态存储结构转化为方法区的运行时数据结构,并且在中生成一个 java.lang.Class 类的对象,作为方法区中这个类的各种数据的访问入口,总结一句话就是:把字节码数据加载到内存中。

类加载器

        实现通过一个类的全限定名来获取二进制字节流这个动作的代码就是类加载器,这个动作是JVM外部实现的所以程序可以自己决定如何获取需要的类即自定义类加载器,任意一个类必须由加载它的类加载器和这个类本身的全限定名才能确立在JVM中的唯一性。即使是同一份 Class 文件但是由不同的类加载器加载的,也是两个不同的类。

双亲委派模型

        为了防止内存中存在多份同样的字节码,如果一个类加载器收到了类加载的请求,它不会自己去加载,而是把这个请求委托给父类加载器去完成,最终都应该传送到启动类加载器中,只有当父加载器无法加载这个请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。但是这里的父子关系不是继承,而是组合的关系来复用父加载器的代码。

        程序中默认的类加载器如下图:

        启动类加载器:主要负责加载存放在 <JAVA_HOME>\lib 目录。

        扩展类加载器:主要复制加载 <JAVA_HOME>\lib\ext 目录。

        应用程序(系统)类加载器:负责加载用户路径(ClassPath)上的所有类库。如果程序没有自定义类加载器,一般情况下这个就是程序中默认的类加载器。

        打破双亲委派模型:只需要自定义类加载器,重写loadclass方法,不往上委托请求,这就打破了双亲委派机制。

        打破双亲委派的场景:Tomcat 给每个 Web 应用创建一个类加载器实例 WebAppClassLoader 重写 loadClass 方法,优先加载当前应用目录下的类,如果当前找不到了才往上找,这样就做到了 Web 应用层级的隔离,打破了双亲委派。

验证

        验证阶段主要内容:当代码数据被加载到内存中后,虚拟机就会对代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。首先对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。 然后对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。

准备

        为 类变量 即(被static修饰的变量)分配内存并设置初始值,但是这里的初始值指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。在准备阶段JVM只会为类变量分配内存,而不会为实例变量分配内存,实例变量的内存分配随着对象实例化时(初始化阶段)一起分配在 Java 堆中,被 final 关键字修饰的变量,在准备阶段就会被赋予设置的值。

解析

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

        符号引用:任何形式的字面量,只要能定位到目标即可,符号引用与内存无关,引用的目标不一定是加载到JVM内存中的内容。

        直接引用:可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用与JVM内存直接相关,如果有了直接引用,那么引用的目标必须已经在JVM内存中存在。

初始化

        到初始化阶段JVM才真正开始执行类中编写的Java代码,为类变量和其他资源赋设置的值。初始化阶段首先会执行类构造器 <clinit>() 之后再执行实例构造器 <init>() 方法。

        <clinit>():并不是我们自己编写的方法,而是由编译器自动收集类变量的赋值语句、静态代码块中的语句组成产生的,按语句出现的顺序,最终组成类构造器由 JVM 执行。JVM会保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 已经执行完毕。因此第一个执行 <clinit>() 方法的类型肯定是java.lang.Object。

        <init>():生成方式与类构造器相同,但针对的是实例变量,JVM必须保证一个类的 <clinit>() 方法在多线程环境中加锁同步,多个线程同时去初始化一个类,那么只有其中一个线程去执行这个类的 <clinit>() 方法。

        初始化顺序:

父类的静态变量         父类的静态代码块
子类的静态变量         子类的静态代码块
父类的非静态变量      父类的非静态代码块        父类的构造方法
子类的非静态变量      子类的非静态代码块        子类的构造方法

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值