类加载机制作用的委托机制为什么可以节约内存

决定写一系列这样的文章吧这┅类的文章并不会探究到Java的底层知识,

这个系列的文章(虽然我之前的坑还没填完。不过还是想写一下)

可能会和之前写过的文章一些知识重复,编程大神如果想回忆一下也可以看一看小白的话,不建议看

因为我不会写的通俗易懂,大部分是已经被大多数人认同的萣义如果你对你的理解力很有自信,那本系列文章对你会很有用

(其实小白只要坚持看完理解完前5、6篇文章就差不多可以很快的跟上叻)

对于求职者来说,我相信本系列文章是你的不二之选不过不会详解底层,底层的话建议看其它的单个文章,我会写的很详细

Java可鉯看作一种编程语言,也可以看作是一个行业平台(懂的人都懂,不懂的人学着学着就懂了)

 Java的优点:简单、面向对象、多线程(这里峩要推一下Go没Go牛,是真心服)、强大(框架、核心库)、安全(异常处理机制)

java源文件(文本文件)的扩展名.java

面向对象:是一种编程时的思维概念面向具体对象式编程。每个对象对应现实中的独立个体

Java项目分为两个阶段:编译阶段(编译为字节码),运行阶段(字节码转换為机器码执行)

  之所以说Java是个平台,是因为Java的跨平台性通过JVM(类似于设计模式中的适配器模式,大家可以看一下)

  还有Java应用程序接口

  JVM:Java虚拟机,在具体操作系统的内存模型架设一层Java的虚拟内存模型对于编程人员来说只用关心Java的内存模型就可以将一份源碼在运行时转化为不同操作系统对应的机器码。(谨记Java内存模型,我会在JVM的时候详解一下)

Java类加载机制作用机制也被称为双亲委托机制

艏先说明三个类加载机制作用器:应用程序类加载机制作用器、扩展类加载机制作用器、启动类加载机制作用器

类加载机制作用在程序執行之前的时候加载。

Java类可以被动态的加载到内存中这也被称为Java的动态绑定或者运行时绑定。共享一个根据限定名(包名+类名)来判斷加载哪个。

应用程序类加载机制作用器Application ClassLoader项目本地路径:自己写的类这个类加载机制作用器是CLassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载機制作用器.一般情况下这就是系统默认的类加载机制作用器.

除此之外,我们还可以加入自己定义的类加载机制作用器,以满足特殊的需求,需要繼承java.lang.ClassLoader类.

不能获取应用程序类加载机制作用器。

双亲委派模型是一种组织类加载机制作用器之间关系的一种规范,他的工作原理是:如果一个类加载机制作用器收到了类加载机制作用的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载机制作用器去完成,这样层层递進,最终所有的加载请求都被传到最顶层的启动类加载机制作用器中,只有当父类加载机制作用器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载机制作用器去尝试加载.

这样的好处是:java类随着它的类加载机制作用器一起具备了带有优先级的层次关系.这昰十分必要的,比如java.langObject,它存放在\jre\lib\rt.jar中,它是所有java类的父类,因此无论哪个类加载机制作用都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载机制作用器中,因此Object类会由启动类加载机制作用器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载机制作用器自荇去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了.

  ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法,该方法将Class文件加载到内存時,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载机制莋用器.

JVM:类加载机制作用、字节码验证(.class)、字节码转化为机器码

Java的全限定名  包名/子包名.类名

Java lang包不用显示导入它是默认加载的。

PS:虽然最近更新频率低了但是思危一直没有停止,共勉!

今天介绍一下 JVM 类加载机制作用器机制主要内容如下:

JVM 把字节码(.class)文件加载到内存中,并对数据进行校验、解析囷初始化最终生成可以被 JVM 直接使用的 Java 类型,这就是 JVM 的类加载机制作用机制在 Java 中各种类型的加载、连接和初始化过程都是在程序运行期間完成的,这种方式会在类加载机制作用时带来一些性能开销但是具有很高的灵活性,Java 的动态扩展的语言特性就是依赖运行期间动态加載和动态链接这个特点实现的如插件化技术中通过自定义类加载机制作用器实现资源的加载和替换,其中就是用的 Java 语言运行期间类加载機制作用的特性

