最近看到了Greys这个工具,感觉很好用,不再想用BTrace了。Greys这个小工具激发了我对于Java类加载机制还有Instrumentation的兴趣,所以想通过这个系列详细分析下。
引用这篇文章的图片:
主要是五大步,加载,链接,初始化,使用和卸载。
JVM虚拟机规范中,并没有规定类在何时被加载。只规定了在何时一定要被初始化 加载主要做三件事: - 获取 - 转换 - 生成
获取这个定义类的二进制流,可用的来源有多种: - 从zip包获取,就是常见的jar包,war包等等 - 从网络获取 - 运行时生成,这就是反射技术 - 其他文件生成,例如JSP文件 - 从数据库获取…. - ….
非数组类型的加载,就是这样。而且这个获取过程,我们可控。例如,我们可以实现自己的类加载器(即重写一个类加载器的loadClass()方法),读取不同来源的字节码,或者是将字节码加密解密读取,实现源代码的加密等等。
数组类型是由虚拟机直接创建:如果是引用类型数组,那么会先去加载这个组件类型,然后在加载该类型的类加载器的类名称空间标识这个数组。如果是原始类型,例如int[]。数组类的可见性和组件类型一致
获取之后,二进制字节流将按照虚拟机所需格式存储在方法区之中。具体如何存储在JVM虚拟机规范中并没有指明
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类各个数据的访问入口
这个加载的流程可以参考下图:
其实某些加载的流程和链接的流程是相互交叉的,但在整体时间顺序上面,还是加载先于链接执行
链接阶段主要分为三步: - 验证 - 准备 - 解析
目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。
这个验证也分为如下几步:
- 文件格式检验:这阶段会验证很多,并且这个阶段是和加载重合的一个阶段,通过这个阶段的验证后,字节流会进入方法区存储(JDK1.8以后是元数据区),之后所有的验证都是基于方法去的数据结构进行,不会再读取字节流
- 是否以魔数0xCAFEBABE开头
- 主、次版本号是否在当前虚拟机处理范围之内
- 常量池中的常量是否有不被支持的类型(检查常量tag标志)
- 指向常量的各项索引值中是否有不被支持的类型或者不存在的常量
- CONSTANT_URF8_INFO是否有非urf8编码的数据
- class文件中是否有被删除或者添加的信息
- 。。。。。。
- 元数据验证:对于每个类元数据(类型,父类子类等等)进行语义分析,以保证符合JAVA语言规范的要求。
- 字节码验证:对于方法体进行分析,保证校验的方法在运行时不会有危害虚拟机的行为,例如:
- 避免如下情况,操作栈放置了一个int类型的数据,使用时却按照long类型加载入本地
- 保证跳转指令不会跳转到方法以外的字节码指令上
- 保证类型转换是安全的:
- 。。。。。。
- 符号引用验证:验证全限定名是否能找到对应类,方法字段是否存在,访问性(private,protected,public,default)是否可达,在这个阶段没有通过验证,一般会抛出一个java.lang.IncompatibleClassChangeError
,如java.lang.IllegalAccessError
、java.lang.NoSuchFieldError
、java.lang.NoSuchMethodError
如果使用的jar已经反复验证过,可以通过-Xverify:none
参数来关闭大部分验证
为类变量分配内存并赋予初始值。
将常量池内部下列信息的符号引用转换为直接引用(直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄) - 类 - 接口 - 字段 - 方法 针对方法的解析,需要注意,虚拟机在这里为了优化解释器性能,增加了constantPoolCache,因此原本字节码中表示常量池项索引位置的字节也需要相应的跟着调整。这个调整过程需要对所有方法的字节码进行重写。
虚拟机规范规定,下列情况下必须触发该类的初始化: - JVM遇到需要引用类的指令时:new, getstatic, putstatic和invokestatic遇到 new 、 getstatic、putstatic或者invokestatic这四条指令,生成这四条指令常见的操作: 使用new关键字实例化对象、读取或者设置一个类的静态字段(被final修饰的常量已在编译器把结果存入了常量池的静态字段除外)的时候、调用一个类的静态方法; - 使用反射的对类进行调用的时候,如果没有初始化,需要先初始化引用类; - 当初始化一个类的时候,如果发现父类还没有进行初始化,那么需要进行父类的初始化操作; - 当jvm启动的时候,用户需要指定一个要执行的主类,jvm会先初始化该类; - 使用jdk7动态语言支持,如果一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getstatic,REF_putstatic,REF_invokestatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化 以上行为成为对一个类进行主动引用,除此之外,所有应用类的方式都不会触发类的初始化过程,称为被动调用
初始化这个过程实际上就是执行类构造器()
方法的过程,这个方法是:
在Java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。一个类变量初始化语句是变量声明后的等号和表达式:
class Example {
static int size = 3 * (int) (Math.random() * 5.0);
}
静态初始化语句是一个以static开头的语句块:
class Example{
static int size;
static {
size = 3 * (int) (Math.random() * 5.0);
}
}
所有的类变量初始化语句和类型的静态初始化语句都被Java编译器收集到了一起,放在一个特殊的方法中。这个方法就是()
同时还要注意,由于多线程加载,每个类在被加载时会上锁,保证每个类不会被同时加载或者加载多次。