目录

  • 0 使用类的准备工作
    • 初始化(Init)
  • 1 加载(Load)
    • 1.1 详细过程
      • 1.1.1 通过类全限定名获取该类的二进制字节流
      • 1.1.2 静态存储结构=》运行时数据结构
      • 1.1.3 创建`java.lang.Class`对象
    • 1.2 加载源
    • 1.3 类 V.S 数组的加载过程
    • 1.4 注意点
  • 2 链接(Link)
    • 2.1 验证
      • 目的
      • 必要性
      • 详细过程
        • 文件格式验证
        • 元数据验证
        • 字节码验证
        • 符号引用验证
    • 2.2 准备
    • 2.3 解析
  • 3 初始化
    • 初始化时机
    • tips
  • 4 类的卸载

0 使用类的准备工作

Java基本类型都由JVM预先定义好的。另一大头就是引用类型,Java将其细分为:

  1. 接口
  2. 数组类
    并没有对应字节流,而是由Java虚拟机直接生成
  3. 泛型参数
    泛型参数会在编译过程中被擦除,因此JVM实际上只有前三种

数组类由JVM直接生成,其他两种则有对应字节流。字节流最常见的就是Java编译器生成的class文件。
也可在程序内部直接生成或从网络中获取(Java applet)字节流。
这些字节流会被加载到JVM,成为类或接口,为方便,下文统一表达为“类”。

无论是直接生成的数组类,还是加载的类,JVM都需要进行链接和初始化。
任何程序都需要加载到内存才能与CPU进行交流,字节码.class文件同理也需要加载到内存中,才可实例化类。
ClassLoader的使命就是提前加载.class 类文件到内存中,在加载类时,使用的是Parents Delegation Model(溯源委派加载模型)。

Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的加载、链接、初始化:

  • Java 类加载过程

初始化(Init)

执行类构造器 <clinit> 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。

类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。
在这个过程中,JVM会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。
某些类在使用时,也可以按需由类加载器进行加载。

  • 全小写的class是关键字,用来定义类
  • 而首字母大写的Class,它是所有class的类
    类已经是现实世界中某种事物的抽象,为什么这个抽象还是另外一个类Class的对象?
    示例代码如下:




    ● 第1处说明:
    Class类下的newInstance()在JDK9中已经置为过时,使用getDeclaredConstructor().newInstance()的方式
    着重说明一下new与newInstance的区别
  • new是强类型校验,可以调用任何构造方法,在使用new操作的时候,这个类可以没有被加载过
  • 而Class类下的newInstance是弱类型,只能调用无参构造方法
    • 如果没有默认构造方法,就拋出InstantiationException异常;
    • 如果此构造方法没有权限访问,则拋 IllegalAccessException异常

Java 通过类加载器把类的实现与类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。

● 第2处说明:
可以使用类似的方式获取其他声明,如注解、方法等

  • 类的反射信息

    ● 第3处说明: private 成员在类外是否可以修改?
    通过setccessible(true),即可使用Class类的set方法修改其值
    如果没有这一步,则抛出如下异常:

1 加载(Load)

类加载(Class Loading)过程的第一步:查找字节流,并且据此创建类的过程。

由类加载器执行。读取类文件(通常在 classpath 所指定的路径中查找,但classpath非必须),查找字节码,从而产生二进制流,并转为特定数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。

1.1 详细过程

1.1.1 通过类全限定名获取该类的二进制字节流

全限定名保证全局唯一,二进制字节流即class文件。

在程序运行过程中,当要访问一个类时,若发现该类还未被加载,并满足类初始化的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程。

为何要把类加载阶段的 通过类的全限定名来获取该类的二进制字节流 这个动作交给虚拟机之外的类加载器来完成呢?

可自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,大大增强加载器的灵活性。

1.1.2 静态存储结构=》运行时数据结构

将字节流的静态存储结构转化为方法区的运行时数据结构

1.1.3 创建java.lang.Class对象

在内存中创建一个该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口,所以所有类都可以调用 getClass 方法。

程序在运行中所有对该类的访问都通过这个类对象,也就是这个Class对象是提供给外界访问该类的接口

1.2 加载源

