我们都知道一个类从加载到卸载一共分为七个过程
加载 - 链接(验证 - 准备 - 解析) - 初始化 - 使用 - 卸载
下文我们将详细解析这些过程
谁需要加载?
在Java中数据类型分为基本数据类型和引用数据类型,基本数据类型由虚拟机预定义,引用数据类型则需要类的加载
1.加载/装载(loading)
装载的过程就是将Java字节码加载到机器内存中,并在内存中构建出Java类的模板class对象
简而言之,就是将二进制的数据加载进内存变成class实例
大概可以概括为三个问题
1.通过类的全名获取二进制数据流
2.解析二进制数据流为方法区数据结构/类模型
3.创建java.lang.Class实例,表示该模型,作为这个类数据的访问入口
什么是类模板?
所谓类模板,其实就是Java类在JVM内存中的一个快照,JVM在字节码文件中解析出的常量池,类字段,类方法等信息存储到一个类模板中,这样JVM在运行的时候就可以在模板中获取类的任何信息,可以进行任何操作
反射就基于这一基础,如果没有类模板,JVM在运行时也无法进行反射操作
注:类模型存在方法区中
二进制流有哪些获取方式?
对于类的二进制文件,虚拟机有很多种获取方式
1.class文件
2.jar包,zip包等
3.数据库中的类的二进制文件
4.通过网络HTTP协议进行加载
在虚拟机获取到类的二进制信息之后就会加载一个Class实例,但是如果二进制文件不满足要求,则会抛出异常
Class文件在哪?
.class文件加载到元空间之后就会在堆区创建一个Class对象,用来封装类的数据结构,这个class对象是加载类的过程中创建的,每个类都对应有class对象
这样外界就可以直接通过访问CLass对象来获取class的数据结构了
特殊:数组类的加载
数组类本身不是由类的加载器负责创建而是JVM运行的时候根据徐区域直接创建的,但是数组元素类型仍然需要依赖类的加载器去创建 ,如果这里数组的元素是引用类型,那么就需要正常递归加载元素类型
2.链接(linking)
1.验证
这里的需求就是验证加载进虚拟机的class文件是否符合java虚拟机规范,大部分要做如下检查
注:其中格式验证会在装载阶段一并执行,验证通过后才会将二进制数据加载到方法区(元空间)
说明:
1.格式验证 看开头魔数是不是0xCAFEBABE版本号是否支持等
2.语义检查,比如是否final修饰的方法或者类被重写或者继承了
3.字节码验证 比如看函数的调用是否指向了正确的类型参数,变量的赋值是否是正确的类型等
注:通过字节码验证也不能说明这个类完全没问题
4.符号引用验证 看符号引用是都能在常量池中找到对应的直接引用
2.准备阶段
这个阶段会为类的静态变量进行分配内存,这个阶段虚拟机会为类进行分配空间,并设置默认的初始值,比如int 赋值为0 long类型赋值为0L一样
注:1.Java不支持boolean类型,对于boolean类型,内部实现是int,int的初始化值为0,对应的boolean默认值就是false
2.修饰为static final的,在编译的时候就会分配,准备阶段会显示赋值
3.这里不会为实例变量进行初始化,实例变量会随着对象一起分配到堆中
4.这个阶段不会有代码被执行
3.解析阶段
简而言之就是将类,方法,接口,字段的符号引用转化为直接引用
Java虚拟机为每个类都准备了一个方法表,所有的方法都列在表里,当需要调用一个类的方法的时候,通过解析将符号引用转化为直接引用就可以得到目标方法在方法表的位置,从而调用方法.
注:解析就是将符号引用转化为直接引用,也就是得到类,字段等在内存中的引用或者是偏移量,可以说如果直接引用存在,系统中一定存在这样的类方法或者字段,符号引用则不能确定,但是虚拟机规范没有规定一定要按照顺序执行,HotSpot中解析就是在初始化之后再执行的
初始化(initiating)
这里的初始化其实就是对静态变量的值进行显示初始化了
类的初始化是类装载的最后一个阶段,到了这里才开始真正意义上执行了Java程序代码
重要方法:<clinit>() 有静态变量才有的方法
这个方法是Java编译器生成并且由JVM调用的,我们无法自定义重名方法,也不能调用这个方法
这个方法主要就是给类的static变量显示赋值和在静态代码块中赋值了
<init>方法,一定会出现在class表中的method中,涉及显示赋值,代码块和构造方法
先加载父类再加载子类?
加载一个类的时候虚拟机总是先加载他的父类,因为父类的clinit总在子类之前被调用,这也就说明了为什么父类的静态代码块要优先于子类了
哪些类没有clinit方法?
1.没有类变量和静态代码块的类
2.一个类中声明类变量但是没有使用初始化语句和静态代码块赋值操作
3.一个类中包含static final 修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
clinit会产生死锁嘛?
类的初始化调用这个方法的时候虚拟机会保证其在多线程环境下正确的被加载同步,如果多个线程去初始化同一个类,只会有一个线程去执行这个clinit方法,正是因为这个方法是线程安全的,所以如果在一个类的clinit方法中有很耗时的操作,那么就可能造成多个线程阻塞的操作,从而引发死锁
主动使用 VS 被动使用
主动使用:Class只有在首次使用的时候才会被装载,java虚拟机不会无条件的装载Class类,Java虚拟机规定,一个类或者是接口在初次使用的时候一定要初始化,这里的使用就是主动使用
主动使用(默认这里加载,验证,准备已经完成)
1.创造类的实例的时候(包括反射,克隆,反序列化)
2.调用类的静态方法的时候
3.使用接口类的静态字段的时候
4.初始化子类发现父类没初始化,就先触发父类的初始化
5.虚拟机启动时,主类main()需要初始化
等等
被动使用:不会引起类的初始化
1.访问静态字段时,只有真正声明这个字段的类会初始化
2.使用数组定义类应用不会出现初始化
3.loadClass()方法记载一个类,也不会导致初始化
被动使用不会进行clinit方法的调用
类的使用
一旦这个类经过了装载,验证,准备,解析,初始化五个阶段,这时候就可以给开发者使用了,开发人员可以在程序中调用它的静态类成员信息,或者使用new关键字创建对象实例了.
类的卸载
我们先展示一个图
这个图表示了一个对象被创建出来之后,默认指向它的引用,这里其他的引用都好去除,就是类的加载器这个引用无法去除,如果是三个系统自带的类加载器,那么无法删除这个引用,这也就说明了为啥JDK8要将永久代变成元空间使用本地内存了,因为类几乎是无法卸载的,除非使用自定义的类的加载器,这才有可能将这个引用删除,从而解决类的卸载问题
类的加载器
类的加载器是JVM执行类加载机制的前提
ClassLoader作为Java的核心组件,所有的Class都是由ClassLoader进行加载的,它负责将class的二进制文件读进JVM内部,然后转化为Class实例,这里的classloader主要是在装载的阶段起作用
显示加载: 指的是在代码中调用ClassLoader来加载class对象如直接写Class.forName(name)
隐式加载:通过JVM自动进行加载,只要class文件中引用到了另一个类的对象,就会自动加载到内存中
加载的类是唯一的吗?
什么叫唯一:由两方面决定,加载器和类本身 比较两个类是否相等,只有这两个都相同才是相等的,不然即使是同一份class文件,被同一个虚拟机加载,只要加载的类加载器不同,那么这个两个类也一定不同 所以加载的类不是唯一的
双亲委派模型
定义:如果一个类加载器在接收到加载类的请求的时候,首先不会自己尝试加载这个类,而是把这个请求任务委托给父类加载器去完成,依此递归,如果父类加载器能完成就完成,完成不了再由子类加载器来完成
本质:规定了加载顺序 引导类先加载,拓展类其次,系统类最后,再是自定义类加载器
我们再说说三个JVM自带的加载器加载哪些内容吧
引导类加载器:加载JVM需要的类
扩展类加载器:记载标准扩展的类
系统类加载器:加载path下指定的类以及上放没有包含的类
源码分析
双亲委派机制在java.lang.ClassLoader.loadClass(String boolean)接口中体现,逻辑如下
1.在加载器的缓存查找有无目标类,有就直接返回
2.看父加载器是否为空,不为空则调用父加载器的接口进行加载
3.如果父加载器为空,则调用引导类加载器进行加载
4.以上都无法加载,就调用Classloader接口中的defineClass系列的native接口加载目标Java类
注:这里不要去想重写loadclass方法来打破双亲委派机制,因为不管是什么类加载其最后都会执行predeDineClass接口,这就是堆核心JDK库的保护
双亲委派机制的优势和劣势
双亲委派机制优势
1.避免了类的重复加载
2.保护核心api的安全
缺点
检查类是否加载的委托过程是单向的,这个方式虽然架构清晰,职责明确,但是顶层的加载器就不能访问底层的加载器所加载的类了,通常情况下,启动类加载器的类称为系统核心类,包括重要的系统接口,应用类访问启动类加载器加载的类自然没问题,但是启动类加载器访问不了应用类加载器加载的类,比如系统类加载器提供一个接口在应用类得以实现,该接口绑定一个工厂方法,用于创建实例,而接口和工厂方法都在启动类加载器中,此时就会出现工厂方法无法创建启动类加载器加载的应用实例的问题
破坏双亲委派机制的举例
1.由于类加载器是在jdk1.0的时候引入的,而jdk1.2才引入双亲委派模型,设计者不得不做出妥协,为了兼容这些代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在lang.ClassLoader中增加一个新的方法findCLass,引导用户尽可能去重写这个方法,按照loadClass的逻辑,在父类加载失败的时候就会来使用这个findCLass完成加载
2.线程上下文加载器
这是因为模型本身的缺陷导致的
如果有基础类型要回调用户的代码怎么办呢
典型的就是JNDI服务,在JDK1.3引入,目的是堆资源进行查找和集中管理,但是他是由启动类加载器进行加载的,需要调用其他厂商实现的SPI(服务提供者接口),通常把核心类提供外部服务并可以由应用层实现的接口称之为SPI
这里线程上下文加载器就要出手了,这个类加载器可以通过Thread类setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器,这时候问题也就迎刃而解了.
3.代码热部署/模块热替换
这就是由用户追求程序动态性导致的
IBM公司主导的视线模块而部署的关键是自定义的类加载机制的实现,每个程序都实现一个自己的类加载器,需要更换的时候直接将加载器一起替换,这里就不遵循双亲委派机制的树状结构,而是进一步发展成网状结构
'
热替换:这里就是不停止服务,止痛膏替换程序文件来修改程序的行为,关键在于服务不能中断,修改也必须立即表现扎起正在运行的系统上
市面上大部分脚本语言是支持热替换的,但是java不是天生支持的,所以只能使用ClassLoader了
注:不同ClassLoader即使加载两个相同的类,也是会认为是不同的类型的,所以可以实现,基本思路如下图
TomCat类加载机制