性能文章>深入理解JAVA虚拟机——类加载>

深入理解JAVA虚拟机——类加载原创

706102

概述

本文基于JDK8分析Java类的加载过程以及双亲委派模型的实现原理。

什么是JVM

毫无疑问,Java一直是企业应用程序的领先的编程语言,我认为这主要是受益于Java虚拟机(JVM)的成熟。JVM已有20多年的历史了,它被认为是有史以来最可靠,优化最好的运行时引擎之一。JVM是Java Virtual Machine(Java虚拟机)的缩写,是一种对于计算设备的规范。虚拟机该怎么理解,我在网上找了如下觉得比较靠谱的解释:

虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

JVM屏蔽了底层操作系统平台的差异并且减少基于原生语言开发的复杂性,从而使得Java能够跨各种平台(Write once,run anywhere)。一般情况下我们无需了解JVM的运行原理,只需要专注于开发业务逻辑就可以了,但是理解JVM的工作机制也许可以帮忙我们写出更加高效健壮的代码。

Java执行过程

首先我们看下Java代码是如何执行的,Java代码的执行过程可以分为两部分

第一步:编译器把源代码(.java)编译成.class 文件;

第二步:类加载器把.class文件装载进虚拟机,JVM来识别并执行。

JVM针对每个操作系统开发其对应的解释器,所以只要其操作系统有对应版本的JVM,那么这份Java编译后的代码就能够运行起来,这就是Java能一次编译,到处运行的原因,那么你是否知道class文件是如何加载的呢?

Java类加载过程

Java类加载过程一般需要以下几个步骤:
image.png
概括起来说就是加载->链接->初始化,而链接又可以细分为验证->准备->解析,各个阶段的主要职责:

  • 加载:类加载器ClassLoader查找和导入Class 文件;

  • 链接:把类的二进制数据合并到JRE中;

  • 验证:检查载入class文件数据的正确性;

  • 准备:给类的静态变量分配存储空间并设置类变量初始值;

  • 解析:虚拟机把常量池的符号引用替换为直接引用;

  • 初始化:开始执行类中定义的Java程序代码,初始化Java代码类变量和其他资源。
    Java类加载阶段的链接和初始化不是本文讨论的重点,本文主要讨论一下类加载的第一步之类加载器ClassLoader是如何加载字节码的。

加载时机

  • 创建类的实例

  • 使用类的静态变量或者为静态变量赋值

  • 调用类的静态方法

  • 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象

  • 初始化某个类的子类

  • 直接使用java命令来运行某个主类

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行过程中遇到未知类时就会调用ClassLoader来加载这些类,加载完成后就会将Class对象存在ClassLoader里面,下次就不需要重新加载了,讲到类的加载不得不提加载器的类型。

加载器的类型

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

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

  • 应用程序类加载器(Application ClassLoader):这个类加载器由 sun.misc.Launcher的AppClassLoader 实现。getSystemClassLoader() 方法返回的就是这个类加载器,因此也被称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
    我们的应用程序都是由这三种类加载器互相配合进行加载的,每个加载器各司其职,在必要时我们还可以自己定义类加载器如下图所示。
    image.png
    那么遇到一个未知类时到底选择哪个ClassLoader来加载呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。什么叫调用者 Class 对象?在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是AppClassLoader。前面提到AppClassLoader 只负责加载 Classpath 下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader 必须将系统类库的加载工作交给 BootstrapClassLoader 和ExtensionClassLoader 来做,这就是我们常说的双亲委派。

双亲委派

双亲委派原理是如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。有一个比较形象的比喻:每个儿子都很懒,每次有活(类加载)就丢给父亲(父加载器)去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。
为了说明这个委托关系我们通过一个自定义类加载器的例子说明一下。

  1. 简单定义一个TestClassLoader类,javac命令编译成class 文件,记下文件的路径
    image.png
  2. 自定义一个类加载器
    自定义一个类加载器,需要继承ClassLoader类,并实现findClass方法。其中defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class,实现如下:
    image.png
  3. debug查看继承关系
    image.png
    从图中可以看到自定义类加载器的父加载器是AppClassLoader,AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader,父加载器是null。Java 的每个类都是有父加载器,为什么ExtClassLoader的父加载器是null?

父加载器不是父类
我们看下ExtClassLoader和AppClassLoader的源代码,这两个ClassLoader同样继承自URLClassLoader,但是为什么说ExtClassLoader是AppClassLoader的父加载器呢?
image.png
继续查看Launcher的源码可以看到AppClassLoader的parent是一个ExtClassLoader实例,如下图所示:

image.png
在源码中没有直接找到对parent的赋值,印证了ExtClassLoader的parent为null。既然ExtClassLoader的父加载器是null,BootstrapCLassLoader为什么可以当作他的父加载器?因为Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个Java类,也就是无法在Java代码中获取它的引用。JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,然后JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例,并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器,比如ExtClassLoader。

委派流程

类加载的委派流程如下所示:
image.png
一个AppClassLoader查找资源时,先看看缓存是否有,如果缓存中有则从缓存中获取,否则委托给父加载器。如果ExtClassLoader也没有加载过,则由Bootstrap ClassLoader出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到就让子加载器自己去找。Bootstrap ClassLoader如果没有查找成功,则ExtClassLoader自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。ExtClassLoader查找不成功,AppClassLoader就自己查找,在java.class.path路径下查找,找到就返回,如果没有找到就让子类找,如果没有子类会抛出各种异常,如下是双亲委派模型的源码实现:

image.png
为什么使用双亲委派
双亲委派模型是JDK1.2之后引入的。根据双亲委派模型原理,可以试想,没有双亲委派模型时,如果用户自己写了一个全限定名为java.lang.Object的类,并用自己的类加载器去加载,同时BootstrapClassLoader加载了rt.jar包中的JDK本身的java.lang.Object,这样内存中就存在两份Object类了,此时就会出现很多问题,例如根据全限定名无法定位到具体的类。有了双亲委派模型后,所有的类加载操作都会优先委派给父类加载器,这样一来,即使用户自定义了一个java.lang.Object,但由于BootstrapClassLoader已经检测到自己加载了这个类,用户自定义的类加载器就不会再重复加载了。所以,双亲委派模型能够保证类在内存中的唯一性,同时避免了Java的核心API被篡改。

破坏双亲委派

当然双亲委任模型不是一个强制性的约束模型,而是一个建议型的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型。但也有例外,例如Tomcat类加载器的设计,作为一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。如果使用默认加载器是无法完成加载两个相同类库的不同版本的,如果全限定类名相同则只会加载一份;作为web容器还需要支持jsp文件的热加载,如果我们改了jsp的内容,但是类名相同默认类加载器会直接查找方法区已经存在的,修改后的jsp是不会重新加载的。为了完成这个需求,Tomcat通过一个新的classloader再次装载了该jsp,每个jsp文件都对应一个唯一的类加载器。所以说只要有足够的意义和理由突破已有的规则也是一种创新。

总结

本文先介绍了ClassLoader的作用,主要用于从指定路径查找class并加载到内存。介绍了双亲委派模型,以及其实现原理,JDK中主要是在ClassLoader的loadClass方法中实现双亲委派模型的。

本文转载自花椒技术微信公众号

请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得