
类加载时机
虚拟机规范中没有规定类加载的时机,但是规定了需要进行初始化的5种情况(而加载、验证、准备必须在此之前)。
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,也就是new一个对象或者读写一个类的字段或者调用一个类的方法。
- 通过反射调用一个类的时候。
- 初始化一个类是,如果父类还没有初始化,那么先初始化父类。
- 虚拟机启动时,指定的含main()的主类。
- JDK1.7的动态语言支持,MethodHandle是一个类字段或者方法的句柄,如果这个类没有初始化,那么初始化。
不会初始化的情况:
- 子类引用父类的静态字段和方法,子类不会初始化。
- 通过数组定义引用类,类不会初始化。
- 引用可以在编译期确定的常量类字段,不会引起类初始化,因为这个常量已经被使用的类放入自身的常量池。
- 一个接口初始化时不要求父接口完成初始化,只有在真正使用父接口时才需要初始化。
类加载的过程
指生命周期中的加载、验证、准备、解析和初始化
加载
- 虚拟机根据类的全限定名获取类的二进制字节流。
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构。
- 内存中生成一个java.lang.Class对象,作为方法区这个类的数据的入口。
获取:
- ZIP(jar, war, ear...)
- 网络(applet)
- 运行时生成,Proxy动态代理
- 其他文件生成,JSP文件生成
- 数据库
加载类的阶段可以通过自定义类加载器来自定义,通过重写loadClass()方法来控制获取字节流。
但是数组类稍有特殊,例如数组类C:
(1)如果组件类型为引用类型,那么递归地加载这个组件类型,然后数组C将在加载了该组件类型的类加载器的类命名空间上被标识。
(2)如果组件类型不是引用类型,那么数组C会被标记为与引导类加载器关联
(3)数组类的可见性和组件类型一致
存储:
(1)二进制流以虚拟机所需格式存储在方法区,格式由虚拟机实现自定。
(2)实例化一个java.lang.Class类的对象,HotSpot将这个对象放在了方法区。
验证
验证字节流的信息符合虚拟机要求,并且不会危害虚拟机的安全。
字节码层面来说,字节码可以完成很多Java语言无法完成的操作,而字节码可以使用很多方式生成,不一定由Java源码编译而来,所以虚拟机为了保证自身安全,需要进行验证。
- 文件格式验证
- 魔数
- 主次版本号是否支持
- 常量池中的常量类型
- 指向常量的索引值指向是否有问题
- 常量池UTF8类型常量的UTF8编码
- Class文件是否有被删除或者附加的其他信息
......
这一阶段的验证是基于字节流的,通过后,字节流才会进入方法区,后面的验证会基于方法区内的存储结构,而不再操作字节流。
- 元数据验证
进行语义分析,以保证满足Java语言规范
- 除了Object外应该有父类
- 是否继承了final类
- 如果不是抽象类,是否实现了所有需要实现的方法。
- 字段与方法是否与父类矛盾(覆盖了父类final字段,不符合规则的重载)
......
- 字节码验证
通过数据流与控制流分析,确定程序语义是合法符合逻辑的。
- 保证操作数栈的数据类型与字节码指令能配合
- 跳转指令不会跳转到方法体之外的字节码指令
- 类型转换是有效的
......
方法体的Code属性的属性表中在JDK1.6后添加了StackMapTable,用于描述方法的所有基本块开始时本地变量表和操作栈应有的状态,验证期间,不需要在根据程序推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可,这样将类型推导转变为类型检查。
- 符号引用验证
这个验证发生在解析阶段虚拟机将符号引用转化为直接引用的时候,对类自身以外的信息进行匹配性校验:
- 字符串描述的全限定名能否找到对应的类
- 指定类中是否存在符合方法描述符所描述的方法,是否存在简单名称所描述的字段
- 符号引用中的类、字段、方法的访问性是否可被当前类访问
准备
为类变量在方法区中分配内存,并设零值(零值而非初始值),赋初始值的指令在<clinit>()方法中,在初始化阶段执行。
但如果是final变量,字段属性表中包括ConstantValue的属性,那么会设为ConstantValue的值。
解析
将常量池中的符号引用替换为直接引用,包括常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
详见《深入理解虚拟机》P221
初始化
执行<clinit>()方法
- <clinit>()方法内容来自于类变量的复制动作和static块,按声明顺序
- <clinit>()方法与构造函数不同,不需要显示调用父类构造器,虚拟机保证父类的<clinit>()方法已经执行
- <clinit>()不是必须的
- 接口没有初始化块,但是可以有赋值
- 虚拟机保证一个类的<clinit>()方法在多线程环境下能被正确地加锁同步,如果多线程同时去初始化一个类,那么只有一个线程会执行类的<clinit>()方法,其他线程会阻塞。同一个类加载器下,一个类只会初始化一次。
类加载器
主要是加载阶段的“通过一个类的全限定名获取此类的二进制字节流”
用于类层次划分,OSGi,热部署,代码加密等领域
类与类加载器
任意一个类都要由加载它的类加载器和类本身一同确立它在Java虚拟机中的唯一性,也就是每个类加载器都有自己独立的类名称空间。
双亲委派模型
- 启动类加载器(Bootstrap ClassLoader):
<JAVA_HOME>\lib和-Xbootclasspath - 扩展类加载器(Extension ClassLoader):
<JAVA_HOME>\lib\ext或者java.ext.dirs系统变量指定的类库 - 应用程序加载器(Application ClassLoader):
ClassPath - 自定义类加载器:
父类加载器为应用程序加载器,通过组合实现
好处:
Java类随着类加载器有了层级关系
摘抄自《深入理解Java虚拟机》
网友评论