让宝贝舍友理解JVM 第二弹 上(类加载子系统)

(:最近在看尚硅谷的JVM教程,把学到的知识点总结一下,让舍友看完也懂JVM。

尚硅谷2020最新版宋红康JVM教程 结合****看更容易懂。

废话少说,立即开冲。
让宝贝舍友理解JVM 第二弹 上(类加载子系统)
上一篇文章我们已经大概了解到了JVM是什么东西,这篇文章我们就开始学习JVM第一个结构,类加载子系统

有兴趣回顾上一篇文章的同学可以点旁边链接: 让宝贝舍友理解JVM 第一弹(JVM简介)

什么是类加载子系统:

让宝贝舍友理解JVM 第二弹 上(类加载子系统)
我们看回上一章的图片,可以很清楚的知道,类加载子系统就是负责加载 class文件的。
让宝贝舍友理解JVM 第二弹 上(类加载子系统)

类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。

ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)决定。

加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常最池信息,可能还包括字符串字而量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

所以我们可以用一句话概括一下什么是类加载子系统:

由于JVM是看不懂class文件的,所以我们用类加载子系统中的类加载器将class文件转换成符合JVM的规范的字节码加载到内存,是否可以运行,则由执行引擎来决定。

类加载器的角色:

让宝贝舍友理解JVM 第二弹 上(类加载子系统)

类的加载过程:

让宝贝舍友理解JVM 第二弹 上(类加载子系统)

加载过程:

通过一个类的全限定名获取定义此类的二进制字节流

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

在内存中生成一个代表这个类的java. lang.Class对象,作为方法区这个类的各种数据的访问入口

加载.class文件的方式:

  • 从本地系统中直接加载
  • 通过网络获取,典型场景: web Applet
  • 从zip压缩包中读取,成为日后jar、war格式的基础
  • 运行时计算生成,使用最多的是:动态代理技术
  • 由其他文件生成,典型场景: JSP应用
  • 从专有数据库中提取. class文件,比较少见
  • 从加密文件中获取,典型的防Class文件被反编译的保护措施

链接(Linking)阶段过程:

让宝贝舍友理解JVM 第二弹 上(类加载子系统)

链接(Linking)的验证(Verify)阶段:

目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。(虚拟机要求:例如字节码文件以cafebabe开头等)

主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。(验证出错会报VerifyError错误)

链接(Linking)的准备(Prepare)阶段:

为类变量分配内存并且设置该类变量的默认初始值。

让宝贝舍友理解JVM 第二弹 上(类加载子系统)
在prepare将number默认赋值为0。

在随后的初始化阶段才将静态代码块的20赋值给number,再将后面的静态常量 10 覆盖掉 20,输出的结果number就是10。

这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;

这里不会为实例变量分配初始化,类变量会在方法区中,而实例变量是会随着对象一起分配到Java堆中。

链接(Linking)的解析(Resolve)阶段:

将常量池内的符号引用转换为直接引用的过程。

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

符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_ Class_ info、 CONSTANT_Fieldref_ info、 CONSTANT Methodref_ info等。

初始化(Initialization)阶段过程:

此方法不需要定义,是javac编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并而来
让宝贝舍友理解JVM 第二弹 上(类加载子系统)
可以看到 < Clinit > 是自动生成的。
让宝贝舍友理解JVM 第二弹 上(类加载子系统)
可以看到下图中,我们是可以输出num,可是输出number的时候就会报错,这就是一个非法的前向引用。虽然前面已经定义了number = 20,可是他的初始化阶段还没有完成,所以这是一个非法引用。
让宝贝舍友理解JVM 第二弹 上(类加载子系统)

如果没有类变量(静态变量)的赋值动作或者是静态代码块语句那么就不会生成这个clinit方法了。

如下图中没有静态的代码,那么他的字节码文件也就不会有clinit的这个方法。
让宝贝舍友理解JVM 第二弹 上(类加载子系统)
让宝贝舍友理解JVM 第二弹 上(类加载子系统)
当加上了静态代码之后,我们可以看见他的字节码文件又生成了clinit这个方法:
让宝贝舍友理解JVM 第二弹 上(类加载子系统)
让宝贝舍友理解JVM 第二弹 上(类加载子系统)
有同学可能发现,无论有没有静态代码,都会出现一个init的方法。那么init这个方法是什么呢?

< init > 这个就是默认的构造方法。

任何一个类声明以后,内部至少存在有一个构造器。对应过来就是这里的init方法。

若该类具有父类,JVM会保证子类的< clinit >()执行前,父类的< clinit >()已经执行完毕。

如下图:

让宝贝舍友理解JVM 第二弹 上(类加载子系统)
让宝贝舍友理解JVM 第二弹 上(类加载子系统)

构造器方法中指令按语句在源文件中出现的顺序执行。

补充一个需要注意的知识点:

类成员加载顺序是:父类的静态字段 ==== 父类静态代码块——>子类静态字段 ==== 子类静态代码块——>父类成员变量(非静态字段)——>父类非静态代码块——>父类(无参)构造器——>子类成员变量——>子类非静态代码块——>子类构造器

需要注意静态字段和静态代码块之间对类(静态)变量的初始化,是按照书写顺序的,并不是按照先字段初始化再代码块初始化。

有篇文章讲解上面知识点十分透彻,有兴趣的同学可以去看看:类成员加载顺序

虚拟机必须保证一个类的< clinit >()方法在多线程下被同步加锁。

让宝贝舍友理解JVM 第二弹 上(类加载子系统)
让宝贝舍友理解JVM 第二弹 上(类加载子系统)

上面的例子用了两个线程t1和t2,还有一个死循环的DeadThread类,
当两个线程运行完输入开始语句时,最先进去这个的线程就进去了死循环中,另外一个线程无法进去。因为这个类在被初始化进行< clinit >方法的时候是处于一个加锁的状态。

结果如下图:

让宝贝舍友理解JVM 第二弹 上(类加载子系统)
为什么要处于加锁状态呢?

< clinit >方法是给类变量(静态变量)赋值,加上这个同步锁就是为了防止多个线程都进行赋值操作。
—————————————————————————————

(:写这么长估计我舍友看到一半就睡着了。所以加载阶段中类加载器的分类以及双亲委派机制等内容我们留到 让宝贝舍友理解JVM 第二弹 下(类加载子系统)去讲解。

整理不易,点个赞不过分吧,球球你们了,这对我真的很重要。
------如有错误,敬请指出。

让宝贝舍友理解JVM 第二弹 上(类加载子系统)
————————————————————————————
参考:
https://www.bilibili.com/video/BV1PJ411n7xZ?
https://blog.****.net/ym15229994318ym/article/details/106223775