JVM类加载

1、JVM是什么?

在这里插入图片描述

2、JDK,JRE,JVM关系

在这里插入图片描述
JDK包含JRE以及相关的工具和工具API
JRE包含JVM
在这里插入图片描述

3、JVM该如何学习

在这里插入图片描述
(1)java源代码到类文件
(2)类文件到JVM
(3)JVM各种对类文件进行处理【内部结构,执行方式,垃圾回收,本地调用】

4、源码到类文件

4.1、前期编译

在这里插入图片描述
java源文件 -> 词法分析器 -> token流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> Class文件

4.2、类文件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3 类文件到虚拟机(类加载机制)

类加载机制是指我们将类的字节码文件所包含的数据读入内存,同时我们会生成数据的访问入口的一种特殊机制,那么我们可以得知,类加载的最终产品是数据访问入口
在这里插入图片描述
这个时候,看到这张图,我们应该有一个问题,那就是我们的字节码加载的方式,也就是我们的字节码文件可以用什么方式进行加载呢?

4.3.1 加载.class文件方式

  1. 从本地系统中直接加载
  2. 通过网络下载.class文件

典型场景:Web Applet,也就是我们的小程序应用

  1. 从zip,jar等归档文件中加载.class文件

典型场景:后续演变为jar,war格式

  1. 从专有数据库中提取.class文件

典型场景:JSP应用从专有数据库中提取.class文件,较为少见

  1. 将java源文件动态编译为.class文件

典型场景:动态代理技术

  1. 从加密文件中获取

典型场景:典型的防Class文件被反编译的保护措施

4.3.2 类加载机制流程

在这里插入图片描述
所谓类加载机制就是

虚拟机把class文件加载到内存
并对数据进行校验,转换解析和初始化
形成可以虚拟机直接使用的java类型,即Java.lang.Class

4.3.2.1 装载

查找和导入class文件

1. 通过一个类的全限定名获取定义此类的二进制字节流
2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法去中这些数据的访问入口
获取类的二进制字节流的阶段是我们java程序员最关注的阶段,也是操作性最强的一个阶段,因为这个阶段我们可以在我们的装载阶段完成之后,这个时候在我们的内存当中,我们的运行时数据区的方法去以及堆就可以有数据了

  • 方法区:类信息,静态变量,常量
  • 堆:代表被加载类的java.lang.Class对象
    即时编译之后的热点代码并不在这个阶段进入方法区
4.3.2.2 链接
  • 验证

验证主要是为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要求我们的信息不会危害虚拟机自身的安全,导致虚拟机崩溃

  1. 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储与方法去之内,这个阶段的验证是基于二进制字节流进行的,只有经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行的

  1. 元数据验证

对类的元数据信息进行语义校验,其实就是对java语法校验,保证不存在不符合java于法规定的元数据信息

  1. 字节码验证

进行数据流和控制流分析,确定程序语义是合法的,符合逻辑的,对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为,获取类的二进制字节流的阶段是我们java程序员最关注的阶段,也是操作性最强的阶段,因为这个阶段我们可以对于我们的类加载器进行操作,比如我们想自定义类加载器进行操作用以完成加载,又或者我们想通过java Agent来完成我们的字节码增强操作

  1. 符号引用验证

这是最后一个阶段的验证,它发生在虚拟机将符号引用转换为直接引用的时候(解析阶段),可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验,符号引用验证的目的是确保解析动作能正常执行

注意:我们很多情况下可能认为我们的代码肯定没有问题,验证的过程完全没有必要,那么其实我们可以添加参数
-Xverify:none 取消验证

  • 准备

为类的静态变量分配内存,并将其初始化为默认值
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中的

进行分配内存的只是包括类变量(静态变量),而不包括实例变量,实例变量实在对象实例化是随着对象一起分配在java堆中的,通常情况下,初始化为默认值,假设public static int a = 1; 那么a在准备阶段之后的初始值0,不为1,这时候只是开辟了内存空间,并没有运行java代码,a赋值为1的指令是程序被编译后,存放于类构造器()方法中,所以a赋值为1是初始化阶段才会执行

思考:ConstantValue属性到底是干什么的呢?

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性,非static类型的变量的赋值是在实力构造器中进行的;static类型变量赋值分两种,在类构造其中赋值,或者使用ConstantValue属性赋值

思考:在实际的程序中,我们为什么才会用到ContstanValue属性呢?