类从被加载到 JVM 内存中开始,一直到从 JVM 内存中卸载为止类加载机制作用的生命周期如下图所示:

加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的,类的解析则不一定可能会在初始化之后再进行,这是为了支持 Java 语言的运行时绑定在整个类加載机制作用的过程中,每一个阶段都由前一个阶段触发进行JVM 规范中规定了类的初始化阶段,但是加载这个阶段没有进行约束具体由 JVM 实現自己控制,当然加载、验证、准备必须在初始化这个阶段之前完成那么什么情况下类开始初始化呢,JVM 严格规定了下面这些情况必须对類进行初始化:

  1. 遇到 new、getstatic/putstatic、invokestatic 指令时如果该类没有被初始化,则需对类进行初始化上面指令分别对应使用 new 关键字进行对象实例化、读取或設置一个静态属性、调用静态方法,具体可以使用 javap 命令查看字节码文件的实现来验证;

  2. 使用 java,lang.reflect 对类进行反射调用的时候如果该类没有被初始化,则需对类进行初始化;

  3. 当初始化一个类的时候如果其父类还没有进行初始化,则先进行该类父类的初始化;

  4. 当 JVM 启动时用户指定偠启动的主类,比如还有 main 方法的类JVM 会先初始化这个类;

下面针对类加载机制作用的几个阶段进行具体说明。

class 文件通过类加载机制作用器將其中的字节码内容加载到内存中将这个静态数据转换成方法区中的运行时数据结构,在堆内存中生成该 class 文件对应的 java.lang.Class 对象这个 Class 对象就昰访问方法区类数据的入口。JVM 规范并没有规定 class 文件的来源举例如下:

  • 从 zip 包中获取,最终成为 jar、war 格式的基础

  • 从网络中获取,典型应用就昰 Applet

  • 其他文件生成、数据库中获取等。

类的加载阶段与后面的链接阶段的过程是交叉进行的没有明确的界限,加载阶段尚未完成链接階段可能已经开始,但是两个阶段的开始时间还是保持着固定的先后顺序

链接包括验证、准备、解析三个阶段,

  • 验证:确保 class 文件的字节鋶中包含的信息符合当前虚拟机的要求且不会危害虚拟机自身的安全,从整体来看验证阶段主要包括文件格式验证、元数据验证、字節码验证和符号引用验证,具体验证内容可以自行查看 Java 虚拟机规范

  • 准备:正式为类变量分配内存并设置类变量的初始值,这个初始值一般是数据类型的初始值而不是真正代码中初始化的值,如 int 初始值就是 0这些类变量使用的内存都将在方法区进行分配,类变量指的就是被 static 关键字修饰过的变量

  • 解析:JVM 将常量池中的符号引用替换为直接引用,这里的符号引用就是在前面验证阶段提到的符号引用验证中的符號引用

类初始化阶段是类加载机制作用阶段的最后一步前面的加载阶段、链接阶段除了用户自定义类加载机制作用其参与外,其余操作嘟是由 JVM 来完成的初始化阶段才真正开始执行 Java 代码,也就是字节码关于类的初始化可以了解一下几点:

  1. 初始化阶段就是执行类构造器 () 方法的过程。

  2. <clinit>() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块 statuc{} 中的语句合并产生的编译器收集顺序和源码中语句顺序┅致,如静态语句块中只能访问定义在它之前的变量定义在它后面的变量只能复制不能访问。
  3. 初始化一个类时如果父类还没初始化,則先进行父类的初始化

  4. JVM 会保证一个类的 () 方法在多线程环境中被正确的加锁、同步。

  5. 当访问一个 Java 类的静态域时只有真正声明这个类才会被初始化。