JVM规范对于加载过程给予了较大的宽松度。一般二进制字节流都从已经编译好的本地class文件中读取,此外还可以从以下地方读取

  • zip包
    Jar、War、Ear等
  • 其它文件生成
    由JSP文件中生成对应的Class类
  • 数据库中
    将二进制字节流存储至数据库中,然后在加载时从数据库中读取.有些中间件会这么做,用来实现代码在集群间分发
  • 网络
    从网络中获取二进制字节流,比如Applet
  • 运行时动态计算生成
    动态代理技术,用PRoxyGenerator.generateProxyClass为特定接口生成形式为"*$Proxy"的代理类的二进制字节流

1.3 类 V.S 数组的加载过程

数组也有类型,称为“数组类型”,如:

String[] str = new String[10];

这个数组的数组类型是Ljava.lang.String,而String只是这个数组的元素类型。
当程序在运行过程中遇到new关键字创建一个数组时,由JVM直接创建数组类,再由类加载器创建数组中的元素类型。

而普通类的加载由类加载器创建。既可以使用系统提供的引导类加载器,也可以由用户自定义的类加载器完成(即重写一个类加载器的loadClass()方法)

1.4 注意点

  • JVM规范并未给出类在方法区中存放的数据结构
    类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,虚拟机规范并没有指定
  • JVM规范并没有指定Class对象存放的位置
    在二进制字节流以特定格式存储在方法区后,JVM会创建一个java.lang.Class类的对象,作为本类的外部访问接口
    既然是对象就应该存放在Java堆中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象
    HotSpot将Class对象存放在方法区.
  • 加载阶段和链接阶段是交叉的
    类加载的过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制.也就是说,类加载过程中,必须按照如下顺序开始:

加载 -> 链接 -> 初始化

但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉 。

2 链接(Link)

将已读入内存的类的二进制数据合并到 JVM 运行时环境,包括验证、准备、解析:

  • 准备
    为类的static字段分配内存,并设定初始默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局
  • 解析
    如果需要的话,将解析这个类创建的对其他类的所有引用,将常量池的符号引用转换成直接引用 。

2.1 验证

验证阶段比较耗时,它非常重要但不一定必要(因为对程序运行期没有影响),如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间

目的

确保被加载类的正确性。验证类中的字节码,是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理。
即保证二进制字节流中的信息符合虚拟机规范,且无安全问题。

必要性

虽然Java是门安全的语言,能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行。即Java的安全性是通过编译器来保证的。

但编译器和虚拟机是两个独立东西,虚拟机只认二进制字节流,不管所获得的二进制字节流哪来的。当然,如果是编译器给它的,那就相对安全,但若从其它途径获得,则无法确保该二进制字节流是安全的。

通过上文可知,虚拟机规范中没有限制二进制字节流的来源,在字节码层面上,上述Java代码无法做到的都是可以实现的,至少语义上是可以表达出来的,为了防止字节流中有安全问题,需要验证!

详细过程

文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理.
本验证阶段是基于二进制字节流进行的,只有通过本阶段验证,才被允许存到方法区
后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流.

通过上文可知,加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区
而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区
也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作
这个过程印证了:加载和验证是交叉进行的

元数据验证

对字节码描述的信息进行语义分析,确保符合Java语法规范。

字节码验证

验证过程的最复杂的阶段。 本阶段对数据流和控制流(主要为方法体)进行语义分析。字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法体的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全。

符号引用验证

发生在JVM将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段,对类自身以外的信息进行匹配校验,确保解析能正常执行.

2.2 准备

Java代码中对静态字段的具体初始化,则会在稍后的初始化阶段中进行。

  • 为(被加载类)已在方法区中的类的静态成员变量分配内存
  • 为静态成员变量设置初始值
    初始值为0、false、null等
public static final int value = 123;

准备阶段后 a 的值为 0,而不是 123,要在初始化之后才变为 123,但若被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段(此处将value赋为123)。

部分JVM还会在此阶段构造其他跟类层次相关的数据结构,如用来实现虚方法的动态绑定的方法表。

2.3 解析

在class文件被加载至JVM前,该类不知道其它类及其方法、字段所对应具体地址,甚至不知道自己方法、字段的地址。
因此,每当需要引用这些成员,Java编译器会生成一个符号引用。在运行阶段,这符号引用一般都能无歧义定位到具体目标。

对一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,指代所要调用的方法。

解析阶段正是将这些符号引用解析成实际引用。若符号引用指向一个:

  • 未被加载的类
  • 或未被加载类的字段或方法

则解析将触发这个类的加载(但未必触发这个类的链接及初始化)。

