本文章将解释java的类加载机制。
类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。
类的生命周期包括以下 7 个阶段:
● 加载(Loading)
● 验证(Verification)
● 准备(Preparation)
● 解析(Resolution
● 初始化(Initialization)
● 使用(Using)
● 卸载(Unloading)
类加载过程包含:加载、验证、准备、解析和初始化 ,一共包括5 个阶段。
加载是类加载的第一个阶段,注意不要混淆。
加载过程完成以下3件事:
○ 通过类的完全限定名称获取定义该类的二进制字节流。
○ 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
○ 在内存中生成一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。
其中二进制字节流可以从以下方式中获取:
○ 从 ZIP 包读取,成为 JAR、EAR、WAR格式的基础。
○ 从网络中获取,最典型的应用是 Applet。
○ 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
○ 由其他文件或容器生成,例如由 JSP 文件生成对应的 Class 类。
确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
● 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存。
● 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
初始值一般为 0 值。
例如:下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0。
例如:下面的常量 value 被初始化为 123 而不是 0。
public static final int value = 123;
将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
●初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器
●
例如:以下代码中静态变量i只能赋值,不能访问,因为i定义在静态代码块的后面。
public class Test {
static {
i = 0; // 给变量赋值可以正常编译通过
System.out.print(i); // 这句编译器会提示“非法向前引用”
}
static int i = 1;
}
由于父类的 () 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
●接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成
●虚拟机会保证一个类的
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列六种情况必须对类进行加载:
除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用。
被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类加载。
- 通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
● 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机的一部分;
● 其它类的加载器,使用 Java 实现,独立于虚拟机,继承自抽象类 java.lang.ClassLoader。
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
● 启动类加载器(Bootstrap ClassLoader),该类加载器负责将存放在
● 扩展类加载器(Extension ClassLoader),该类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现,负责将
● 应用程序类加载器(Application ClassLoader),该类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,比如:我们自己编写的自定义类或第三方jar包。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序是由三种类加载器互相配合,从而实现类加载,除此之外还可以加入自己定义的类加载器。
类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一,避免冲突
例如:java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath中的 Object使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中使用的所有的 Object 都是由启动类加载器所加载的 Object。