Java虚拟机加载类的过程_深入理解Java虚拟机之加载Java类的过程

前言

在上一篇深入理解Java虚拟机之内存模型分析中,我简单的介绍了Java虚拟机的内存模型,在开篇的时候也有贴一张类加载的流程图。在对象还没进内存区域的之前,Java类又是如何变成成class文件,进而在Java内存里的呢?

因此在本篇,主要讲Java虚拟机是如何加载Java类的。

类加载器

在说类加载过程前,先说说类加载器,毕竟这也是类加载过程首要的一步。类加载器主要负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。

而JVM预定义有三种类加载器,如下三种类加载器:

根类加载器(bootstrap class loader)

根类加载器加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。

由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

扩展类加载器(extensions class loader)

扩展类加载器负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。

扩展类加载器是由Java语言实现,父类加载器为null。

系统类加载器(system class loader)

系统类加载器也被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。

程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。

系统类加载器也是由Java语言实现,父类加载器为扩展类加载器。

类加载过程

一般来说,我们把Java的类加载过程分为三个主要步骤:加载、链接、初始化。因此我们要从这三个主要步骤开始说起,这里我会用搭建楼房的方式来比喻类加载过程,也方便理解类加载过程。

加载阶段

加载这里我们可以理解为是Java查找字节流,将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(class文件),如 jar 文件、 class文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError 。

在加载过程中,除了启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类,因此有对应的 Java 对象。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中,方能执行类加载。

加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。

链接阶段

链接阶段是类加载过程中最重要的阶段,主要是把原始的类定义信息转化到JVM运行的过程中,这里也分了三个步骤:验证、准备、初始化。

验证

在JVM接收到类加载过来的字节信息后,也需要需要核验是符合Java虚拟机规范的,否则就被认为是VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。

因此,验证阶段也是JVM安全的一道保障。

准备

通过验证完后,接收到的字节信息就可以在JVM具体初始化,这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,

不会去执行更进一步的 JVM 指令。

就比如,搭房子的时候,我们先把这一块的材料都准备好,等建筑工人过来,就可以直接搭房子了。

除了分配内存外,部分 Java 虚拟机还会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。

解析

解析阶段主要是将常量池中的符号引用(symbolic reference)替换为直接引用。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

初始化阶段

初始化阶段是类加载的最后一步,这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这

部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。

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

那么,类的初始化何时会被触发呢?

在JVM 规范枚举了下述多种触发情况:

当虚拟机启动时,初始化用户指定的主类;

当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;

当遇到调用静态方法的指令时,初始化该静态方法所在的类;

当遇到访问静态字段的指令时,初始化该静态字段所在的类;

子类的初始化会触发父类的初始化;

如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

使用反射 API 对某个类进行反射调用时,初始化这个类;

当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

双亲委派

说完了类加载过程,也顺便说说双亲委派问题吧。在之前的一次面试中,面试官也刚好问过这个问题,那时候只知道一个模糊的概念,因此回答得也是挺模糊的,所以在此也好好梳理一下双亲委派模型。

什么是双委派模型

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

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

e427bf32d3f3059d5577f8533ab41091.png

Java 9 变化

在 Java 9 之前,启动类加载器负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。除了启动类加载器之外,另外两个重要的类加载器是扩展类加载器(extension class loader)和应用类加载器(application class loader),均由 Java 核心类库提供。

扩展类加载器的父类加载器是启动类加载器。它负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。

应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的。

Java 9 引入了模块系统,并且略微更改了上述的类加载器1。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。

最后

Java类加载这一篇也算总结完了,JVM一直以来都是比较复杂的一块,最近也在看一本《深入Java虚拟机》的书,书中还是有很多自己不能理解的,包括类加载过程中,还是有一些小细节需要好好打磨的。在以后,我也会把自己理解的写出来,类加载过程的总结就先到这里结束了!

Coding for dream!

The first step is as good as half over!!

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值