把常量池中的符号引用转换成直接引用的过程。包括:

  • 符号引用
    以一组无歧义的符号来描述所引用的目标,与虚拟机的实现无关。
  • 直接引用
    直接指向目标的指针、相对偏移量、或是能间接定位到目标的句柄,是和虚拟机实现相关的。

主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。

在盖房子的语境下:

  • 符号引用好比“Tony的房子”,不管它存在不存在,都可以用这种说法指代Tony的房子
  • 实际引用则好比实际的通讯地址,如想与Tony通信,则需要启动盖房子的过程,让他真的有房

Java虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

3 初始化

要初始化一个静态字段:

  • 在声明时直接赋值
    若直接赋值的静态字段被final修饰,且为基本类型或字符串,则该字段会被Java编译器标记成常量值(ConstantValue),其初始化直接由JVM完成。此外的直接赋值操作及所有静态代码块中的代码,则会被Java编译器置于同一方法< clinit >
  • 在静态代码块中对其赋值

类加载的最后一步初始化,给标记为常量值的字段赋值及执行< clinit >方法的过程。JVM会通过加锁确保类的< clinit >仅被执行一次。

仅当初始化完成后,类才正式成为可执行状态。即只有当房子装修过后,你才能真正住进去吧?
真正开始执行类中定义的Java程序代码(或字节码),类的初始化就是为类的静态变量赋初始值,初始化阶段就是执行类构造器<clinit>的过程。

  • 如果类还没有加载和连接,就先加载和连接
  • 如果类存在父类,且父类没有初始化,就先初始化父类
  • 如果类中存在初始化语句,就依次执行这些初始化语句
  • 如果是接口
    • 初始化一个类时,并不会先初始化它实现的接口
    • 初始化一个接口时,并不会初始化它的父接口
      只有当程序首次使用接口里面的变量或者是调用接口方法的时候,才会导致接口初始化
  • 调用Classloader类的loadClass方法来装载一个类,并不会初始化这个类,不属于对类的主动使用

clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。
在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。

初始化时机

Java程序对类的使用方式分为:

  • 主动使用
  • 被动使用

JVM必须在每个类或接口“首次主动使用”时才初始化它们,被动使用类不会导致类的初始化。主动使用的场景:

  • 创建类实例
  • 访问某个类或接口的静态变量
    如果是 final 常量,而常量在编译阶段就会在常量池,没有引用到定义该常量的类,因此不会触发定义该常量类的初始化
  • 调用类的静态方法
  • 反射某个类
  • 初始化某个类的子类,而父类还没有初始化
  • JVM启动的时候运行的主类(等于第三条)
  • 定义了 default 方法的接口,当接口实现类初始化时

tips

  • clinit()方法是IDE自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,IDE收集的顺序是由语句在源文件中出现的顺序所决定的.
  • 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问.
public class Test {static {i=0;System.out.println(i); //编译失败:"非法向前引用"}static int i = 1;
}
  • 实例构造器init()需要显式调用父类构造器,而类的clinit()无需调用父类的类构造器,JVM会确保子类的clinit()方法执行前已执行完毕父类的clinit()方法。
    因此在JVM中第一个被执行的clinit()方法的类肯定是java.lang.Object
  • 如果一个类/接口无static代码块,也无 static成员变量的赋值操作,则编译器不会为此类生成clinit()方法
  • 接口也需要通过clinit()方法为接口中定义的static成员变量显示初始化。
  • 接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成clinit()方法。不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法。只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法。
  • 虚拟机会保证在多线程环境中一个类的clinit()方法别正确地加锁,同步.当多条线程同时去初始化一个类时,只会有一个线程去执行该类的clinit()方法,其它线程都被阻塞等待,直到活动线程执行clinit()方法完毕.

其他线程虽会被阻塞,只要有一个clinit()方法执行完,其它线程唤醒后不会再进入clinit()方法。同一个类加载器下,一个类型只会初始化一次。

4 类的卸载

当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载。
Jvm自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的。

参考

  • 《码到成功》
  • 《深入理解Java虚拟机第三版》
  • 最新版JDK17下的JVM类加载器原理详解

JVM类加载、验证、准备、解析、初始化、卸载过程详解相关推荐

  1. 深入理解Java虚拟机之Java类加载机制,Java类实例化过程详解。

    目录 Java类加载机制 类加载过程 加载(Loading) 连接(Linking) 初始化(Initialzation) 使用(Useing) 卸载(Unloading) 引言 什么情况下开始类加载 ...

