目录
1、启动类加载器(Bootstrap ClassLoader)
2、扩展类加载器(Extension ClassLoader)
3、应用程序类加载器(Application ClassLoader)
类是在运行期间第一次使用时动态加载的,而不是编译时期一次性加载。因为如果在编译时期一次性加载,那么会占用很多的内存。
类的生命周期为:
加载(Loading) 验证(Verification) 准备(Preparation) 解析(Resolution) 初始化(Initialization)
使用(Using) 卸载(Unloading)
类加载过程:
包含了加载、验证、准备、解析和初始化这 5 个阶段。
1、加载
加载是类加载的第一个阶段。
此过程完成三件事:
1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
3、在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
但该Class文件不一定非要从.class文件获取,还可以有:从 ZIP 包中读取(比如从 jar 包和 war 包中读取)、在运行时计算生成(动态代理)、也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)、从网络中获取( Applet)
2、验证
主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3、准备
类变量(被static修饰的变量)准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化后随着对象一起分配在堆中。注意,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
public static int value = 8080;
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 8080,将 v 赋值为 8080 的 put static 指令是程序被编译后。
public static final int v = 8080;
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
4、解析
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的 动态绑定。
符号引用:
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。
直接引用:
直接引用可以是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
5、初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
<clinit>() 方法具有以下特点:
1、是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。
2、虚拟机会保证子<client>方法执行之前,父类的<clinit>()方法已经执行完毕。
3、如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<clinit>()方法。
4、父类中定义的静态语句块要优先于子类的变量赋值操作
5、与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。
6、接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。
7、虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步
类初始化时机:
1、主动引用
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
1、遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没 有进行过初始化,则必须先触发其初始化。
2、使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初 始化,则需要先触发其初始化。
3、当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发 其父类的初始化。
4、当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个 类),虚拟机会先初始化这个主类;
5、当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的 方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其 初始化;
2、被动引用
除以上之外,所有引用类的方式 都不会触发初始化,称为被动引用。被动引用的常见例子包括:
1、通过子类引用父类的静态字段,只会触发父类的初始化,不会导致子类初始化。
2、通过数组定义来引用类,不会触发此类的初始化。
3、常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量 的类,因此不会触发定义常量的类的初始化。
4、通过类名获取 Class 对象,不会触发类的初始化。
5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初
始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。
类加载器
两个类相等需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个 类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、 isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关 系判定结果为 true。
分类
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚 拟机自身的一部分;
所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继 承自抽象类 java.lang.ClassLoader。
细致一些可以分为三类:
1、启动类加载器(Bootstrap ClassLoader)
负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath 参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。
2、扩展类加载器(Extension ClassLoader)
负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。
3、应用程序类加载器(Application ClassLoader)
负责加载用户路径(classpath)上的类库。
双亲委派模型:
JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader实现自定义的类加载器。
应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己 定义的类加载器。
下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都 应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合 (Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
工作过程:
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试自己去加载。
好处:
采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
本节主要整理类加载机制,由于某些部分较为繁琐,省去了一些长篇解释,只留下了一些总结。
JVM部分就用这三节去整理,更多细节请查看相应书籍,如《周志明. 深入理解 Java 虚拟机》