写点什么

JVM 笔记 -- 来,教你类加载子系统

发布于: 2021 年 03 月 17 日

类加载子系统


类文件首先需要经过类加载子系统,进行加载,进类信息等加载到运行时数据区,生成 Klass 的实例。


在类加载子系统中有以下 3 个阶段操作(广义上的加载):

  • 加载阶段

- Bootstrap ClassLoader:引导类加载器,主要加载 JDK 里面的核心类

- Extension ClassLoader:拓展类加载器

- Application ClassLoader:应用加载器

  • 链接阶段

- 验证

- 链接

- 解析

  • 初始化阶段


如果加载的时候失败了,则不会执行后面的链接等操作。


类加载子系统的作用:

  • 类加载器子系统可以从本地文件或者*网络*中加载 Class 文件,Class 文件开头有特定标识“CAFEBABY”(魔数)。

  • 类加载器只负责将文件加载到运行时数据区,但是否可以运行,是执行引擎管的

  • 加载的类信息存放在方法区中,除了类信息以外,方法区还存放了运行时产量池信息,可能 HIA 包括字符串字面量和数字常量(这部分常量是 Class 文件中常量池部分的内存映射)。


譬如反编译后,会产生常量信息,里面包括常量以及符号引用等:


类加载器 ClassLoader 的角色,以下面的 People.class 为例:


通过类信息实例,可以通过 new 实例化对象,也可以通过 getClassLoader()获取类加载器,也可以通过实例 getClass()获取类信息实例。


  1. People.class 存在本地硬盘上,相当于一个模板,最终可以实例化出 n 个同一个类但是属性不同的实例。

  2. People.class 加载到 JVM 中,被称为 DNA 元数据模板,存放在方法区,也就是类信息。类信息也是对象。

  3. 从.class 文件,到加载到 JVM 中,称为元数据模板,这个过程需要一个转换工具,这个工具就是类加载器(Class Loader)。


加载(Loading)

此处的加载,指的是类加载过程中的第一个阶段(环节),主要工作包括:

  • 1.通过类的全限定名获取定义此类的二进制字节流。

  • 2.将这个二进制字节流所代表的静态存储结构转化为方法区(JDK7 以及之前叫永久代,JDK8 之后成为元空间)的运行时数据结构。

  • 3.在内存中生成一个该类的java.lang.Class对象,作为方法区该类的各种数据的访问入口,也就是类信息对象。


类的.class 文件来源方式包括以下:

  • 本地系统直接加载

  • 网络传输获取

  • 从 zip 压缩包读取

  • 运行的时候计算生成,譬如动态代理技术

  • 由其他文件生成,譬如场景:JSP

  • 从加密文件中解密获得


链接


链接阶段又分为 3 个阶段:

  • 验证:

- 目的是校验安全和法,确保 Class 文件的字节流中包含信息符合当前虚拟机要求,保证加载的类的正确性,不会危害到虚拟机的安全。

- 主要包括 4 种验证:

- 文件格式验证(譬如文件开头是"CAFEBABY")

- 元数据验证

- 字节码验证

- 符号引用验证

  • 准备:

- 为类变量(static)分配内存并且设置该变量的默认初始值,即零值

- 不包含 final 修饰的 static,因为 final 在编译的时候已经分配了,准备阶段会显示初始化。

- 不会为实例变量分配初始化,类变量会分配在方法区,但是实例变量是跟随对象一起分配在 Java 堆里面(一般情况)

  • 解析:

- 将常量池的符号引用转化成为直接引用的过程

- 事实上,解析操作往往会伴随 JVM 在执行完初始化之后再执行

- 符号引用就是一组符号来描述所引用的目标,《Java 虚拟机规范》的 Class 文件格式中,直接引用就是直接指向目标的指针,相对偏移量或者一个间接定位到目标的句柄。

- 解析这个阶段,主要是针对类或者接口,字段,类方法,接口方法,方法类型等等,对应的常量池中的 CONSTANTClassinfo,CONSTANTFieldredinfo,CONSTANTMethodrefinfo 等。


初始化

初始化,就是执行类的构造器<clinit>()的过程,注意<clinit>()是类的构造器,不是对象的。<clinit>()是初始化类的,就是把类装到 JVM 里的初始化,不是运行时对象的初始化。


<clinit>()这个方法不需要显式定义,而是javac编译器自动收集类中的所有变量的赋值动作,加上静态代码块,合并成的一个方法。


<clinit>()中代码的顺序和我们在类文件写的顺序一致。


执行子类的<clinit>()方法之前,JVM 会保证先执行其父类的<clinit>(),默认父类是Object



仔细观察上面的代码,会发现,final 的属性,即使是 static 修饰的,在<clinit>()里面都不会存在,这是为什么呢?


这是因为 final 修饰的是常量,常量不会在初始化的时候执行赋值!!!常量在编译的时候已经分配了,准备阶段会显示初始化。


如果我们将 final 去掉,就可以发现,去掉 final 修饰,字节码就会加上该字段的赋值:(下面的 ldc 是指常量池的意思,从常量池编号为 #6 的地方,加载该常量)


虚拟机在初始化的时候,已经保证了类的<clinit>()方法,即使在多线程的环境下,也只会执行一次,其底层的逻辑就是默认同步加锁了。


【作者简介】

秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人写作方向:Java 源码解析,JDBC,Mybatis,Spring,redis,分布式,剑指 Offer,LeetCode 等,认真写好每一篇文章,不喜欢标题党,不喜欢花里胡哨,大多写系列文章,不能保证我写的都完全正确,但是我保证所写的均经过实践或者查找资料。遗漏或者错误之处,还望指正。


2020年我写了什么?


开源刷题笔记


平日时间宝贵,只能使用晚上以及周末时间学习写作,关注我,我们一起成长吧~


发布于: 2021 年 03 月 17 日阅读数: 11
用户头像

纵使缓慢,驰而不息。 2018.05.17 加入

慢慢走,比较快。公众号:秦怀杂货店

评论

发布
暂无评论
JVM笔记 -- 来,教你类加载子系统