  2. U-Boot 之三 U-Boot 源码文件解析及移植过程详解

      在之前的博文 Linux 之八 完整嵌入式 Linux 环境介绍及搭建说明 中我们说了要一步步搭建整个嵌入式 Linux 运行环境.我所使用的硬件平台及整个要搭建的嵌入式 Linux 环境见博文 ...

  3. java 解析器_高性能Java解析器实现过程详解

    如果你没有指定数据或语言标准的或开源的Java解析器, 可能经常要用Java实现你自己的数据或语言解析器.或者,可能有很多解析器可选,但是要么太慢,要么太耗内存,或者没有你需要的特定功能.或者开源解析 ...

  4. 高性能Java解析器实现过程详解

    如果你没有指定数据或语言标准的或开源的Java解析器, 可能经常要用Java实现你自己的数据或语言解析器.或者,可能有很多解析器可选,但是要么太慢,要么太耗内存,或者没有你需要的特定功能.或者开源解析 ...

  5. HTML的解析顺序及过程详解

    1.解析规则: 1.html字符串被浏览器接收后一句一句读取并解析 2.如果解析到link标签,便发送请求获取css: 3.解析到script标签,发送请求获取js后并执行相应的代码 4.解析到img ...

  6. Spring 是解析配置类过程详解

    Spring执行流程图如下: Spring执行流程图 这个流程图会随着我们的学习不断的变得越来越详细,也会越来越复杂,希望在这个过程中我们都能朝着精通Spring的目标不断前进!  在上篇文章我们学 ...

  7. 微信小程序富文本解析点击图片放大_微信小程序解析富文本过程详解

    前言 最近公司在开发OTA微信小程序,一些页面的详情内容是HTML富文本格式的的,但是微信小程序不能直接解析HTML,需要将内容中的HTML标签转换成微信小程序所支持的标签. 开始的时候想过自己写方法 ...

  8. django-rest-framework解析请求参数过程详解

    https://www.jb51.net/article/165699.htm 转载于:https://www.cnblogs.com/gcgc/p/11544187.html

  9. U-Boot 之一 零基础编译 U-Boot 过程详解 及 编译后的使用说明

      在之前的博文 Linux 之八 完整嵌入式 Linux 环境介绍及搭建过程详解 中我们说了要一步步搭建整个嵌入式 Linux 运行环境,今天就开始编译 U-Boot.我所使用的硬件平台及整个要搭建 ...

最新文章

  1. VS2013安装OpenCV4.1版本并搭建一个小程序
  2. CNDO-INTGRL-SS-AINTGS-斯莱特轨道指数
  3. sqlyog设置自动补全_sqlyog智能提示如何设置?sqlyog智能提示设置流程介绍
  4. 吴恩达深度学习笔记9-Course3-Week2【机器学习策略(ML Strategy)2】
  5. 将内存单元中小写字符改成大写字符
  6. leetcode-387-字符串中的第一个唯一字符
  7. ocelot简单入门
  8. IAR stm8 调试时无法看到局部变量解决
  9. U盘流畅运行linux发行版,做各种linux发行版的启动U盘方法
  10. 英特网rst服务器未在运行,技术员示范win7系统提示“英特尔(R)RST服务未在运行”的详细办法...
  11. 我要重构“软件行业”......
  12. 关于mat2gray
  13. [转载] python迭代器
  14. 前景检测算法(三)--帧差法
  15. 老妈~老妈~我爱你~~~~~~~阿弥陀佛保佑你~~~~~~~
  16. ENGINEER 05
  17. Android webview拦截请求
  18. IRQL_NOT_LESS_OR_EQUAL蓝屏分析
  19. 为什么你996猝死,老板007没事?
  20. Oracle安装之后电脑变慢

热门文章

  1. BUUCTF reverse1-10 WP
  2. 微信运营营销推广方案
  3. Vue引入并使用Element-UI组件库的两种方式
  4. 海康威视SDK二次开发通过云台参数设置控制摄像机的位置
  5. 计算机win7卡顿如何解决方法,电脑win7系统出现卡顿怎么处理
  6. 简单实现一个虚拟形象系统
  7. php市场占比 2019,2019年家电市场份额占比排名
  8. omf多路径 oracle_Oracle OMF 功能详解
  9. Response响应详解
  10. Redis深入浅出—hash、set