正文前先来一波福利推荐:
福利一:
百万年薪架构师视频,该视频可以学到很多东西,是本人花钱买的VIP课程,学习消化了一年,为了支持一下女朋友公众号也方便大家学习,共享给大家。
福利二:
毕业答辩以及工作上各种答辩,平时积累了不少精品PPT,现在共享给大家,大大小小加起来有几千套,总有适合你的一款,很多是网上是下载不到。
获取方式:
微信关注 精品3分钟 ,id为 jingpin3mins,关注后回复 百万年薪架构师 ,精品收藏PPT 获取云盘链接,谢谢大家支持!
-----------------------正文开始---------------------------
类的加载过程:
1、 JVM会先去方法区中找有没有相应类的.class存在。如果有,就直接使用;如果没有,则把相关类的.class加载到方法区
2、 在.class加载到方法区时,会分为两部分加载:先加载非静态内容,再加载静态内容
3、 加载非静态内容:把.class中的所有非静态内容加载到方法区下的非静态区域内
4、 加载静态内容:
4.1、把.class中的所有静态内容加载到方法区下的静态区域内
4.2、静态内容加载完成之后,对所有的静态变量进行默认初始化
4.3、所有的静态变量默认初始化完成之后,再进行显式初始化
4.4、当静态区域下的所有静态变量显式初始化完后,执行静态代码块
5,当静态区域下的静态代码块,执行完之后,整个类的加载就完成了。
6,如果存在继承关系,则父类先加载,再加载子类。
实例的初始化过程:
1、在堆内存中开辟一块空间
2、 给开辟空间分配一个地址
3、 把对象的所有非静态成员加载到所开辟的空间下
4、 所有的非静态成员加载完成之后,对所有非静态成员变量进行默认初始化
5、 所有非静态成员变量默认初始化完成之后,调用构造函数
6, 在构造函数入栈执行时,分为两部分:先执行构造函数中的隐式三步,再执行构造函数中书写的代码
6.1、隐式三步:
1,执行super语句
2,对开辟空间下的所有非静态成员变量进行显式初始化
3,执行构造代码块
6.2、在隐式三步执行完之后,执行构造函数中书写的代码
7,在整个构造函数执行完并弹栈后,把空间分配的地址赋值给一个引用对象【注意是最后把地址值赋给栈中引用】
类的验证过程:
验证部分主要包含下面四个阶段的验证:
1. 文件格式的验证:
验证文件格式是否按照虚拟机的规范,也就是我们前面class文件结构中的内容,比如这是不是一个Class文件(看魔数,是否位CAFEBABE);
Java版本是否符合当前虚拟机的范围(Java可以向下兼容,但是不能处理大于当前版本的程序)等等。
2. 元数据的验证:
对Class文件中的元数据进行验证,是否存在不符合Java语义的元数据信息。这里有的朋友可能会比较疑惑,什么是元数据呢?一般情况下,一个文件中都数据和元数据。数据指的是实际数据,而元数据(Metadata)是用来描述 数据的数据。用过Java注解的朋友应该对元数据这种叫法并不陌生,对应的元注解,其实说的差不多都是一个意思。
举个例子:比如说我们定义了一个变量 int a = 1;可以理解成数据就是1,而元数据就是描述有一个字符串变量“a”,这个“a”的类型是int型的,它的值也是一个int型的1,这就是描述数据的数据,就是元数据。
3. 字节码的验证:
通过数据流和控制流分析,来确定程序语义是否合法。
以数据来说,要保证类型转换是有效的;对于控制流程的代码,不能让指令跳转到其它方法的字节码指令上等……
4. 符号引用的验证:
为了保证解析动作能正常完成,还需在虚拟机将符号引用转成直接引用的时候,判断其它要引用的类是否符合规定。比如,要引用的类是否能够被找到;引用的属性在对应类中是否存在,权限是否符合要求(private的是不能访问 的)等。
最后再说一点,验证阶段不是必须的,如果代码运行已经稳定了之后,可以通过设置参数-Xverfy:none参数来关闭类验证,减少虚拟机的类加载时间,提高运行效率。
类的解析过程:
解析阶段是将常量池中的符号引用替换为直接引用的过程。在进行解析之前需要对符号引用进行解析,不同虚拟机实现可以根据需要判断到底是在类被加载器加载的时候对常量池的符号引用进行解析(也就是初始化之前),还是等到一个符号引用被使用之前进行解析(也就是在初始化之后)。
到现在我们已经明白解析阶段的时机,那么还有一个问题是:如果一个符号引用进行多次解析请求,虚拟机中除了invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录引用,并把常量标识为一解析状态),这样就避免了一个符号引用的多次解析。
解析动作主要针对的是类或者接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用。这里主要说明前四种的解析过程。
类或者接口解析
要把一个类或者接口的符号引用解析为直接引用,需要以下三个步骤:
1. 如果该符号引用不是一个数组类型,那么虚拟机将会把该符号代表的全限定名称传递给类加载器去加载这个类。这个过程由于涉及验证过程所以可能会触发其他相关类的加载
2. 如果该符号引用是一个数组类型,并且该数组的元素类型是对象。我们知道符号引用是存在方法区的常量池中的,该符号引用的描述符会类似”[java/lang/Integer”的形式,将会按照上面的规则进行加载数组元素类型,如果描述符如前面假设的形式,需要加载的元素类型就是java.lang.Integer ,接着由虚拟机将会生成一个代表此数组对象的直接引用
3. 如果上面的步骤都没有出现异常,那么该符号引用已经在虚拟机中产生了一个直接引用,但是在解析完成之前需要对符号引用进行验证,主要是确认当前调用这个符号引用的类是否具有访问权限,如果没有访问权限将抛出java.lang.IllegalAccess异常
字段解析
对字段的解析需要首先对其所属的类进行解析,因为字段是属于类的,只有在正确解析得到其类的正确的直接引用才能继续对字段的解析。对字段的解析主要包括以下几个步骤:
1. 如果该字段符号引用就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,解析结束
2. 否则,如果在该符号的类实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果在接口中包含了简单名称和字段描述符都与目标相匹配的字段,那么久直接返回这个字段的直接引用,解析结束
3. 否则,如果该符号所在的类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都相匹配的字段,那么直接返回这个字段的直接引用,解析结束
4. 否则,解析失败,抛出java.lang.NoSuchFieldError异常
如果最终返回了这个字段的直接引用,就进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常
类方法解析
进行类方法的解析仍然需要先解析此类方法的类,在正确解析之后需要进行如下的步骤:
1. 类方法和接口方法的符号引用是分开的,所以如果在类方法表中发现class_index(类中方法的符号引用)的索引是一个接口,那么会抛出java.lang.IncompatibleClassChangeError的异常
2. 如果class_index的索引确实是一个类,那么在该类中查找是否有简单名称和描述符都与目标字段相匹配的方法,如果有的话就返回这个方法的直接引用,查找结束
3. 否则,在该类的父类中递归查找是否具有简单名称和描述符都与目标字段相匹配的字段,如果有,则直接返回这个字段的直接引用,查找结束
4. 否则,在这个类的接口以及它的父接口中递归查找,如果找到的话就说明这个方法是一个抽象类,查找结束,返回java.lang.AbstractMethodError异常
5. 否则,查找失败,抛出java.lang.NoSuchMethodError异常
如果最终返回了直接引用,还需要对该符号引用进行权限验证,如果没有访问权限,就抛出java.lang.IllegalAccessError异常
接口方法解析
同类方法解析一样,也需要先解析出该方法的类或者接口的符号引用,如果解析成功,就进行下面的解析工作:
1. 如果在接口方法表中发现class_index的索引是一个类而不是一个接口,那么也会抛出java.lang.IncompatibleClassChangeError的异常
2. 否则,在该接口方法的所属的接口中查找是否具有简单名称和描述符都与目标字段相匹配的方法,如果有的话就直接返回这个方法的直接引用。
3. 否则,在该接口以及其父接口中查找,直到Object类,如果找到则直接返回这个方法的直接引用
4. 否则,查找失败
接口的所有方法都是public,所以不存在访问权限问题
*************************************************************
类卸载过程分析:
类的生命周期
当Sample类被加载、连接和初始化后,它的生命周期就开始了。
当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。
由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
引用关系
加载器和Class对象:双向引用
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。
另一方面,一个Class对象总是会引用它的类加载器。调用Class对象的getClassLoader()方法,就能获得它的类加载器。
由此可见,Class实例和加载它的加载器之间为双向关联关系。
类、类的Class对象、类的实例对象:单向引用
一个类的实例总是引用代表这个类的Class对象。
在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。
此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。
类的卸载
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。
前面介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。
Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。
具体例子
oader1变量和obj变量间接应用代表Sample类的Class对象,而objClass变量则直接引用它。
如果程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。
当再次有需要时,会检查Sample类的Class对象是否存在,如果存在会直接使用,不再重新加载;如果不存在Sample类会被重新加载,在Java虚拟机的堆区会生成一个新的代表Sample类的Class实例(可以通过哈希码查看是否是同一个实例)。
【总结】
(1) 启动类加载器加载的类型在整个运行期间是不可能被卸载的(jvm和jls规范);
(2) 被系统类加载器和标准扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者标准扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到unreachable的可能性极小。(当然,在虚拟机快退出的时候可以,因为不管ClassLoader实例或者Class(java.lang.Class)实例也都是在堆中存在,同样遵循垃圾收集的规则);
(3) 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到.可以预想,稍微复杂点的应用场景中(尤其很多时候,用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)
综合以上三点, 一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下来实现系统中的特定功能。