类加载机制
类加载机制是指将类的字节码文件所包含的数据读入内存,同时会生成数据的访问入口的一种特殊机制。那么可以得知,类加载的最终产品是数据访问入口。

加载.class文件的方式:
1.从本地系统中直接加载:最常使用
2.通过网络下载class文件:Web Applet,也就是小程序应用
3.从zip,jar等归档文件中加载class文件:后续演变成jar,war格式
4.从专有数据库中提取class文件:JSP应用从专有数据库中提取class文件,极为少见
5.将java源文件动态编译为class文件,也就是运行时计算而成:动态代理技术
6.从加密文件中获取:典型的防class文件被反编译的保护措施。

1 加载(Load)
查找和导入class文件
(1) 通过一个类的全限定名获取定义此类的二进制字节流(由上可知,不一定从字节码文件中获得,还有很多方法)。那么这个时候需要一个工具来寻找获取类二进制字节流。而java中正好有这么一段代码模块,可以实现通过类全名来获取此类的二进制字节流这个动作,并且将这个动作放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类,实现这个动作的代码就是“类加载器“
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3) 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
注:获取类的二进制字节流的阶段是java程序员最关注的阶段,也是操控性最强的一个阶段。因为这个阶段可以对于类加载器进行操作,比如想自定义类加载器进行操作用以完成加载,又或者想通过java agent来完成字节码增强操作。
在装载完成之后,这时在内存中,运行时数据区的方法区以及堆就已经有数据了。即时编译之后的热点代码并不在这个阶段进入方法区。
方法区:类信息、静态常量、常量
堆:代表被加载类的java.lang.Class对象
2 链接(Link)
2.1 验证(Verify)
验证只是为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要求文件中的信息不会危害虚拟机自身的安全,导致虚拟机的奔溃。
1) 文件格式验证:验证字节流是否符合class文件格式的规范,并且能够被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确的解析并存储于方法区之内。这阶段的验证是基于二进制字节流进行的,只有经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面验证都是基于方法区的存储结构进行的。
举例:是否以16进制cafebabe开头;版本号是否正确。
2) 元数据验证:对类的元数据信息进行语义校验(其实就是对java语法校验),保证不存在不符合java语法规范的元数据信息。
举例:是否有父类;是否继承了final类;一个非抽象类是否实现了所有的抽象方法。
3) 字节码验证:进行数据流和控制流分析,确定程序语义是合法的,符合逻辑的。对类的方法体进行校验分析,保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。字节码的验证相对来说较为复杂。
举例:运行检查;栈数据类型和操作码参数吻合(比如栈空间只有4个字节,但是实际需要的远远大于4个字节,那么这时字节码就是有问题的);跳转指令指向合理的位置。
4) 符号引用验证:这时最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段),可以看作是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。符号引用验证的目的是确保解析动作能正常执行。
举例:常量池中描述类是否存在;访问的方法或者字段是否存在且具有足够的权限。
但是,很多情况下可能认为代码肯定是没有问题的,验证过程完全没必要,那么其实可以添加参数取消验证:
1 | |
2.2 准备(Prepare)
为类的静态变量分配内存,并将其初始化为默认值。
| 数据类型 | 零值 |
|---|---|
| int | 0 |
| long | 0L |
| short | (short)0 |
| char | ‘\u0000’ |
| byte | (byte)0 |
| boolean | false |
| float | 0.0f |
| double | 0.0d |
| reference | null |
这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显示初始化。这个不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
1 | |
进行分配内存的只是包括类变量(静态变量),而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在java堆中的。通常情况下,初始值为零值,假设public static int a = 1;那么a在准备阶段过后的初始值为0,不为1,这时只是开辟了内存空间,并没有运行java代码,a赋值为1的指令是程序被编译后,存放于类构造器(方法)中,所以a被赋值为1是在初始化阶段才会执行。
对于一些特殊情况,如果类字段属性表中存在ConstantValue属性,那在准备阶段变量a就会被初始化为ConstantValue属性所指的值。
1.ConstantValue属性到底是干什么的?
Constant Value属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性,非static类型的变量的赋值是在实例构造器方法中进行的;static类型变量赋值分两种,在类构造器中赋值,或使用ConstantValue属性赋值。
2.在实际的程序中,什么时候才会用到ConstantValue属性?
在实际的程序中,只有同时被final和static修饰的字段才有ConstantValue属性,且限于基本类型和String。编译时javac将会为该变量生成ConstantValue属性,在类加载的准备阶段虚拟机便根据ConstantValue为常量设置相应的值,如果该变量没有被final修饰,或者并非基本类型即字符串,则选择在类构造器中进行初始化。
3.为什么ConstantValue的属性值只限于基本类型和String?
因为常量池中只能引用到基本类型和String类型的字面量。
2.3 解析(Resolve)
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。
符号引用就是一组符号来描述目标。可以是任何字面量。引用的目标并不一定已经加载到内存中。
直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。
直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定存在内存中。
对解析结果进行缓存:
同一符号引用进行多次解析请求是很常见的,除invokedynamic指令以外,虚拟机实现可以对第一次解析结果进行缓存,来避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个引用符号之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样的,如果第一次解析失败,那么其他指令对这个符号的解析请求也应该收到相同的异常。
inDy(invokedynamic)是 java 7 引入的一条新的虚拟机指令,这是自 1.0 以来第一次引入新的虚拟机指令。到了 java 8 这条指令才第一次在 java 应用,用在 lambda 表达式中。 indy 与其他 invoke 指令不同的是它允许由应用级的代码来决定方法解析。
3 初始化(Initialize)
初始化阶段是执行类执行器方法的过程。或者讲的通俗易懂些,在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,比如赋值。
在java中对类变量进行初始值设定由两种方式:1.声明类变量时指定初始值。2.使用静态代码块为类变量指定初始值。
按照程序员的逻辑,必须把静态变量定义在静态代码块的前面。因为两个的执行是会根据代码编写的顺序来决定的,顺序搞错了可能会影响业务代码。
JVM初始化步骤:1.假如这个类还没有被加载和连接,则程序先加载并连接该类。2.假如该类的直接父类还没有被初始化,则先初始化其直接父类。3.假如类中有初始化语句,则系统一次执行这些初始化语句。
4 使用
- 主动引用:只有当对类的主动引用的时候才会导致类的初始化,类的主动使用有6种:
(1)创建类的实例,也就是new的方式。
(2)访问某个类或接口的静态变量,或者对该静态变量赋值
(3)调用类的静态方法
(4)反射,如Class.fonName(“com.carl.Test”)
(5)初始化某个类的子类,则其父类也会被初始化
(6)java虚拟机启动时被标明为启动类的类(JvmCaseApplication),直接使用java.exe命令来运行某个主类。
2. 被动引用:
(1)引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
(2)定义类数组,不会引起类的初始化。
(3)引用类的static final常量,不会引起类的初始化,如果只有static修饰,还是会引起类的初始化。
5 卸载
在类使用完之后,如果满足下面的情况,类就会被卸载:
(1)该类的所有实例都已经被回收,也就是java堆中不存在该类的任何实例
(2)加载该类的ClassLoader已经被回收。
(3)该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用他们所加载的类的Class对象,因此这些Class对象始终是可触及的。如果以上三个条件全部满足,JVM就会在方法区垃圾回收时堆类进行卸载,类的卸载过程其实就是在方法区种清空类信息,java类的整个声明周期就结束了。但是一般情况下启动类加载器加载的类不会被卸载,而其他两种基础类型的类加载器只有在极少数情况下才会被卸载。