类加载子系统
类文件首先须要通过类加载子系统,进行加载,进类信息等加载到运行时数据区,生成 Klass 的实例。
在类加载子系统中有以下 3 个阶段操作(狭义上的加载):
-
加载阶段
- Bootstrap ClassLoader:疏导类加载器,次要加载 JDK 外面的外围类
- Extension ClassLoader:拓展类加载器
- Application ClassLoader:利用加载器
-
链接阶段
- 验证
- 链接
- 解析
- 初始化阶段
如果加载的时候失败了,则不会执行前面的链接等操作。
类加载子系统的作用:
- 类加载器子系统能够从 本地文件 或者 网络 中加载 Class 文件,Class 文件结尾有特定标识“CAFEBABY”(魔数)。
- 类加载器只负责将文件加载到运行时数据区,但 是否能够运行,是执行引擎管的。
- 加载的类信息寄存在办法区中,除了类信息以外,办法区还寄存了运行时产量池信息,可能 HIA 包含字符串字面量和数字常量(这部分常量是 Class 文件中常量池局部的内存映射)。
譬如反编译后,会产生常量信息,外面包含常量以及符号援用等:
类加载器 ClassLoader 的角色, 以上面的 People.class 为例:
通过类信息实例,能够通过 new 实例化对象,也能够通过 getClassLoader()获取类加载器,也能够通过实例 getClass()获取类信息实例。
- People.class 存在本地硬盘上,相当于一个模板,最终能够实例化出 n 个同一个类然而属性不同的实例。
- People.class 加载到 JVM 中,被称为 DNA 元数据模板,寄存在办法区,也就是类信息。类信息也是对象。
- 从.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 文件格式中,间接援用就是间接指向指标的指针,绝对偏移量或者一个间接定位到指标的句柄。
- 解析这个阶段,次要是针对类或者接口,字段,类办法,接口办法,办法类型等等,对应的常量池中的 CONSTANT_Class_info,CONSTANT_Fieldred_info,CONSTANT_Methodref_info 等。
初始化
初始化,就是执行类的结构器 <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 年我写了什么?
开源刷题笔记
素日工夫贵重,只能应用早晨以及周末工夫学习写作,关注我,咱们一起成长吧~