java 虚拟机浅析(二)JVM是怎么加载class文件的

本文详细介绍了Java虚拟机(JVM)如何加载class文件,包括加载(loading)、链接(linking)和初始化(initializing)三个步骤。重点探讨了类加载器的层次结构和双亲委派模型,以及其对安全性的影响。同时,文章通过源码阅读解析了类加载的过程,并给出了一道面试题以加深理解。
摘要由CSDN通过智能技术生成

前言

上一篇文章已经介绍过JVM只跟class类型的文件发生交互。

本篇文章就要介绍JVM是如何将硬盘中的class文件加载到内存中的。

如果之前您使用过java的反射机制,就知道java中每个类都有个类对象,即Class的对象,通过ClassName.class或者instance.getClass()可以拿到这个对象。

我们可能会问这个类对象是如何产生的呢?

简而言之是将class文件加载进内存后,JVM创建了这个类对象指向这块内存。

JVM中将这个过程分成了三步,分别是loadinglinkinginitializing,本文就会介绍这三个步骤,其中着重介绍loading阶段。

loading

loading就是将class文件从硬盘中加载进JVM内存中的过程。

这个加载过程的核心是类加载器(ClassLoader),负责将class文件加载进内存。

通过调用类对象的getClassLoader()方法就能知道是哪个类加载器加载的当前的类文件。

下面我们就来介绍一下类加载器。

类加载器

要注意一点的是,JVM中的类加载器不只有一个,类加载器之间是有层次高低区别的。

类加载器的种类

JVM中的类加载器根据它们负责加载的类的不同,分为下面四种不同的类型:

Bootstrap:负责加载核心类。这个类加载器是由C++实现的,也就是说即使调用getClassLoader()也得不到这个类加载器,只能得到null。

Extension:负责加载位于jre/lib/ext/目录下的类。

App:负责加载classpath指定的内容,我们自己编写的类都在classpath中。

Custom:我们可以自定义的类加载器,可以由我们来指定负责加载哪里的类。

类加载器的层次与双亲委派

JVM在加载类文件的时候,不同的类加载器的层次决定了各自发挥作用的顺序。

类加载器的层次从高到低依次是:Bootstrap,Extension,App,Custom。其中上一层的加载器是下一层加载器的父加载器

注意,这里的父加载器,不是java中继承(extends)的关系,而是一种逻辑上的关系,父加载器在子加载器中以parent的成员变量形式存在。

这个过程的流程如下图所示:

类加载器加载类文件的流程图

根据上面的流程图,JVM加载类文件的时候是分为两个阶段的——检查缓存与执行加载。

这个过程有个值得被记住的名字:双亲委派

检查缓存

每个类加载器内部都维护了一个缓存,存放着之前加载过的类文件。

当JVM检查缓存的时候,会从最低级的类加载器开始,也就是Custom加载器。

如果命中缓存就直接返回结果。

如果没命中,就去上一层的父加载器的缓存中找。

依次寻找App、Extension、Boostrap加载器的缓存。如果命中就返回,如果都没命中,就进入加载阶段。

执行加载

当所有层次的加载器的缓存中都不存在要加载的类文件,就会执行加载操作。

从Bootstrap加载器开始,如果要加载的类文件是当前加载器负责,那么当前加载器完成加载。

如果不属于当前加载器负责的类,依次经过Extension、App、Custom加载器判断是否要加载这个类。

如果所有的加载器都负责加载这个类,就会报ClassNotFound的异常。

双亲委派的价值

主要是出于安全的考虑

试想如果没有双亲委派的机制,即从下到上的缓存检查和从上到下的检查加载,那么黑客们完全可以自己开发一个最底层的custom加载器,将JDK运行需要的各种核心类替换成自己写的同名类,当把这些类加载进内存,我们的程序就运行在不可靠的平台上了。

源码阅读

ClassLoader加载类文件的代码在ClassLoader类中的loadClass(String name, boolean resolve)方法中。

这个方法的大致执行流程如下图所示

在这里插入图片描述
开始执行loadClass方法,首先会在当前类加载器上调用findLoadedClass()方法,这个方法就是之前说的在缓存中查找,如果找到了直接返回该类。

如果findLoadedClass在当前类加载器中没找到要加载的类文件的缓存,就会检查这个加载器有没有父加载器,如果有父加载器,就递归调用父加载器的loadClass方法。

如果父加载器的缓存中有这个类文件,那么这个类文件会被返回,否则继续调用父加载器的父加载器继续找,直至最顶层的加载器。

如果都没有找到,那么就进入加载阶段,就是图中的findClass()方法。

这个方法应用了模板方法设计模式,ClassLoader的子类重写这个方法来定义自己负责加载的类的范围。

linking

当class文件加载到内存后,就会对文件进行处理,主要分为verificationpreparationresolusion

下面对每一步进行解释

  • verification
    对文件内容进行校验。

    class文件的内容都有固定的格式,如果载入的文件压根儿就不是class类型的文件,这一步就会报错。

  • preparation

    是linking步骤的核心,主要负责将类中的静态成员变量设置为默认值

  • resolusion

    将类中的符号引用替换为直接引用,类似于把自己写的代码与用到的类库代码拼接在一起。

initializing

这一步就是执行类的初始化代码,给静态成员变量赋予代码中的设定值

一道面试题

这里用一道面试题来进一步讲解linking的preparation步骤和initializing步骤的区别。

有如下一段代码

public class T {
	public static T t = new T(); // 语句1
	public static int count = 2; // 语句2

	private T() {
		count++;
	}
}

public class Test {
	public static void main(String[] args) {
		System.out.println(T.count);
	}
}

问题:上述代码的输出是多少?

答案:2

原因:当T.class文件加载进内存中,经过linking的preparation,会将静态成员变量赋为默认值,也就是t = nullcount = 0状态。

然后经过initializing后,会执行初始代码,给静态成员变量赋初始值。此时t = new T(),构造函数的代码得到执行,count由0自增到1,但此后count = 2语句得到执行,将count = 1的结果覆盖掉,最后count的值为2。

拓展问题:如果上述代码语句1和语句2位置进行调换,输出的结果是多少?

也就是

	public static int count = 2; // 语句2
	public static T t = new T(); // 语句1

答案:3

原因:经过preparation之后,两个变量的值为默认值,也就是t = nullcount = 0

经过initializing之后,count的值先设为2,然后构造函数中的代码得到执行,count的值由2自增为3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值