顾名思义类加载机制作用器(class loader) 用来加载 Java 类到 JVM 中的,所有的类加载机制作用器都是

类的一个实例前面知道在类的加载阶段会通過类加载机制作用器来加载 class 文件,也就是可以通过一个类的全限定名来获取定义此类的二进制字节流 这个动作的代码实现就是类加载机淛作用器的实现。对于任意一个类都需要加载它的类加载机制作用器和这个类本身一同确立其在 JVM 中的唯一性,每个类加载机制作用器都擁有独立的类名称空间也就是说,两个相同的类被不同的类加载机制作用器加载后将不再相等

从 JVM 的角度来说,只存在两种不同的类加載机制作用器:

从 Java 开发人员的角度来说类加载机制作用器可以分为三类:

  1. 启动类加载机制作用器(Bootstrap ClassLoader):负责加载的是 JAVA_HOME\lib 下的类库,或者被-Xbootclasspath 参数所指定的路径中的并且是 JVM 识别的(仅按照文件名识别如 rt.jar 等,名字不符合的类库即使放在 lib 目录中也不会被加载)启动类加载机制作用器无法被JAVA程序直接应用。

  2. 开发者可以直接使用这个类加载机制作用器 如果应用程序中没有自定义过自己的类加载机制作用器, 这个就是程序中默认的类加载机制作用器

先来看一下上面类加载机制作用器的关系:

上图中展示的类加载机制作用器之间的这种层次关系, 称为类加载機制作用器的双亲委派模型( Parents Delegation Model)双亲委派模型要求除了顶层的启动类加载机制作用器外, 其余的类加载机制作用器都应当有自己的父类加载機制作用器 这里类加载机制作用器之间的父子关系一般不会以继承(Inheritance) 的关系

来实现, 而是都使用组合(Composition) 关系来复用父加载器的代码这种方式并不是一个强制性的约束模型, 而是 Java 设计者推荐给开发者的一种类加载机制作用器实现方式那么双亲委托模型的工作流程是什么呢?當一个类加载机制作用器收到类加载机制作用的请求首先不会去加载这个类,而是把这个类加载机制作用的请求委托给父类类加载机制莋用器以此类推,最终每个类加载机制作用的请求都会委托给启动类加载机制作用器只有当父类类加载机制作用器无法完成该类的加載,子类加载机制作用器才会尝试自己去加载加载过程参考如下:

 
JVM 类加载机制作用机制以及类加载机制作用器的相关知识,类加载机制莋用器很好的支持了 Java 的动态扩展特性在 Android 中也有使用,如在插件化技术中用到的 PathClassLoader、DexClassLoader 都是 ClassLoader 的间接子类正是这种对 class 文件来源没有进行限制,基于此可以实现 App 的插件化

JVM 类加载机制作用机制也是一道常見的暖场题 令人感到厌烦的是, 这个类加载机制作用机制的翻译就和“套接字”一样令人感到窒息

大部分的计算机英文术语在命名时, 都会尽可能做到直白易懂 体现技术概念的本质。 但是中文翻译中往往因为翻译者水平有限导致这种信息的丢失, 使得原本直白的概念变得晦涩难懂容易误解。

双亲委派模型 就是一个典型的例子

大部分程序员第一眼看到这个术语, 脑子中必定会浮现这样一种画面:
仩面这个第一印象 再加上百度 “双亲委派模型” 最常见的如下配图, 基本上就足以误导 80 % 80\% 80% 的读者

  • 误解2: 双亲是指两层父结点

正确的翻译(委派模型 或 父委派模型)

关于 jvm 类加载机制作用机制所用的描述是:

java 平台使用 委派模型来加载类。 基本思想就是 每一个类加载机制作用器都有一个父加载器, 当需要加载一个 class
首先把该 class 的查询和加载优先委派给父加载器进行, 如果父加载器无法加载该 class, 再去尝试自行加载這个 class

查找是父引用 parent 是否为空

寻找待加载的 class 物理文件并解析加载为 class 予以返回

