深入理解Java虚拟机(类加载机制)

时间:2021-11-24 14:19:47

文章首发于微信公众号:BaronTalk

上一篇文章我们介绍了「类文件结构」,这一篇我们来看看虚拟机是如何加载类的。

我们的源代码经过编译器编译成字节码之后,最终都需要加载到虚拟机之后才能运行。虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制

与编译时需要进行连接工作的语言不同,Java 语言中类的加载、连接和初始化都是在程序运行期间完成的,这种策略虽然会让类加载时增加一些性能开销,但是会为 Java 应用程序提供高度的灵活性,Java 里天生可动态扩展的语言特性就是依赖运行期间动态加载和动态连接的特点实现的。

例如,一个面向接口的应用程序,可以等到运行时再指定实际的实现类;用户可以通过 Java 预定义的和自定义的类加载器,让一个本地的应用程序运行从网络上或其它地方加载一个二进制流作为程序代码的一部分。

一. 类加载时机

类从被虚拟机从加载到卸载,整个生命周期包含:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)7 个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking)。这 7 个阶段的发生顺序如下图:

深入理解Java虚拟机(类加载机制)

上图中加载、验证、准备、初始化和卸载 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始「注意,这里说的是按部就班的开始,并不要求前一阶段执行完才能进入下一阶段」,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

虚拟机规范中对于什么时候开始类加载过程的第一节点「加载」并没有强制约束。但是对于「初始化」阶段,虚拟机则是严格规定了有且只有以下 5 种情况,如果类没有进行初始化,则必须立即对类进行「初始化」(加载、验证、准备自然需要在此之前开始):

  1. 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令;
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候;
  3. 当初始化一个类的时候,发现其父类还没有进行初始化的时候,需要先触发其父类的初始化;
  4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个类;
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有初始化。

「有且只有」以上 5 种场景会触发类的初始化,这 5 种场景中的行为称为对一个类的主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。比如如下几种场景就是被动引用:

  1. 通过子类引用父类的静态字段,不会导致子类的初始化;
  2. 通过数组定义来引用类,不会触发此类的初始化;
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化;

二. 类加载过程

加载

这里的「加载」是指「类加载」过程的一个阶段。在加载阶段,虚拟机需要完成以下 3 件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流;
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致上会完成下面 4 个阶段的检验动作:

  1. 文件格式验证:第一阶段要验证字节流是否符合 Class 文件格式的规范,并且能够被当前版本的虚拟机处理。验证点主要包括:是否以魔数 0xCAFEBABE 开头;主、次版本号是否在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型;Class 文件中各个部分及文件本身是否有被删除的或者附加的其它信息等等。

  2. 元数据验证:第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求,这个阶段的验证点包括:这个类是否有父类;这个类的父类是否继承了不允许被继承的类;如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法;类中的字段、方法是否与父类产生矛盾等等。

  3. 字节码验证:第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  4. 符号引用验证:最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段--解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的形象进行匹配性校验。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区进行分配。这个阶段中有两个容易产生混淆的概念需要强调下:

  • 首先,这时候进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中;

  • 其次这里所说的初始值「通常情况」下是数据类型的零值。假设一个类变量的定义为public static int value = 123; 那么变量 value 在准备阶段过后的初始值为 0 而不是 123,因为这个时候尚未执行任何 Java 方法,而把 value 赋值为 123 的 putstatic 指令是程序被编译之后,存放于类构造器 () 方法之中,所以把 value 赋值为 123 的动作将在初始化阶段才会执行。

这里提到,在「通常情况」下初始值是零值,那相对的会有一些「特殊情况」:如果类字段的字段属性表中存在 ConstantsValue 属性,那在准备阶段变量 value 就会被初始化为 ConstantValue 属性所指的值。假设上面的类变量 value 的定义变为 public static final int value = 123;,编译时 JavaC 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 123。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。前面提到过很多次符号引用和直接引用,那么到底什么是符号引用和直接引用呢?

  • 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以上任何形式的字面量,只要使用时能无歧义地定位到目标即可。

  • 直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

初始化

