什么叫类加载
JVM 将Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
类加载的生命周期
- 1、加载
- 2、验证
- 3、准备
- 4、解析
- 5、初始化
- 6、使用
- 7、卸载
其中2、3、4统称连接阶段
类的初始化
只有在以下5种场景必须立刻对类进行初始化
- 遇到new、getstatic、putstatic或者invokestatic这四条指令时,如果类没进行初始化,则先触发其初始化。具体的场景:通过new实例化对象时、或者读取或者配置一个类的静态变量(final关键字修饰的除外,此时会在编译期将结果放入常量池)是,以及调用类的静态方法。
- 使用java.lang.reflect包的方法对类进行反射调用时
- 当调用一个类,发现其父类还没初始化,应触发其父类先初始化
- 当虚拟机初始化时,用户需要指定一个启动类(包含main()方法的那个类),虚拟机会先初始化这个类
- 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没初始化化,则需要先触发其初始化
以上5中场景称为对一个类的主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用
- 父类定义类方法和类变量,子类引用父类的类方法或者类变量,此时,只会触发父类初始化而不子类初始化;
- 类中引用数组,数组会初始化,由newarray指令触发一个虚拟机直接生成的的类[Lorg.fenixsoft.classloading.SuperClass触发初始化但此类不会触发初始化。
- 类引用某个类的final常量时,不是触发初始化
一个接口初始化时,并不要求它的父接口全部都初始化,只有在真正使用时才初始化
类加载的过程
加载
加载需要完成3件事情
- 通过类全限定名获取类的二进制字节流
- 根据二进制字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表此类的java.lang.class对象,作为方法区这个类所有数据的访问入口
“获取类的二进制字节流”,获取的来源非常灵活,可以来自、
- 读取jar、war包
- 从网络中来,如:applet
- 运行时生成,如:Java的反射机制
- 由其他文件生成,如:jsp
- 还有其他方式
数组类不是有类加载创建,它是有虚拟机直接创建,但是数据元素的类型由类加载创建
- 如果数组的元素类型为引用类型,数组将会在加载该元素类型的类加载器的类命名空间上被标识
- 如果数组的元素类型为基本类型,Java虚拟机会把数组标识为与引导类加载器关联
- 数组类的可见性与元素类型一致,如果元素类型不是引用类型,则默认为public
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。通过在内存中实例化一个java.lang.class对象访问类的数据
验证
验证是确保二进制字节流的信息符合当前虚拟机的要求,并且不会危害虚拟机的安全
文件格式验证
保证输入的字节流能正确地解析并存储于方法区内,格式上符合描述一个Java类型信息的要求,包括以下验证点
- 是否以魔数开头
- 主、次版本是否在当前虚拟机处理的范围内
- 常量池的常量是否有不被支持的常量
等等
元数据验证(除方法体)
保证不存在不符合Java语言规范的元数据信息,包括以下验证点
- 这个类是否有父类
- 如果不是抽象类,是否已经实现父类或接口中的所有方法
- 是否继承了不允许被继承的类(final修饰的类)
等等
字节码验证(方法体)
对类的方法体进行校验,包括以下验证点:
- 保证操作数栈的数据类型和指令代码一致
- 保证跳转指令代码不会跳转到方法体之外
- 保证方法体中类型转换是有效的,如:可以把一个子类对象赋给父类,反过来则不行。
符号引用验证
对类以外的信息进行匹配性校验,比如
- 符号引用中根据全限定名是否找到对应类
- 符号引用的类、字段、方法的访问性(protected、public等)是否可被当前类访问
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的字段和方法
对虚拟机的类加载机制来说,验证是一个非常重要的、但不一定不要的阶段。如果所运行的代码已经被反复验证,可以通过-Xverify:none参数关闭大部分的类验证措施,以缩短虚拟机类加载的时间
准备
准备阶段是为类变量(static)分配内存并设置初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。如
public static int a = 100;
此时,会为a分配内存,并赋值为0
如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量的value就会被初始化为ConstantValue属性所指定的值,如
public static final int a = 100;
此时,会为a分配内存,并赋值为100
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用:符号引用是用以一组符号来描述所引用的目标,与内存布局无关,引用目标并不一定已经加载到内存中
- 直接引用:可以是直接指向目标的指针、偏移量或者一个能间接定位到目标的句柄
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符(动态调用),分别对应CONSTANT_Class_info、CONSTANT_Fieldref_info等7中常量类型,此次不包括实例方法的解析。
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
类初始化时类加载的最后一步,除了在加载阶段用户可以自定义类加载器之外,其余动作都是完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。
在初始化阶段,会根据程序员通过程序制定的主观计划去初始化类变量和其他资源。初始化阶段是执行类构造器<clinit>()方法的过程
-
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,收集的顺序由语句在源文件中出现的顺序所决定。静态语句中只能访问定义在它之前的变量,定义在它之后的变量可以在静态语句中赋值,但不能访问。
public class Test{
static{
i = 0; //赋值可以编译过
System.out.print(i); //引用报错
}
static int i = 2;
}
- <clinit>()方法与类构造函数(init())不同,它不需要显式地调用父类的构造器,虚拟机保证在子类的<clinit>()方法执行前,父类的<clinit>方法已经执行完毕。在虚拟机中第一个被执行<clinit>的类肯定是java.lang.Object
- 由于父类的<clinit>方法先执行,也就是父类的静态语句要先由于子类的变量赋值
- <clinit>()方法对类和接口来说不是必需的,如果类中没有静态语句或类变量,编译器不会为这个类生产<clinit>方法
- 接口中不能使用静态语句,但仍然会有类变量赋值,因此接口也会生成<clinit>()方法。但与类不同的是,执行接口的<clinit>()方法不用先执行父接口中的<clinit>()方法。只有父类中的变量需要使用时,才会初始化父接口。另外,实现类在出示化时也不会执行接口的<clinit>()方法
- 虚拟机会保证一个类的<clinit>()方法在多线程情况下的线程安全,如果当前线程在执行类的<clinit>()方法,虚拟机会阻塞另外线程对<clinit>()的执行,直到当前线程释放锁。同一个类加载器下,一个类型只会初始化一次。
类加载器
什么是类加载
虚拟机的设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部实现,以便应用程序可以自己决定如何去获取所需要的类。这个动作的实现代码模块称为“类加载器”。
类与类加载器
对应任意一个类,都需要由加载它的类加载器和这个类本身一同确定它在虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。也就是说,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。
双亲委派模型
从Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器,这个类加载器使用C++实现,是虚拟机自身的一部分;另外一种就是其他的类加载器,这个类加载器都是有Java语言实现,独立于虚拟机外部,并都继承自抽象类java.lang.ClassLoader。
- 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将%JAVA_HOME%\lib目录下的,或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。用户在编写自定义类加载器时,如果需要将加载请求委派给启动类加载器,那直接使用null代替即可。
- 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所有一般称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义的过自己定义的类加载器,一般情况下这个就是程序中默认的类加载器
双亲委派模型含义和工作原理
启动类加载器<--扩展类加载器<--应用程序类加载器<--自定义类加载器(1……n)
除了顶层类加载器(启动类加载器)外,其余类加载器都有自己的父类加载器。这种父子关系并不是通过继承来实现,而是通过组合关系来复用父类加载器的代码。
双亲委派模型的工作原理:如果一个类加载器收到了类加载的请求,它首先不会去尝试加载这个类,而是将类加载的请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求都应该传送到最顶层的类加载器中,只用当父类加载器反馈自己无法加载请求(路径中不存在该类)时,子加载器才是自己去加载。
双亲委派模型的优点:提供一种具有优先级别的层次关系(比如,基础类可以放在父类加载器中),保证Java程序运行的稳定。
破坏双亲委派模型
双亲委派模型是Java设计者推荐给开发者的一种类加载器的实现方式,大部分的类加载器都遵循这个模型,但也有一个列外情况的出现:
- 在双亲委派模式出现之前,即JDK1.2发布之前。
- 双亲委派模式自有的缺陷问题:基础类调用用户代码,通过线程上下文类加载器(Thread Context ClassLoader)解决。线程上下文类加载器可以通过java.lang.Thread类的setContextClassLoader()方法设置,如果创建线程时还未设置,它将会从父进程中继承一个,如果在应用程序的全局范围内都没有设置的话,那这个类加载器默认就是应用程序类加载器。通过线程上下文类加载器,JNDI服务可以加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作。Java中所有涉及SPI的加载动作基本都采用这种方式,例如:JNDI、JDBC、JCE、JAXB和JBI等
- 用户对程序动态性的追求:热部署等等