寻找是否已经加载过该类

 
 
 
 
 
 
 
 
 

细心的同学应该会有疑问, 加载一个類时 发现该类还继承其他的类,或者方法定义中用到了别的类作为参数或者返回值 会发生什么。

这就涉及到了类的具体加载过程 如丅图, 类的加载过程被从左到右划分为 3 大阶段:

    • 该阶段负责找到待加载类的二进制 class 文件 并把它以 bytecode 的形式装载到虚拟机。 在这个过程中 JVM 會给这个类分配一个基本的内存结构, 但是方法 变量域, 和它引用到的其他类在这个阶段都还没有处理 也就是说, 这个类在目前阶段還不可用
    • 这个步骤又可细分为3个阶段
      • 验证字节码是否是一个正确符合规范的类字节码
      • 为这个类定义好必须的数据结构以表示成员变量域, 方法 以及实现的接口等等
      • 把这个类锁引用的其他类全部加载进来 , 引用的方式有如下几种:
  • 执行类中定义的静态代码块, 初始化静态变量为默认值

隐式加载 vs 显示加载

从上文类加载机制作用的详细过程可以看出 类有两种方式被加载进来

    • 程序主动调用下列类型的方法去主动加载一个类
  • 被显式加载的类对其他类可能存在如下引用:
  • 被引用的类会被动地一并加载至虚拟机, 这种加载方式属于隐式加载

不要把父加載器误解为父类

看过 ClassLoader 的源码以后 会意识到所谓的父加载器, 只是一个简单的成员变量引用 parent 该引用在构造 ClassLoader 时, 由外部传递

正如先前所展礻的 jdk 默认提供了三类内建的类加载机制作用器。


下面代码的输出了不同类的加载器

如何看出三种内置加载器的父子关系

源码为 IDE 反编译获嘚 所以变量名可读性较弱, 但不影响理解

其实上文提到过的 ClassLoader 的源码逻辑提供了答案

查找是父引用 parent 是否为空

寻找待加载的 class 物理文件并解析加载为 class 予以返回

使用了父委派模型后, **类随着它的类加载机制作用器 一起具备了一种层级关系

如果将父加载器的层级视为更高层级的加载器(如上图所示), 那么由于父加载器总是拥有优先加载一个类的机会 那么当不同的 child class loader 试图加载一个属于更高层级的parent class loader 加载范围的 class 时, 該请求总会被转发给对应的最高层级的父加载器

例如应用层级 classpath 中的代码, 是由 AppClassLoader 负责加载的 但是如果有懵懂或邪恶的程序员定义了与 jdk 中嘚核心类同名的类, 如 sun.applet.Main 会发生什么呢 是否会导致项目里面其他使用了这个sun.applet.Main 错误访问到这个由程序员自行定义的类,导致行为异常呢

可以嘗试运行下面自定义的这个类的 main 方法

-debug 在 Java 调试器中启动小应用程序查看器 -J 选项是非标准选项, 如有更改, 恕不另行通知

使用了父委派模型的另┅个影响是, 一个类加载机制作用器只能看到由他自己或是由其父辈加载的类 它自己是看不到更低层级加载器所负责加载的类。

当这种需求出现的时候 可以使用 JDK 提供的另一种类加载机制作用器 ContextClassLoader 予以解决, 这里不做展开描述 有兴趣的同学请自行查阅资料

如何自定义符合父委派模型的类加载机制作用器

如何自定义一个违背父加载模型的类加载机制作用器

以为我们之前自行定义的 sun.applet.Main 为例, 如果我们就是想让这個自定义的类加载机制作用到 JVM 中 并得以执行自定义 main 方法, 该如何自定义一个类加载机制作用器完成该操作

然后自定义类加载机制作用器如下

注意到为了打破父委派模型, 我们重写 loadClass(String name) 方法, 在该方法中 java. 开头的类, 我们还是调用 jdk 提供的加载器去加载因为这些核心类 jdk 做了权限保护, 如果直接尝试加载一个自定义的 java. 开头的核心类 例如

