以上就是我今天没有总结学习类加载器时候对类加载器仅有的知识,虽然有个大概印象,但是还是有点模糊。今天一口气总结一下,参考文献我就不列举了。本文不生产知识,只是知识的搬运工。
静态.class文件到内存实例
这个话题可以换一个说法就是类的生命周期是什么样的?这个可以分为两部分:
- .class类文件变为class对象:装载、连接和初始化
- class对象实例化最后gc:类实例化,垃圾收集
图 1 Java中类和对象的生命周期
类型装载、连接与初始化
Java虚拟机通过装载、连接和初始化一个java类型,使该类型可以被正在运行的Java程序所使用。
- 装载:把二进制形式的Java类型读入Java虚拟机中。
- 连接:把这种已经读入了虚拟机的二进制形式的类型合并到虚拟机的运行时状态中;连接阶段分为三个子步骤:
- 验证:确保Java类型数据格式正确并且适于Java虚拟机使用。
- 准备:为该类型分配它所需的内存,比如为类变量分配内存。
- 解析:负责把常量池中的符号引用转为直接引用,虚拟机的实现可以推迟解析这一步,它可以在当运行中的程序真正使用某个符号引用时再去解析它(把该符号引用转换为直接引用)。
- 初始化:为了让一个类或者接口被首次主动使用,最后一个步骤就是初始化,也就是为类变量赋予正确的值。
图2 类型声明周期的开始
就像图2中看到的那么样,装载、连接和初始化这三个阶段必须按顺序进行。唯一的例子就是连接阶段的第三步——解析,它可以在初始化之后再进行。
类或接口的主动使用
在类和接口被装在和连接的时机上,Java虚拟机规范给实现提供了一定的灵活性。但是它严格定义了初始化的时机。所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化。以下六种情形都符合主动使用的要求:
- 当创建某个类的新实例时(或通过在字节码中执行new指令;或者通过不明确的创建、反射、克隆或者反序列化)。
- 当调用某个类的静态方法时(即在字节码中执行invokestatic指令时)。
- 当使用某个类或接口的静态字段,或者对该字段赋值时(即在字节码中,执行getstatic或putstatic指令时),用final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。
- 当用Java API中的某些反射方法时,比如Class中的方法或者java.lang.reflect包中的类的方法。
- 当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)。
- 当虚拟机启动时某个被标明为启动类的类(即含有main()方法的那个类)。
除了上述六种情形外,所有其他使用Java类型的方式都是被动使用,它们都不会导致Java类型的初始化。后面会有例子说明主动使用和被动使用的区别。
上面提到过,任何一个类的初始化都要求它的超类在此之前已经初始化了。以此类推,该规则就意味着某个类的所有祖先类都必须在该类之前被初始化。然而,这对于接口来说,这条规则并不适用。只有在某个接口所声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的子接口或者类要初始化而被初始化。因而,任何一个类的初始化都要求它的所有祖先类(而不是祖先接口)预先被初始化。而一个接口的初始化,并不要求它的祖先接口预先被初始化。
"在首次主动使用时初始化"这个规则直接影响着装载、连接和初始化类的机制。在首次主动使用时,其类型必须被初始化。然而,在类型能被初始化之前,它必须已经被连接了,而在它能被连接之前,它必须应被装载了。Java虚拟机的实现可以根据需要在更早的时候装载以及连接类型,没必要一直等到该类型的首次主动使用采取装载和连接它。无论如何,如果一个类型在它的首次主动使用之前还没有被装载和连接的话,那它必须在此时被装载和连接,这样它才能被初始化。
装载
装载阶段由三个基本动作组成,要装载一个类型,Java虚拟机必须:
- 通过该类型的完全限定名,产生一个代表该类型的二进制数据流。
- 解析这个二进制数据流为方法区内的内部数据结构。
- 创建一个表示该类型java.lang.Class类的实例。
这个二进制数据流可能遵守Java class文件格式,但是也可能遵守其他的格式。就像前一章提到的那样,所有的Java虚拟机实现必须能识别Java class文件格式,但是个别的实现可以识别其他的二进制格式。
Java虚拟机规范并没有说Java类型的二进制数据流应该怎样产生,额,产生方式就不赘述了,只有InputStream就可以啊。
有了类型的二进制数据之后,Java虚拟机必须对这些数据进行足够的处理,然后它才能创建类java.lang.Class的实例对象。虚拟机必须把这些二进制数据解析为与实现相关的内部数据结构。装载步骤的最终产品就是这个Class类的实例对象,它成为Java程序与内部数据结构之间的接口。要访问该类型的信息(它们是存储在内部数据结构中的),程序就要调用该类型对应的Class实例对象的方法。这样一个过程就是把一个类型的二进制数据解析为方法区中的内部数据结构、并在堆上建立一个Class对象的过程,这被称为"创建"类型。
Java类型要么由启动类装载器装载,要么通过用户自定义的类装载器装载。启动类装载器是虚拟机实现的一部分,它以与实现无关的方式转载类型(包括Java API的类和接口),用户自定义的类装载器是类java.lang.ClassLoader的子类实例,它以定制的方式装入类。
类装载器(启动型或者或用户自定义的)并不需要一直等到某个类型"首次主动使用"时再去装入它。Java虚拟机规范允许类装载器缓存Java类型的二进制表现形式,在预料某个类型将要被使用时装载它,或者把这些类型装载到一些相关的分组里面。如果一个类装载器在预先装载时遇到问题,无论如何,它应该在该类型被首次主动使用时报告该问题(通过抛出一个LinkageError异常的子类)。换句话说,如果一个类装载器在预先装载时遇到缺失或者错误的class文件,它必须等到程序首次主动使用该类时才报告错误。如果这个类一直没有被程序主动使用,那么该类装载器将不会报告错误。
验证
当类型被装载后,就准备进行连接了。连接过程的第一步是验证——确认符合Java语言的语义,并且它不会危及虚拟机的完整性。
准备
随着Java虚拟机装载了一个类,并执行了一些它选择进行的验证之后,类就可以进入准备阶段了。在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。但在到达初始化阶段之前,类变量都没有被初始化为真正的初始值。(在准备阶段是不会执行Java代码的。)
解析
类型经过了连接的前两个阶段——验证和准备——之后,它就可以进入第三个(也就是最后一个)连接阶段了——解析。解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。在前面提到过,在符号引用被程序首次使用之前,连接的这个步骤都是可选的。
初始化
为了准备让一个类或者接口被首次主动使用,最后一个步骤就是初始化,也就是为类变量赋予正确的初始值。这里的"正确"初始值指的是程序员希望这个类变量所具备的起始值。
1. <clinit>()方法
2. 主动使用和被动使用
对象的生命周期
一旦一个类装载、连接和初始化,它就随时可以使用了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。这里会讨论类的实例化和初始化,即对象生命起始阶段的活动,还要讨论垃圾收集和终结,即对象生命活动的尽头。
类实例化
垃圾收集和对象的终结
Java虚拟机实现必须具有某种自动堆存储管理策略——大部分是采用垃圾收集器。
卸载类型
装载、连接或初始化失败了会怎么样?
Reference
- 类和对象的生命周期:B.· 文纳斯 (美), 曹晓钢, 蒋靖. 深入 Java 虚拟机: 原书[M]. 机械工业出版社, 2003.
- 图1:JVM系列[1]-Java类的生命周期