在实际的程序中,只有同时被final和static修饰的字段才有ConstantValue属性,且限于基本类型和String,编译时javac将会为该厂里生成ConstantValue属性,在类加载的准备阶段虚拟机便会根据ConstantValue的常量设置相应的值,如果该变量没有被final修饰,或者并且基本类型及字符串吗,则选择在类构造器中进行初始化

思考:为什么ConstantValue的属性值只限于基本类型和String

因为从常量池中只能引用到基本类型和String类型的字面量
举个例子:假设上面的类变量a被定义为:private static final int a = 1;
编译时javac将会为a生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为1,我们可以理解static final常量在编译期就将其结果放入了调用它的类的常量池中

  • 解析
    把类中的符号引用转换为直接引用

符号引用就是一组符号来描述目标,可以是任何字面量
直接引用是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,
解析动作主要是针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用限定符7类符号引用进行

直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那么引用的目标必定存在内存中

对解析结果进行缓存
同一符号引用进行多次解析请求是很常见的,出invokedynamic指令以外,虚拟机实现可以堆第一次解析结果进行缓存,来避免解析动作重复进行,无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个引用符号之前已经被成功解析过,那么后续的引用解析请求就应当一直成功,同样的,如果第一次解析失败,那么其他指令对这个符号的解析请求也应该收到相同的异常
inDy是java7引入的一条新的虚拟机指令,这是自1.0以来第一次引入新的虚拟机执行,到了java8这条指令第一次在java应用,用在lambda表达式中,indy与其他invoke指令不同的是他允许由应用级的代码来决定方法解析

4.3.2.3 初始化

初始化阶段是执行类构造器()方法的过程
或者讲的通俗易懂,在准备阶段,类变量已经父之过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,比如赋值

在java中对类变量进行初始值设定有两种方式

  • 声明类变量时指定初始值
  • 使用静态代码块为类变量指定初始值

按照程序员的逻辑,你必须把静态变量定义在静态代码块的前面,因为两个的执行是会根据代码编写的顺序来决定的,顺序搞错了可能会影响你的业务代码

jvm初始化步骤

  • 假如这个类还没有被加载和链接,则程序先加载并链接该类
  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句
4.3.2.4 使用

那么这个时候我们去思考一个问题,我们的初始化过程什么时候会被触发呢? 或者换句话来说类初始化时机是什么呢?

  1. 主动引用

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用由有六种:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射
  • 初始化某个类的子类,则其父类也会被初始化
  • java虚拟机启动时会标明为启动类的类,直接使用java.exe命令来运行某个主类
  1. 被动引用
  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化
  • 定义类数组,不会引起类的初始化
  • 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会因为该类的初始化的)
4.3.2.5 卸载

在类使用完之后,如果满足下面的情况,类就会被卸载

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang,Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法

java虚拟机本身会使用引用这些类加载器,而这些类加载器则会使用引用他们所加载的类的Class对象,因为这些Class对象始终是可触及的

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了,但是一般情况下启动类加载器加载的类不会被卸载,而我们的其他两种基础类型的类加载器只有在极少数情况下才会被卸载

4.3.3 类加载器ClassLoader

在装载阶段,其中通过类的全限定名获取其定义的二进制字节流,需要借助类装载器完成,顾名思义,就是用来装载Class文件的

4.3.3.1 什么是类加载器?
  • 负责读取java字节代码,并转换成java.lang.Class类的一个实例的代码模块
  • 类加载器除了用于加载类外,还可用于确定类在java虚拟机中的唯一性

一个类在同一个类加载器中具有唯一性,而不同类加载器中允许同名类存在的,这里的同名是指全限定名相同,但是在整个JVM里,从然全限定名相同,若类加载器不同,则仍然不算做是同一个类,无法通过instanceOf,equals等方式的校验