这是 jdk 对于 java. 开头的类加载机制作用的一种权限保护, 确保用户没办法错误或恶意嘚加载自定义的核心 java 类

哪些场景下需要违背父委派模型

目前有不少框架都会自行实现 classLoader 满足一些特定需求, 其中就有一些框架会在一定程喥上违背父加载模型 例如 Tomcat, JNDI、OSGi .

这里分析一下 Tomcat 什么如何违背父委派模型, 以及为什么违背

首先 中描述了其自定义的类加载机制作用器层级关系:

这极大程度上保证了不同 web 项目的独立性和自由度

理解类加载机制作用机制有什么用

应该很多人会疑问, 作为普通程序员 为什么有必要理解类的加载过程 ? 我平时又没有需求要开发自己的类加载机制作用器

JAR Hell 是一个术语, 用于描述由 java 类加载机制作用机制特性而引发的一系列问题

  • 问题一: jar 包对于自身依赖的表达能力缺失
    • 一个 jar 包并没有途径向 JVM 表达它自己依赖哪些其他的 jar 包。 必须有一个外部的实体负责主动的紦相互依赖的 jar 包都添加到类路径下 让 jvm 统一予以加载。 在没有构建工具的时候 程序员需要人工根据文档, 找到相互依赖的 jar 包 将其下载恏, 并把他们添加到项目中
    • JVM 运行时 并不会检查 jar 包中缺失的依赖, 只有当那些依赖需要被访问时 才会直接抛出 NoClassDefFoundError
    • jar A 依赖 jar B , jar B 又依赖 jar C , 这种延伸可以指数级地进行, 导致项目的依赖变得庞大且难以管理
  • 问题三: 同名类的相互遮蔽(Shadowing
    • 回顾前文提到的类的父委派加载机制 可以发现一个重偠的特点, 当一个类加载机制作用器被要求加载一个类时 它首先会查询这个类是否已经被加载过findLoadedClass( myClassName), 如果加载过 就会直接返回
    • 这一特性嘚好处时提升了效率,避免重复加载 坏处是当 classpath 中不同的 jar 包含有相同的类全局限定名时, 只有一个类文件会得到加载 例如: 当一个项目Φ意外引入了多个版本的同一个类库以后, JVM 中具体加载哪一个类就取决于类加载机制作用器会优先访问到哪一个 jar 包 这显然会导致难以排查的异常, 因为 IDE 和 生产环境的类加载机制作用顺序很有可能产生不一致
  • 问题四: 日趋复杂的类加载机制作用模型
    • 由于程序员可以自行实現各种类加载机制作用器, 框架也可以自定义类加载机制作用器 当一个项目中引入了很多会自行创建类加载机制作用器的框架以后, 整個项目的类加载机制作用就会混乱而难以管理

好处二: 利用类加载机制作用机制 实现对第三方库的低侵入式 bug fix

上文提到, 全局限定名相同的類只会被同一个类加载机制作用器加载一次 这容易引发问题, 但也可以用来实现对第三方类库的修改

想象你引用了一个第三方 jar 包, 但昰发现有一点小问题 你希望简单地修改这个 jar 包中的某一个类。 但是这个 jar 包其他项目也在引用 你无权或不便修改。 但你的项目又确实需偠进行这种修改

此时最为便捷的方式, 是把 jar 包中的类拷贝到你的项目中包路径及类名和 jar 中的完全相同, 然后直接进行修改如果类加載机制作用能直接优先加载项目源码中, 你所定义的 class 文件而不再使用 jar 包中的那个类文件, 不是极好的吗

中所引用的第三方 jar 包进行类的替换 --》 想替换或修改哪个类, 就在项目下面自定义一个包路径相同的同名类 自由修改。 jvm 运行时 只会加载这个我们自定义的类, 忽略 jar 包Φ原始的那个类

我要回帖

更多关于 类加载机制作用 的文章

 

随机推荐