深入理解JVM第七章笔记
类加载
虚拟机的类加载机制:
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型
特别的:
Java语言中,类型的加载,连接,初始化过程都是在程序运行期间完成的
类加载时机
类从被加载到虚拟机内存到卸载出内存,整个生命周期:
-
加载
-
验证
-
准备
-
解析
-
初始化
-
使用
-
卸载
其中验证,准备,解析3个部分统称为连接
图示:
图1.png类加载过程
加载
加载过程主要完成三件事情:
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
- 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口
加载阶段完成之后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构,然后在内存中实例化一个java.lang.Class类的对象
验证
该环节主要用于确保加载进来的字节流是否符合jvm规范,不危害jvm安全。如果验证错误。将抛出诸如java.lang.VerifyError或其子类异常
根据java虚拟机规范,验证按顺序有4个阶段:
- 1.文件格式验证
目的:确保输入的字节流可以正确解析并存储到方法区中,通过这个阶段验证后,后续三个阶段都是基于方法区进行验证的。
- 2.元数据验证
目的:对类的元数据语义校验,保证符合Java语言规范要求
- 3.字节码验证
目的:验证程序语义是否符合逻辑,保证字节码流可以被jvm安全执行
- 4.符号引用验证
目的:判断引用是否符合规定
值得一提的是,虽然对于jvm的类加载机制来说,验证是非常重要:可以有效防止恶意代码攻击。但不是一定必要的阶段,对运行期不影响,而且从性能来讲,验证阶段的工作量在类加载的过程中,占比较大的工作量,所以对于很有把握,反复验证过代码:包括自己写的和第三方包,可以设置-Xverify:none参数来关闭类验证,缩短类加载时间。
准备
准备阶段主要作用
-
1.在方法区内为类变量,被static修饰的变量,不包括实例变量分配内存
-
2.设置其初始值:通常情况下是数据类型的初始值,比如int是0,boolean是false,除去基本类型的reference为null
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用
一组来描述所引用的目标对象,这里的符号可以是各种形式的字面量或者说是字符串,用于无歧义的定位到目标。
- 直接引用
直接引用可以是(1)直接定位到目标的指针 ( 2 )偏移量, 指向实例变量,实例方法的直接引用是通过偏移量,通过这个偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置 ( 3 )可以直接定位到目标的句柄。
解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄,调用点限定符等7类符号引用进行。
简单来说,就是符号引用和虚拟机内存布局无关,引用的目标不一定加载到内存中,而有了直接引用,那引用的目标必定已经被加载入内存中了。符号引用是字面量,会被jvm识别进而转为可以直接使用的"直接引用",比如说一个类org.simple.People要引用另外一个类。举个例子org.simple.Food, 实际是以二进制形式的完全限定名放在class文件中,编译时并不知道实际地址,所以先用某个符号表示,接着再让jvm翻译成自己能直接引用的内存地址。
初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java代码。
在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的计划来初始化类变量和其他资源。
< clinit >()方法执行过程中一些可能会影响程序运行行为的特点和细节
- 1.< clinit>()方法是由编译器自动收集类中的变量赋值动作和静态语句块合并产生的,编译器收集顺序是源文件出现顺序决定的,后面定义的变量,在前面静态块中可以赋值但不能访问。
- 2.< clinit>()方法和类的构造函数,或者说实例构造器< init>,不同,不需要显示调用父类构造器,虚拟机会自动保证子类的< clinit>()方法执行前先执行完父类的< clinit>()方法,所以虚拟机中第一个被执行< clinit>()方法方法的类肯定是Object。
- 3.由于父类的< clinit>()方法比子类先执行,这就意味着父类的static块执行顺序优于子类。
- 4.< clinit>()对于类和接口不是必须的,如果没有静态语句块,也没有对变量赋值操作,可以不生成这个方法。
- 5.接口不能使用静态语句块,但可以赋值,虽然不是必须的但接口也可以生成< clinit>(),但跟类不同,执行接口的< clinit>()不需要先执行父接口的< clinit>()方法,只有父接口定义的变量使用时才会初始化父接口。
- 6.< clinit>()方法在多线程中会被正确的做加锁同步操作,如果多线程去初始化一个类,只有一个类去执行这个< clinit>(),其它线程都需要等待知道执行< clinit>()完毕。
类加载器
类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,而实现这个动作的代码模块叫做---“类加载器”
类与类加载器
对于任何一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
更好的理解:
比较两个类是否“相等”,只有这两个类是由同一个类加载器加载的前提下比较才有意义;否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只有加载它们的类加载器不同,那么这两个类就必然不一样。
双亲委派模型
从JVM角度,只存在两种不同的类加载器
- 一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现,是虚拟机自身的一部分
- 另一种就是所有其他的类加载器,这些加载器由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader
绝大部分Java程序都会使用到一些三种系统提供的类加载器:
- 启动类加载器(Bootstrap ClassLoader)
-
扩展类加载器(Extension ClassLoader)
-
应用程序类加载器(Application ClassLoader)
双亲委派机制:
一个类加载器查找class和resource时,是通过“委托委派”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。
参考资料
<<深入理解Java虚拟机>>
网友评论