【03】Java虚拟机是如何加载Java类的

从class文件到内存中的类,按先后顺序需要经过加载、链接以及初始化三个步骤

一、加载

加载就是查找字节流,并且据此创建类的过程。

除了启动类加载器(所有类加载器的祖师爷,由C++实现,没有对应的Java对象)之外,其他的类加载器都是 java.lang.ClassLoader 的子类。这些类加载器需要先由另一个类加载器,比如说启动类加载器加载至JVM中,才能进行类加载。

双亲委派模型:每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器,在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。

Java9之前:
启动类加载器 :加载最基础和最重要的类(JRE的lib目录下jar包中的类)。
扩展类加载器 :其父类加载器是启动类加载器,负责加载相对次要,但通用的类(JRE的lib/ext目录下jar包中的类)。
应用类加载器:其父类加载器是扩展类加载器,负责加载应用程序路径下的类(这里的应用程序路径,是指虚拟机参数 -cp/-classpath、系统变量Java.class.path或环境变量CLASSPATH所指定的路径)默认情况下,应用程序中包含的类便是由应用类加载器加载的。

Java9之后:引入模块系统
扩展类加载器 rename为 平台类加载器

除了由 Java 核心类库提供的类加载器外,我们还可以加入自定义的类加载器,实现特殊的加载方式,eg 可以对class文件进行加密,加载时再利用自定义的类加载器对其解密。

在JVM中,类的唯一性是由类加载器实例以及类的全名一同确认。即使是同一串字节类,经由不同类加载器加载,也会得到两个不同的类。

二、链接

链接,是指将创建成的类合并至JVM中,使之能够执行的过程,分为三个如下阶段

Created with Raphaël 2.3.0 验证(确保被加载类能满足JVM的约束条件) 准备(为被加载类的静态字段分配内存) 解析(将符号引用解析成为实际引用)

注意:在class文件被加载至JVM之前,该类无法知道其他类及方法、字段对应的具体地址,甚至不知道自己方法、字段的地址,因此,每当需要引用这些成员时,Java编译器会生成一个符号引用(java编译器会暂时使用符号引用表表示目标方法),在运行阶段,该符号引用一般能无歧义的定位到具体目标,解析的目的就是将这些符号引用解析成为实际引用
当然,这些符号引用有可能指向一个未被加载的类或类的字段,那么解析也将触发这个类的加载(但未必触发这个类的链接及初始化)

1.非接口符号引用

假定该符号引用指向C类

Created with Raphaël 2.3.0 在C中查找符合名字及描述符的方法 未找到,则找C父类直到Object类 未找到,则找C直接或间接实现的接口中搜索
2.接口符号引用

假定该符号引用指向接口I

Created with Raphaël 2.3.0 在I中查找符合名字及描述符的方法 未找到,在Object类中的公有实例方法中搜索 未找到,则在I的超接口中搜索

经过上述解析步骤后,符号引用会被解析成实际引用。
对于静态绑定的方法调用而言,实际引用是一个指向方法的指针;对于动态绑定的方法调用而言,实际引用是一个方法表的索引

三、初始化

只有当初始化完成以后,类才正式成为可执行的状态。

JVM进行类的初始化之前,先简单了解下Java编译器都做了什么工作。
对于Java中初始化,静态字段的初始化有点特殊,
两种方式初始化一个静态字段,
(1)声明时直接赋值(若直接赋值的静态字段被final所修饰,且它的类型是基本类型或字符串时,该字段便会被编译器标记成常量,其初始化直接由JVM完成)
(2)在静态代码块中对其赋值。

所以除了对于常量是直接由JVM完成,其余的初始化(直接赋值操作及所有静态代码块中的代码),会被Java编译器置于同一个方法中,并把它命名为***< clinit >***。

总结为一句话就是,类加载的最后一步初始化,便是为标记为常量 的字段赋值以及执行***< clinit >***方法的过程。

类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。如下demo

public class Singleton {
  private Singleton() {}
  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
  }
  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
}

这段代码是著名的单例延迟初始化例子,只有当调用Singleton.getInstance 时,程序才会访问LazyHolder.INSTANCE,才会触发对 LazyHolder 的初始化(对应下图第 4 种情况),继而新建一个 Singleton 的实例

![image.png-74.8kB][1]

个人思考

public class Singleton {
	private Singleton() {}
	private static class LazyHolder {
		static final Singleton INSTANCE = new Singleton();
		static {
			System.out.println("LazyHolder.<clinit>");
		}
	}
	public static Object getInstance(boolean flag) {
		if (flag) return new LazyHolder[2];
		return LazyHolder.INSTANCE;
	}
	public static void main(String[] args) {
		getInstance(true);
		System.out.println("----");
		getInstance(false);
	}
}

1.新建数据(10行)会导致LazyHolder的加在吗?会初始化吗?
2.新建数组会导致LazyHolder的链接吗?
答案:
1.虚拟机必须知道(加载)有这个类,才能创建这个类的数组(容器),但是这个类并没有被使用到(没有达到初始化的条件),所以不会初始化。所以新建数组会加载元素类LazyHolder;不会初始化元素类
2.新建数组的时候并不是要使用这个类(只是定义了放这个类的容器),所以不会被链接,调用getInstance(false)的时候约等于告诉虚拟机,我要使用这个类了,你把这个类造好(链接),然后把static修饰的字符赋予变量(初始化)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值