类初始化阶段是类加载过程中的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全是由虚拟机主导和控制的。到了初始化阶段,才真正开始执行类中定义的 Java 程序代码。初始阶段是执行类构造器 () 方法的过程。

三. 类加载器

虚拟机设计团队把类加载阶段中的「通过一个类的全限定名来获取描述此类的二进制字节流」这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为「类加载器」。

类与类加载器

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机的唯一性,每个类加载器都拥有一个独立的类名称空间。也就是说:比较两个类是否「相等」,只要在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

双亲委派模型

从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用 C++ 来实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由 Java 来实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader

从 Java 开发者的角度来看,类加载器可以划分为:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在 <java_home>\lib 目录中的类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,那直接使用 null 代替即可;

  • 扩展类加载器(Extension ClassLoader):这个类加载器由 sun.misc.Launcher$ExtClassLoader实现,它负责加载 <java_home>\lib\ext 目录中,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;

  • 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher$App-ClassLoader 实现。getSystemClassLoader() 方法返回的就是这个类加载器,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这 3 种类加载器互相配合进行加载的,在必要时还可以自己定义类加载器。它们的关系如下图所示:

深入理解Java虚拟机(类加载机制)

上图中所呈现出的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器以外,其余的类加载器都应当有自己的父类加载器。

双亲委派模型的工作过程是这样的:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个类加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

这样做的好处就是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如 java.lang.Object,它放在 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器来加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为 java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,Java 类型体系中最基本的行为也就无法保证了。

双亲委派模型对于保证 Java 程序运行的稳定性很重要,但它的实现很简单,实现双亲委派模型的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中,逻辑很清晰:先检查是否已经被加载过,若没有则调用父类加载器的 loadClass() 方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出 ClassNotFoundException 异常后,再调用自己的 findClass() 方法进行加载。

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 首先,检查请求的类是不是已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类抛出 ClassNotFoundException 说明父类加载器无法完成加载
} if (c == null) {
// 如果父类加载器无法加载,则调用自己的 findClass 方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

关于类文件结构和类加载就通过连续的两篇文章介绍到这里了,下一篇我们来聊聊「虚拟机的字节码执行引擎」。

参考资料:

  • 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 2 版)》

如果你喜欢我的文章,就关注下我的公众号 BaronTalk 、 知乎专栏 或者在 GitHub 上添个 Star 吧!

深入理解Java虚拟机(类加载机制)

深入理解Java虚拟机(类加载机制)的更多相关文章

  1. 深入理解Java虚拟机---类加载机制(简略版)

    类加载机制 谈起类加载机制,在这里说个题外话,当初本人在学了两三个月的Java后,只了解了一些皮毛知识,就屁颠屁颠得去附近学校的招聘会去蹭蹭面试经验,和HR聊了一会后开始了技术面试,前抛出了两个简单的 ...

  2. 深入理解Java虚拟机类加载机制

    1.类加载时机 对于类加载的第一个阶段---加载,虚拟机没有强制的约束,但是对于初始化阶段,虚拟机强制规定有且只有以下的5中情况必须开始初始化,当然,加载.验证.准备阶段在初始化前就已经开始. ①使用 ...

  3. 面试官,不要再问我&OpenCurlyDoubleQuote;Java虚拟机类加载机制”了

    关于Java虚拟机类加载机制往往有两方面的面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断程 ...

  4. &lbrack;转&rsqb;Java虚拟机类加载机制

    原文地址:http://blog.csdn.net/u013256816/article/details/50829596 看到这个题目,很多人会觉得我写我的java代码,至于类,JVM爱怎么加载就怎 ...

  5. 面试官,不要再问我&OpenCurlyDoubleQuote;Java虚拟机类加载机制”了&lpar;转载&rpar;

    关于Java虚拟机类加载机制往往有两方面的 面试题:根据程序判断输出结果和讲讲虚拟机类加载机制的流程.其实这两类题本质上都是考察面试者对Java虚拟机类加载机制的了解. 面试题试水 现在有这样一道判断 ...

  6. 【转载】Java虚拟机类加载机制与案例分析

    出处:https://blog.csdn.net/u013256816/article/details/50829596 https://blog.csdn.net/u013256816/articl ...

  7. Java虚拟机类加载机制——案例分析

    转载: Java虚拟机类加载机制--案例分析   在<Java虚拟机类加载机制>一文中详细阐述了类加载的过程,并举了几个例子进行了简要分析,在文章的最后留了一个悬念给各位,这里来揭开这个悬 ...

  8. java虚拟机类加载机制和双亲委派模型

    java虚拟机类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型. 类的生命周期是从类被加载到虚拟机内存中,到卸 ...

  9. Java 虚拟机类加载机制

    看到这个题目,很多人会觉得我写我的java代码,至于类,JVM爱怎么加载就怎么加载,博主有很长一段时间也是这么认为的.随着编程经验的日积月累,越来越感觉到了解虚拟机相关要领的重要性.闲话不多说,老规矩 ...

  10. 十年Java程序员-带你走进Java虚拟机-类加载机制

    类的生命周期 1.加载 将.class文件从磁盘读到内存 2.连接 2.1 验证 验证字节码文件的正确性 2.2 准备 给类的静态变量分配内存,并赋予默认值 2.3 解析 类装载器装入类所引用的其它所 ...