4.3.3.2 分类
  • Bootstrap ClassLoader 负责加载$JAVA_HONE中jre/lib/rt.jar里面所有的class或Xbootclassoath选项指定的jar包
  • Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HONE中jre/lib/*.jar或-Djava.e
  • App ClassLoader 负责加载classpath中指定的jar包以及Djava.class.path所指定目录下的类和jar包
  • Custom ClassLoader 通过java.lang.classLoader的子类自定义加载class,属于应用程序根据自身需要自定的类加载器

在这里插入图片描述
为什么我们的类加载器要分层?
1.2版本的jvm中,只有一个类加载器,就是现在的Bootstarp类加载器,也就是根类加载器,但是这样会出现一个问题
假如用户调用它编写的java.lang.String类,理论上该类可以访问和改变java.lang包下其他类的默认访问修饰符的属性和方法的能力,也就是说,我们其他类使用String时也会调用这个类,因为只有一个类加载器,我无法判定到底加载哪个,因为java语言本身并没有阻止这种行为,所有会出现问题
这个时候,我们就想到,可不可以使用不同级别的类加载器来对我们信任级别做一个分区呢?
比如用三种基础的类加载器作为我们的三种不同的信任级别,最可信的级别是java核心API类,然后是安装的拓展类,最后才是类路径中的类
所以,我们三种基础类的加载器由此而生

JVM类加载机制的三种特性

  • 全盘负责
  • 当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器宅如

例如,系统类加载器AppClassLoader加载入口类时,会把main方法所依赖的类及引用的类也载入,以此类推,"全盘负责"机制也可以成为当前类加载器负责机制。显然,入口类所依赖的类及引用的类的当前类加载器就是入口类的加载器

以上步骤只是调用ClassLoader.loadClass(name)方法,并没有真正定义类,整整加载class字节码文件生成Class对象由双亲委派机制完成

  • 父类委托
    父类委托也叫双亲委派机制,是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查到并装载目标类

父类委托别名就叫做双亲委派机制
双亲委派机制加载Class具体的过程
1、ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象,如果没有则委托给父类加载器
2、父类加载器判断是否加载过该Class,如果已加载,则返回Class对象,如果没有则委托给祖父类加载器
3、以此类推,直到始祖类加载器
4、始祖类加载器判断是否加载过该Class,如果以加载,则返回Class对象,如果没有则尝试从其对应的类路径下寻找class字节码文件并载入,如果载入成功,则返回Class对象,如果载入失败,则委托给始祖类加载器的子类加载器
5、始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入,如果载入成功,则返回class对象,如果载入失败,则委托给始祖类加载器的孙类加载器
6、依此类推,直接源ClassLoader
7、源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入,如果载入成功,则返回Class对象,如果载入失败,源ClassLoader不会在委托其子类加载器,而是抛出异常
在这里插入图片描述

  • 缓存机制
    缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区,这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效,对于一个类加载器实例来说,相同的全名的类只加载一次,即loadClass方法不会被重复调用

而这里我们JDK8使用的时直接内存,所以我们会用到直接内存进行缓存,这也就是我们类变量为什么只会被初始化一次的由来

打破双亲委派
双亲委派这个模型并不是强制模型,而且会带来一些问题,就比如java.sql.Driver这个东西,JDK只能提供一个规范接口,而不能提供是实现,提供实现的是实际的数据库提供商,提供商的库总不能放在JDK目录中
所以java想到几种办法可以打破我们的双亲委派
SPI:比如java从1.6搞出了SPI就是为了优雅的解决这类问题------JDK提供接口,供应商提供服务,编程人员编码时面向接口编码,然后JDK能够自动找到合适的实现

java在核心类库中定义了许多接口,并且还给出了这些接口的调用逻辑,然而未给出实现,开发者要做的就是定制一个实现类,在META-INF/services中注册实现类信息,以供核心类库使用,比如JDBC中的DriverManager

OSGI: 比如我们的java程序员更加追求程序的动态性,比如代码热部署,代码热替换,也就是机器不用重启,只要不部署上就能用,OSGI实现模块化热部署的关键则是它自定义的类加载器机制实现的,每一个程序模块都有自己的类加载器,当需要更换一个程序模块时,就把程序模块连同类加载器一起换掉以实现代码的热替换

自定义类加载器
自定义类加载器的核心在于对字节码文件的获取,如果是加密的字节码则需要在该类中对文件进行解密,有几点需要注意:

  • 这里传递的文件名需要时类的全限定名,因为defifineClass方法是按照Test格式进行处理的

如果没有全限定名,那么我们需要做的事情就是将类的全路径加载进去,而我们的setRoot就是前缀地址setRoot + loadClass的路径就是文件的绝对路径

  • 最好不要重写loadClass方法,因为这样容易破坏双亲委派模式
  • 这类Test本身可以被AppClassLoader类加载,因此我们不能把Test.class放在类路径下,否则,由于双亲委派机制的存在,会导致该类由AppClassLoader加载,而不会通过我们自定义的加载器来加载
  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值