ClassLoader的主要职责就是负责各种class文件到jvm中,ClassLoader是一个抽象的class,给定一个class文件的二进制名,ClassLoader会尝试加载并且在jvm中生成构建这个类的各个数据结构,然后使其分布在对应的内存区域中。
- 1类的加载过程简介
类的记载过程一般分为三个比较大的阶段,分别是加载阶段,连接阶段和初始化阶段,如下图所示
加载阶段:主要负责查找并且加载类的二进制数据文件,其实就是class文件。
连接阶段:连接阶段所做的工作比较多,细分的话还可以分为如下三个阶段。
- 验证:主要是确保类文件的正确性,比如class文件的版本,class文件的魔术因子是否正确。
- 准备:为类的静态变量分配内存,并且为其初始化默认值。
- 解析:把类中的符号引用转换为直接引用。
初始化阶段:为类的静态变量赋予正确的初始值(代码编写阶段给定的值)
jvm对类的初始化是一个延迟的机制,即:使用的是lazy的方式,当一个类在首次使用的时候才会被初始化,在同一个运行时包下,一个class只会被初始化一次
(运行时包和类的包时有区别的,下次再说),那么什么是类的主动使用和被动使用呢?
- 2 类的主动使用和被动使用
jvm虚拟机规范规定了,每个类或者接口被java程序首次主动使用时才会对其进行初始化,当然随着JIT技术越来越成熟,JVM运行期间的编译也越来越只能,
不排除JVM在运行期间提前预判并且初始化某个类。
JVM同时规范了以下6种主动使用类的场景,具体如下
- 通过new关键字会导致类的初始化:这种是大家经常采用的初始化一个类的方式,它肯定会导致类的加载并且最终初始化。
- 访问类的静态变量,包括读取和更新会导致类的初始化,这种情况的示例代码如下:
public class SimpleOne { static{ System.out.println("我会被初始化"); } public static int x = 10; }
这段代码中x是一个简单的静态变量,其他类即使不对SimpleOne进行new的创建,直接访问x也会导致类的初始化。
- 访问类的静态方法,会导致类的初始化,这种情况的示例代码如下:
public class SimpleTwo {
static{
System.out.println("我会被初始化");
}
// 静态方法
public static void test(){
}
}
同样,在其他类中直接调用test静态方法也会导致类的初始化。
- 对某个类进行反射操作,会导致类的初始化,这种情况的示例代码如下:
public class InvokeClass {
public static void main(String[] args) {
try{
Class.forName("com.lanlei.classLoader.SimpleOne");
}catch(ClassNotFoundException e){
}
}
}
运行上面的代码,同样会看到静态代码块中的输出语句执行。
- 初始化子类会导致父类的初始化,这种情况的示例代码如下:
public class Parent {
static{
System.out.println("父类初始化了");
}
public static int y = 100;
}
public class Child extends Parent{
static{
System.out.println("子类会被初始化");
}
public static int x = 10;
}
public class ActiveLoadTest {
public static void main(String[] args) {
System.out.println(Child.x);
}
}
在ActiveLoadTest中,我们调用了Child的静态变量,根据前面的知识可以得出Chid类被初始化了,Child类又是Parent类的子类,子类的初始化会进一步导致
父类的初始化,当然这里需要注意的一点是,通过子类使用父类的静态变量只会导致父类的初始化,子类则不会被初始化,示例代码如下:
public class ActiveLoadTest {
public static void main(String[] args) {
System.out.println(Child.y);
}
}
改写后的ActiveLoadTest,直接用Child访问子类的静态变量y,并不会导致Child的初始化,仅仅会导致Parent的初始化。
- 启动类:也就是执行main函数所在的类会导致该类的初始化,比如使用java命令运行上下文中的ActiveLoadTest类。
除了上述6种情况,其余的都被称为被动引用,不会导致类的加载和初始化。
- 3 类的加载过程详解
在正式讲解类的各个阶段的内容之前,请大家思考下面这段程序的输出结果,如果你不能准确计算出结果或者感觉有点模棱两可,那么请认真看完本小节。
public class Singleton {
// ①
private static int x =0;
private static int y;
private static Singleton instance = new Singleton(); // ②
private Singleton(){
x++;
y++;
}
public static Singleton getInstance(){
return instance;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.x);
System.out.println(singleton.y);
}
}
运行上面的程序代码输出将是多少?如果将注释②的代码移到注释①的位置,输出结果又是什么呢?两种输出会产生不一样的结果,为何会发生这样的
现象,下面就看下本小节寻找答案。
- 3.1 类的加载阶段
简单来说,类的加载就是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存总
生成一个该类的java.lang.Class对象,作为访问方法区数据结构的入口,如下图所示。
类加载的最终产物就是堆内存中的class对象,对同一个ClassLoader来讲,不管某个类被加载了多少次,对应到堆内存中的class对象始终是同一个。虚拟机
规范中指出了类的加载是通过一个全限定名(包名+类名)来获取二进制数据流,但是并没有限定必须通过某种方式获得,比如我们常见的二进制文件的形式,
但是除此之外还会有如下的几种形式。
- 运行时动态生成,比如通过开源的ASM包可以生成一些class,或者通过动态代理java.lang.Proxy也可以生成代理类的二进制字节流。
- 通过网络获取,比如很早之前的Applet小程序,以及RMI动态发布等。
- 通过读取zip文件获得类的二进制字节流,比如jar、war(其实,jar和war使用的是和zip同样的压缩算法)。
- 将类的二进制数据存储在数据库的BLOB字段类型中。
- 运行时生成class文件,并且动态加载,比如使用Thrift,AVRO等都是可以在运行时将某个Schema文件生成对应的若干个class文件,然后进行加载。
- 3.2类的连接阶段
类的连接阶段可以细分为三个小的过程,分别为验证,准备,解析。
- 验证
先写到这,改天补充,有点事