随机推荐

  1. 【JavaScript】ArtTemplate个人的使用体验。

    据说ArtTemplate是腾讯的,感觉这东西真不错,使用方便,用起来很简单,哈哈.腾讯也不完全只是坑爹啊. ArtTemplate 使用是,正常引入js,这个自然不用说.这东西啥时候使用呢?我觉得这 ...

  2. &lbrack;No00002E&rsqb;关于大数据&comma;你不知道的6个迷思

    还是那个观点:计算机,编程语言,互联网,大数据等等都只是工具! 导语:看过美剧<纸牌屋>没?知道这部"白宫甄嬛传"为什么会火吗?靠的是大!数!据! 过去两年,在 Net ...

  3. php时间戳与时间转换

    PHP时间大的来分有两种,一是时间戳类型(1228348800),二是正常日期格式(2008-12-4) 所以存到数据库也有两种形式了(真正不止,我的应用就两种),时间戳类型我是保存为字符串的,这个是 ...

  4. 简单理清一下proto与prototype

    这篇博客主要是为了理清自己的思路. 先上图,所有内容都从这张图来讲. 在js中,所有的东西都是对象,包括是function. prototype这个属性是函数特有的.有两层含义,第一层含义指的是某对象 ...

  5. IDEA热部署(一)---解析关键配置。

    本编博客转载自:因为自己在研究热部署,包括热部署那些文件,部署实现的包括那些操作.这一块,所以这篇好博客. http://www.mamicode.com/info-detail-1699044.ht ...

  6. Android图表库MPAndroidChart&lpar;二&rpar;——线形图的方方面面,看完你会回来感谢我的

    Android图表库MPAndroidChart(二)--线形图的方方面面,看完你会回来感谢我的 在学习本课程之前我建议先把我之前的博客看完,这样对整体的流程有一个大致的了解 Android图表库MP ...

  7. &lbrack;原创&rsqb;SecureCRT终端软件连接VMware Workstation Pro虚拟机

    Step1:检查主机的桥接有没有禁用 Step2:进入Ubuntu系统,进入到Ubuntu下,先查看Ubuntu虚拟机的IP配置,打开终端(Ctrl+Alt+T),通过ifconfig命令查看,可以看 ...

  8. 虚拟机硬盘vmdk压缩瘦身并挂载到VirtualBox

    这个问题其实困扰了挺久的,一直没闲情去解决,网上搜索过很多压缩方法感觉都太麻烦太复杂,因最近在windows上搞docker就一并解决了. 压缩vmdk 首先下载DiskGenius,这工具很牛X,相 ...

  9. python数据结构与算法第七天【链表】

    1.链表的定义 如图: 注意: (1)线性表包括顺序表和链表 (2)顺序表是将元素顺序地存放在一块连续的存储区里 (3)链表是将元素存放在通过链构造的存储快中 2. 单向链表的实现 #!/usr/bi ...

  10. WebSocket和long poll、ajax轮询的区别,ws协议测试

    WebSocket和long poll.ajax轮询的区别,ws协议测试 WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连 ...